summaryrefslogtreecommitdiffstats
path: root/remote/targets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /remote/targets
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/targets')
-rw-r--r--remote/targets/MainProcessTarget.jsm59
-rw-r--r--remote/targets/TabTarget.jsm164
-rw-r--r--remote/targets/Target.jsm74
-rw-r--r--remote/targets/TargetList.jsm173
4 files changed, 470 insertions, 0 deletions
diff --git a/remote/targets/MainProcessTarget.jsm b/remote/targets/MainProcessTarget.jsm
new file mode 100644
index 0000000000..f723edc93a
--- /dev/null
+++ b/remote/targets/MainProcessTarget.jsm
@@ -0,0 +1,59 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MainProcessTarget"];
+
+const { Target } = ChromeUtils.import(
+ "chrome://remote/content/targets/Target.jsm"
+);
+const { MainProcessSession } = ChromeUtils.import(
+ "chrome://remote/content/sessions/MainProcessSession.jsm"
+);
+const { RemoteAgent } = ChromeUtils.import(
+ "chrome://remote/content/RemoteAgent.jsm"
+);
+
+/**
+ * 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
+ */
+class MainProcessTarget extends Target {
+ /*
+ * @param Targets targets
+ */
+ constructor(targets) {
+ super(targets, MainProcessSession);
+
+ this.type = "browser";
+
+ // Define the HTTP path to query this target
+ this.path = `/devtools/browser/${this.id}`;
+ }
+
+ get wsDebuggerURL() {
+ const { host, port } = 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/targets/TabTarget.jsm b/remote/targets/TabTarget.jsm
new file mode 100644
index 0000000000..fd672677aa
--- /dev/null
+++ b/remote/targets/TabTarget.jsm
@@ -0,0 +1,164 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TabTarget"];
+
+const { Target } = ChromeUtils.import(
+ "chrome://remote/content/targets/Target.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TabSession } = ChromeUtils.import(
+ "chrome://remote/content/sessions/TabSession.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { RemoteAgent } = ChromeUtils.import(
+ "chrome://remote/content/RemoteAgent.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "Favicons",
+ "@mozilla.org/browser/favicon-service;1",
+ "nsIFaviconService"
+);
+
+/**
+ * Target for a local tab or a remoted frame.
+ */
+class TabTarget extends Target {
+ /**
+ * @param Targets targets
+ * @param BrowserElement browser
+ */
+ constructor(targets, browser) {
+ super(targets, TabSession);
+
+ this.browser = 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.
+ *
+ * @return {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;
+ }
+
+ /** @return {Promise.<String=>} */
+ get faviconUrl() {
+ return new Promise((resolve, reject) => {
+ 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 } = 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,
+ browsingContextId: this.browsingContext.id,
+ 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/targets/Target.jsm b/remote/targets/Target.jsm
new file mode 100644
index 0000000000..b413b4b220
--- /dev/null
+++ b/remote/targets/Target.jsm
@@ -0,0 +1,74 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Target"];
+
+const { Connection } = ChromeUtils.import(
+ "chrome://remote/content/Connection.jsm"
+);
+const { WebSocketTransport } = ChromeUtils.import(
+ "chrome://remote/content/server/WebSocketTransport.jsm"
+);
+const { WebSocketHandshake } = ChromeUtils.import(
+ "chrome://remote/content/server/WebSocketHandshake.jsm"
+);
+
+const UUIDGen = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+/**
+ * Base class for all the Targets.
+ */
+class Target {
+ /**
+ * @param Targets targets
+ * @param Class sessionClass
+ */
+ constructor(targets, sessionClass) {
+ // Save a reference to Targets instance in order to expose it to:
+ // domains/parent/Target.jsm
+ this.targets = targets;
+
+ // 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 = UUIDGen.generateUUID()
+ .toString()
+ .slice(1, -1);
+ }
+
+ /**
+ * Close all active connections made to this target.
+ */
+ destructor() {
+ for (const conn of this.connections) {
+ conn.close();
+ }
+ }
+
+ // nsIHttpRequestHandler
+
+ async handle(request, response) {
+ const so = await WebSocketHandshake.upgrade(request, response);
+ const transport = new WebSocketTransport(so);
+ const conn = new Connection(transport, 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/targets/TargetList.jsm b/remote/targets/TargetList.jsm
new file mode 100644
index 0000000000..7117fbbee4
--- /dev/null
+++ b/remote/targets/TargetList.jsm
@@ -0,0 +1,173 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TargetList"];
+
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+const { TabTarget } = ChromeUtils.import(
+ "chrome://remote/content/targets/TabTarget.jsm"
+);
+const { MainProcessTarget } = ChromeUtils.import(
+ "chrome://remote/content/targets/MainProcessTarget.jsm"
+);
+const { TabObserver } = ChromeUtils.import(
+ "chrome://remote/content/observers/TargetObserver.jsm"
+);
+
+class TargetList {
+ constructor() {
+ // Target ID -> Target
+ this._targets = new Map();
+
+ 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 TabObserver({ registerExisting: true });
+ this.tabObserver.on("open", async (eventName, tab) => {
+ const target = new TabTarget(this, tab.linkedBrowser);
+ this.registerTarget(target);
+ });
+ this.tabObserver.on("close", (eventName, tab) => {
+ const browser = tab.linkedBrowser;
+ // Ignore the browsers that haven't had time to initialize.
+ if (!browser.browsingContext) {
+ return;
+ }
+ const target = this.getByBrowsingContext(browser.browsingContext.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
+ *
+ * @return {Target}
+ */
+ getById(id) {
+ return this._targets.get(id);
+ }
+
+ /**
+ * Get Target instance by browsing context id
+ *
+ * @param {number} id
+ * browsing context id
+ *
+ * @return {Target}
+ */
+ getByBrowsingContext(id) {
+ let rv;
+ for (const target of this._targets.values()) {
+ if (target.browsingContext && target.browsingContext.id === id) {
+ rv = target;
+ break;
+ }
+ }
+ return rv;
+ }
+
+ /**
+ * 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 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}]`;
+ }
+}