diff options
Diffstat (limited to 'remote/cdp/sessions')
-rw-r--r-- | remote/cdp/sessions/ContentProcessSession.sys.mjs | 104 | ||||
-rw-r--r-- | remote/cdp/sessions/MainProcessSession.sys.mjs | 12 | ||||
-rw-r--r-- | remote/cdp/sessions/Session.sys.mjs | 80 | ||||
-rw-r--r-- | remote/cdp/sessions/TabSession.sys.mjs | 150 | ||||
-rw-r--r-- | remote/cdp/sessions/frame-script.js | 13 |
5 files changed, 359 insertions, 0 deletions
diff --git a/remote/cdp/sessions/ContentProcessSession.sys.mjs b/remote/cdp/sessions/ContentProcessSession.sys.mjs new file mode 100644 index 0000000000..d7aa3de57b --- /dev/null +++ b/remote/cdp/sessions/ContentProcessSession.sys.mjs @@ -0,0 +1,104 @@ +/* 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, { + ContentProcessDomains: + "chrome://remote/content/cdp/domains/ContentProcessDomains.sys.mjs", + ContextObserver: + "chrome://remote/content/cdp/observers/ContextObserver.sys.mjs", + DomainCache: "chrome://remote/content/cdp/domains/DomainCache.sys.mjs", +}); + +export class ContentProcessSession { + constructor(messageManager, browsingContext, content, docShell) { + this.messageManager = messageManager; + this.browsingContext = browsingContext; + this.content = content; + this.docShell = docShell; + // Most children or sibling classes are going to assume that docShell + // implements the following interface. So do the QI only once from here. + this.docShell.QueryInterface(Ci.nsIWebNavigation); + + this.domains = new lazy.DomainCache(this, lazy.ContentProcessDomains); + this.messageManager.addMessageListener("remote:request", this); + this.messageManager.addMessageListener("remote:destroy", this); + } + + destructor() { + this._contextObserver?.destructor(); + + this.messageManager.removeMessageListener("remote:request", this); + this.messageManager.removeMessageListener("remote:destroy", this); + this.domains.clear(); + } + + get contextObserver() { + if (!this._contextObserver) { + this._contextObserver = new lazy.ContextObserver( + this.docShell.chromeEventHandler + ); + } + return this._contextObserver; + } + + // Domain event listener + + onEvent(eventName, params) { + this.messageManager.sendAsyncMessage("remote:event", { + browsingContextId: this.browsingContext.id, + event: { + eventName, + params, + }, + }); + } + + // nsIMessageListener + + async receiveMessage({ name, data }) { + const { browsingContextId } = data; + + // We may have more than one tab loaded in the same process, + // and debug the two at the same time. We want to ensure not + // mixing up the requests made against two such tabs. + // Each tab is going to have its own frame script instance + // and two communication channels are going to be set up via + // the two message managers. + if (browsingContextId != this.browsingContext.id) { + return; + } + + switch (name) { + case "remote:request": + try { + const { id, domain, command, params } = data.request; + + const result = await this.domains.execute(domain, command, params); + + this.messageManager.sendAsyncMessage("remote:result", { + browsingContextId, + id, + result, + }); + } catch (e) { + this.messageManager.sendAsyncMessage("remote:error", { + browsingContextId, + id: data.request.id, + error: { + name: e.name || "exception", + message: e.message || String(e), + stack: e.stack, + }, + }); + } + break; + + case "remote:destroy": + this.destructor(); + break; + } + } +} diff --git a/remote/cdp/sessions/MainProcessSession.sys.mjs b/remote/cdp/sessions/MainProcessSession.sys.mjs new file mode 100644 index 0000000000..259e1312ac --- /dev/null +++ b/remote/cdp/sessions/MainProcessSession.sys.mjs @@ -0,0 +1,12 @@ +/* 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 { Session } from "chrome://remote/content/cdp/sessions/Session.sys.mjs"; + +/** + * A session, dedicated to the main process target. + * For some reason, it doesn't need any specific code and can share the base Session class + * aside TabSession. + */ +export class MainProcessSession extends Session {} diff --git a/remote/cdp/sessions/Session.sys.mjs b/remote/cdp/sessions/Session.sys.mjs new file mode 100644 index 0000000000..cbc82fb097 --- /dev/null +++ b/remote/cdp/sessions/Session.sys.mjs @@ -0,0 +1,80 @@ +/* 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, { + DomainCache: "chrome://remote/content/cdp/domains/DomainCache.sys.mjs", + NetworkObserver: + "chrome://remote/content/cdp/observers/NetworkObserver.sys.mjs", + ParentProcessDomains: + "chrome://remote/content/cdp/domains/ParentProcessDomains.sys.mjs", +}); + +/** + * A session represents exactly one client WebSocket connection. + * + * Every new WebSocket connections is associated with one session that + * deals with dispatching incoming command requests to the right + * target, sending back responses, and propagating events originating + * from domains. + * Then, some subsequent Sessions may be created over a single WebSocket + * connection. In this case, the subsequent session will have an `id` + * being passed to their constructor and each packet of these sessions + * will have a `sessionId` attribute in order to filter the packets + * by session both on client and server side. + */ +export class Session { + /** + * @param {Connection} connection + * The connection used to communicate with the server. + * @param {Target} target + * The target to which this session communicates with. + * @param {number=} id + * If this session isn't the default one used for the HTTP endpoint we + * connected to, the session requires an id to distinguish it from the default + * one. This id is used to filter our request, responses and events between + * all active sessions. For now, this is only passed by `Target.attachToTarget()`. + */ + constructor(connection, target, id) { + this.connection = connection; + this.target = target; + this.id = id; + + this.domains = new lazy.DomainCache(this, lazy.ParentProcessDomains); + } + + destructor() { + if ( + this.networkObserver && + this.networkObserver.isActive(this.target.browser) + ) { + this.networkObserver.dispose(); + } + this.domains.clear(); + } + + execute(id, domain, command, params) { + return this.domains.execute(domain, command, params); + } + + get networkObserver() { + if (!this._networkObserver) { + this._networkObserver = new lazy.NetworkObserver(); + } + return this._networkObserver; + } + + /** + * Domains event listener. Called when an event is fired + * by any Domain and has to be sent to the client. + */ + onEvent(eventName, params) { + this.connection.sendEvent(eventName, params, this.id); + } + + toString() { + return `[object ${this.constructor.name} ${this.connection.id}]`; + } +} diff --git a/remote/cdp/sessions/TabSession.sys.mjs b/remote/cdp/sessions/TabSession.sys.mjs new file mode 100644 index 0000000000..90d098fba4 --- /dev/null +++ b/remote/cdp/sessions/TabSession.sys.mjs @@ -0,0 +1,150 @@ +/* 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 { Session } from "chrome://remote/content/cdp/sessions/Session.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.CDP) +); + +/** + * A session to communicate with a given tab + */ +export class TabSession extends Session { + /** + * @param {Connection} connection + * The connection used to communicate with the server. + * @param {TabTarget} target + * The tab target to which this session communicates with. + * @param {number=} id + * If this session isn't the default one used for the HTTP endpoint we + * connected to, the session requires an id to distinguish it from the default + * one. This id is used to filter our request, responses and events between + * all active sessions. + * For now, this is only passed by `Target.attachToTarget()`. + * Otherwise it will be undefined when you are connecting directly to + * a given Tab. i.e. connect directly to the WebSocket URL provided by + * /json/list HTTP endpoint. + */ + constructor(connection, target, id) { + super(connection, target, id); + + // Request id => { resolve, reject } + this.requestPromises = new Map(); + + this.registerFramescript(this.mm); + + this.target.browser.addEventListener("XULFrameLoaderCreated", this); + } + + destructor() { + super.destructor(); + + this.requestPromises.clear(); + + this.target.browser.removeEventListener("XULFrameLoaderCreated", this); + + // this.mm might be null if the browser of the TabTarget was already closed. + // See Bug 1747301. + this.mm?.sendAsyncMessage("remote:destroy", { + browsingContextId: this.browsingContext.id, + }); + + this.mm?.removeMessageListener("remote:event", this); + this.mm?.removeMessageListener("remote:result", this); + this.mm?.removeMessageListener("remote:error", this); + } + + execute(id, domain, command, params) { + // Check if the domain and command is implemented in the parent + // and execute it there. Otherwise forward the command to the content process + // in order to try to execute it in the content process. + if (this.domains.domainSupportsMethod(domain, command)) { + return super.execute(id, domain, command, params); + } + return this.executeInChild(id, domain, command, params); + } + + executeInChild(id, domain, command, params) { + return new Promise((resolve, reject) => { + // Save the promise's resolution and rejection handler in order to later + // resolve this promise once we receive the reply back from the content process. + this.requestPromises.set(id, { resolve, reject }); + + this.mm.sendAsyncMessage("remote:request", { + browsingContextId: this.browsingContext.id, + request: { id, domain, command, params }, + }); + }); + } + + get mm() { + return this.target.mm; + } + + get browsingContext() { + return this.target.browsingContext; + } + + /** + * Register the framescript and listeners for the given message manager. + * + * @param {MessageManager} messageManager + * The message manager to use. + */ + registerFramescript(messageManager) { + messageManager.loadFrameScript( + "chrome://remote/content/cdp/sessions/frame-script.js", + false + ); + + messageManager.addMessageListener("remote:event", this); + messageManager.addMessageListener("remote:result", this); + messageManager.addMessageListener("remote:error", this); + } + + // Event handler + handleEvent = function ({ target, type }) { + switch (type) { + case "XULFrameLoaderCreated": + if (target === this.target.browser) { + lazy.logger.trace("Remoteness change detected"); + this.registerFramescript(this.mm); + } + break; + } + }; + + // nsIMessageListener + + receiveMessage({ name, data }) { + const { id, result, event, error } = data; + + switch (name) { + case "remote:result": + const { resolve } = this.requestPromises.get(id); + resolve(result); + this.requestPromises.delete(id); + break; + + case "remote:event": + this.connection.sendEvent(event.eventName, event.params, this.id); + break; + + case "remote:error": + const { reject } = this.requestPromises.get(id); + reject(error); + this.requestPromises.delete(id); + break; + } + } +} diff --git a/remote/cdp/sessions/frame-script.js b/remote/cdp/sessions/frame-script.js new file mode 100644 index 0000000000..88071086cb --- /dev/null +++ b/remote/cdp/sessions/frame-script.js @@ -0,0 +1,13 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +const { ContentProcessSession } = ChromeUtils.importESModule( + "chrome://remote/content/cdp/sessions/ContentProcessSession.sys.mjs" +); + +new ContentProcessSession(this, docShell.browsingContext, content, docShell); |