diff options
Diffstat (limited to 'remote/cdp/targets')
-rw-r--r-- | remote/cdp/targets/MainProcessTarget.sys.mjs | 55 | ||||
-rw-r--r-- | remote/cdp/targets/TabTarget.sys.mjs | 161 | ||||
-rw-r--r-- | remote/cdp/targets/Target.sys.mjs | 62 | ||||
-rw-r--r-- | remote/cdp/targets/TargetList.sys.mjs | 159 |
4 files changed, 437 insertions, 0 deletions
diff --git a/remote/cdp/targets/MainProcessTarget.sys.mjs b/remote/cdp/targets/MainProcessTarget.sys.mjs new file mode 100644 index 0000000000..1f32b5bde5 --- /dev/null +++ b/remote/cdp/targets/MainProcessTarget.sys.mjs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Target } from "chrome://remote/content/cdp/targets/Target.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + MainProcessSession: + "chrome://remote/content/cdp/sessions/MainProcessSession.sys.mjs", + RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", +}); + +/** + * The main process Target. + * + * Matches BrowserDevToolsAgentHost from chromium, and only support a couple of Domains: + * https://cs.chromium.org/chromium/src/content/browser/devtools/browser_devtools_agent_host.cc?dr=CSs&g=0&l=80-91 + */ +export class MainProcessTarget extends Target { + /* + * @param TargetList targetList + */ + constructor(targetList) { + super(targetList, lazy.MainProcessSession); + + this.type = "browser"; + + // Define the HTTP path to query this target + this.path = `/devtools/browser/${this.id}`; + } + + get wsDebuggerURL() { + const { host, port } = lazy.RemoteAgent; + return `ws://${host}:${port}${this.path}`; + } + + toString() { + return `[object MainProcessTarget]`; + } + + toJSON() { + return { + description: "Main process target", + devtoolsFrontendUrl: "", + faviconUrl: "", + id: this.id, + title: "Main process target", + type: this.type, + url: "", + webSocketDebuggerUrl: this.wsDebuggerURL, + }; + } +} diff --git a/remote/cdp/targets/TabTarget.sys.mjs b/remote/cdp/targets/TabTarget.sys.mjs new file mode 100644 index 0000000000..a35b0c3f07 --- /dev/null +++ b/remote/cdp/targets/TabTarget.sys.mjs @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { Target } from "chrome://remote/content/cdp/targets/Target.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + TabSession: "chrome://remote/content/cdp/sessions/TabSession.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "Favicons", + "@mozilla.org/browser/favicon-service;1", + "nsIFaviconService" +); + +/** + * Target for a local tab or a remoted frame. + */ +export class TabTarget extends Target { + /** + * @param {TargetList} targetList + * @param {BrowserElement} browser + */ + constructor(targetList, browser) { + super(targetList, lazy.TabSession); + + this.browser = browser; + + // The tab target uses a unique id as shared with WebDriver to reference + // a specific tab. + this.id = lazy.TabManager.getIdForBrowser(browser); + + // Define the HTTP path to query this target + this.path = `/devtools/page/${this.id}`; + + Services.obs.addObserver(this, "message-manager-disconnect"); + } + + destructor() { + Services.obs.removeObserver(this, "message-manager-disconnect"); + super.destructor(); + } + + get browserContextId() { + return parseInt(this.browser.getAttribute("usercontextid")); + } + + get browsingContext() { + return this.browser.browsingContext; + } + + get mm() { + return this.browser.messageManager; + } + + get window() { + return this.browser.ownerGlobal; + } + + get tab() { + return this.window.gBrowser.getTabForBrowser(this.browser); + } + + /** + * Determines if the content browser remains attached + * to its parent chrome window. + * + * We determine this by checking if the <browser> element + * is still attached to the DOM. + * + * @returns {boolean} + * True if target's browser is still attached, + * false if it has been disconnected. + */ + get closed() { + return !this.browser || !this.browser.isConnected; + } + + get description() { + return ""; + } + + get frontendURL() { + return null; + } + + /** @returns {Promise<string|null>} */ + get faviconUrl() { + return new Promise((resolve, reject) => { + lazy.Favicons.getFaviconURLForPage(this.browser.currentURI, url => { + if (url) { + resolve(url.spec); + } else { + resolve(null); + } + }); + }); + } + + get title() { + return this.browsingContext.currentWindowGlobal.documentTitle; + } + + get type() { + return "page"; + } + + get url() { + return this.browser.currentURI.spec; + } + + get wsDebuggerURL() { + const { host, port } = lazy.RemoteAgent; + return `ws://${host}:${port}${this.path}`; + } + + toString() { + return `[object Target ${this.id}]`; + } + + toJSON() { + return { + description: this.description, + devtoolsFrontendUrl: this.frontendURL, + // TODO(ato): toJSON cannot be marked async )-: + faviconUrl: "", + id: this.id, + // Bug 1680817: Fails to encode some UTF-8 characters + // title: this.title, + type: this.type, + url: this.url, + webSocketDebuggerUrl: this.wsDebuggerURL, + }; + } + + // nsIObserver + + observe(subject, topic, data) { + if (subject === this.mm && subject == "message-manager-disconnect") { + // disconnect debugging target if <browser> is disconnected, + // otherwise this is just a host process change + if (this.closed) { + this.disconnect(); + } + } + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIHttpRequestHandler", "nsIObserver"]); + } +} diff --git a/remote/cdp/targets/Target.sys.mjs b/remote/cdp/targets/Target.sys.mjs new file mode 100644 index 0000000000..9264110c37 --- /dev/null +++ b/remote/cdp/targets/Target.sys.mjs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CDPConnection: "chrome://remote/content/cdp/CDPConnection.sys.mjs", + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + WebSocketHandshake: + "chrome://remote/content/server/WebSocketHandshake.sys.mjs", +}); + +/** + * Base class for all the targets. + */ +export class Target { + /** + * @param {TargetList} targetList + * @param {Class} sessionClass + */ + constructor(targetList, sessionClass) { + // Save a reference to TargetList instance in order to expose it to: + // domains/parent/Target.jsm + this.targetList = targetList; + + // When a new connection is made to this target, + // we will instantiate a new session based on this given class. + // The session class is specific to each target's kind and is passed + // by the inheriting class. + this.sessionClass = sessionClass; + + // There can be more than one connection if multiple clients connect to the remote agent. + this.connections = new Set(); + this.id = lazy.generateUUID(); + } + + /** + * Close all active connections made to this target. + */ + destructor() { + for (const conn of this.connections) { + conn.close(); + } + } + + // nsIHttpRequestHandler + + async handle(request, response) { + const webSocket = await lazy.WebSocketHandshake.upgrade(request, response); + const conn = new lazy.CDPConnection(webSocket, response._connection); + const session = new this.sessionClass(conn, this); + conn.registerSession(session); + this.connections.add(conn); + } + + // XPCOM + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIHttpRequestHandler"]); + } +} diff --git a/remote/cdp/targets/TargetList.sys.mjs b/remote/cdp/targets/TargetList.sys.mjs new file mode 100644 index 0000000000..a68b36763f --- /dev/null +++ b/remote/cdp/targets/TargetList.sys.mjs @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + MainProcessTarget: + "chrome://remote/content/cdp/targets/MainProcessTarget.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + TabObserver: "chrome://remote/content/cdp/observers/TargetObserver.sys.mjs", + TabTarget: "chrome://remote/content/cdp/targets/TabTarget.sys.mjs", +}); + +export class TargetList { + constructor() { + // Target ID -> Target + this._targets = new Map(); + + lazy.EventEmitter.decorate(this); + } + + /** + * Start listing and listening for all the debuggable targets + */ + async watchForTargets() { + await this.watchForTabs(); + } + + unwatchForTargets() { + this.unwatchForTabs(); + } + + /** + * Watch for all existing and new tabs being opened. + * So that we can create the related TabTarget instance for + * each of them. + */ + async watchForTabs() { + if (this.tabObserver) { + throw new Error("Targets is already watching for new tabs"); + } + + this.tabObserver = new lazy.TabObserver({ registerExisting: true }); + + // Handle creation of tab targets for opened tabs. + this.tabObserver.on("open", async (eventName, tab) => { + const target = new lazy.TabTarget(this, tab.linkedBrowser); + this.registerTarget(target); + }); + + // Handle removal of tab targets when tabs are closed. + this.tabObserver.on("close", (eventName, tab) => { + const browser = tab.linkedBrowser; + + // Ignore unloaded tabs. + if (browser.browserId === 0) { + return; + } + + const id = lazy.TabManager.getIdForBrowser(browser); + const target = Array.from(this._targets.values()).find( + target => target.id == id + ); + if (target) { + this.destroyTarget(target); + } + }); + await this.tabObserver.start(); + } + + unwatchForTabs() { + if (this.tabObserver) { + this.tabObserver.stop(); + this.tabObserver = null; + } + } + + /** + * To be called right after instantiating a new Target instance. + * This will hold the new instance in the list and notify about + * its creation. + */ + registerTarget(target) { + this._targets.set(target.id, target); + this.emit("target-created", target); + } + + /** + * To be called when the debuggable target has been destroy. + * So that we can notify it no longer exists and disconnect + * all connecting made to debug it. + */ + destroyTarget(target) { + target.destructor(); + this._targets.delete(target.id); + this.emit("target-destroyed", target); + } + + /** + * Destroy all the registered target of all kinds. + * This will end up dropping all connections made to debug any of them. + */ + destructor() { + for (const target of this) { + this.destroyTarget(target); + } + this._targets.clear(); + if (this.mainProcessTarget) { + this.mainProcessTarget = null; + } + + this.unwatchForTargets(); + } + + get size() { + return this._targets.size; + } + + /** + * Get Target instance by target id + * + * @param {string} id + * Target id + * + * @returns {Target} + */ + getById(id) { + return this._targets.get(id); + } + + /** + * Get the Target instance for the main process. + * This target is a singleton and only exposes a subset of domains. + */ + getMainProcessTarget() { + if (!this.mainProcessTarget) { + this.mainProcessTarget = new lazy.MainProcessTarget(this); + this.registerTarget(this.mainProcessTarget); + } + return this.mainProcessTarget; + } + + *[Symbol.iterator]() { + for (const target of this._targets.values()) { + yield target; + } + } + + toJSON() { + return [...this]; + } + + toString() { + return `[object TargetList ${this.size}]`; + } +} |