summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/browser-toolbox/Launcher.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/browser-toolbox/Launcher.sys.mjs')
-rw-r--r--devtools/client/framework/browser-toolbox/Launcher.sys.mjs467
1 files changed, 467 insertions, 0 deletions
diff --git a/devtools/client/framework/browser-toolbox/Launcher.sys.mjs b/devtools/client/framework/browser-toolbox/Launcher.sys.mjs
new file mode 100644
index 0000000000..a619fbdff2
--- /dev/null
+++ b/devtools/client/framework/browser-toolbox/Launcher.sys.mjs
@@ -0,0 +1,467 @@
+/* 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";
+
+// Keep this synchronized with the value of the same name in
+// toolkit/xre/nsAppRunner.cpp.
+const BROWSER_TOOLBOX_WINDOW_URL =
+ "chrome://devtools/content/framework/browser-toolbox/window.html";
+const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";
+import {
+ useDistinctSystemPrincipalLoader,
+ releaseDistinctSystemPrincipalLoader,
+} from "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs";
+import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ XreDirProvider: [
+ "@mozilla.org/xre/directory-provider;1",
+ "nsIXREDirProvider",
+ ],
+});
+
+const Telemetry = require("resource://devtools/client/shared/telemetry.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const processes = new Set();
+
+/**
+ * @typedef {Object} BrowserToolboxLauncherArgs
+ * @property {function} onRun - A function called when the process starts running.
+ * @property {boolean} overwritePreferences - Set to force overwriting the toolbox
+ * profile's preferences with the current set of preferences.
+ * @property {boolean} forceMultiprocess - Set to force the Browser Toolbox to be in
+ * multiprocess mode.
+ */
+
+export class BrowserToolboxLauncher extends EventEmitter {
+ /**
+ * Initializes and starts a chrome toolbox process if the appropriated prefs are enabled
+ *
+ * @param {BrowserToolboxLauncherArgs} args
+ * @return {BrowserToolboxLauncher|null} The created instance, or null if the required prefs
+ * are not set.
+ */
+ static init(args) {
+ if (
+ !Services.prefs.getBoolPref("devtools.chrome.enabled") ||
+ !Services.prefs.getBoolPref("devtools.debugger.remote-enabled")
+ ) {
+ console.error("Could not start Browser Toolbox, you need to enable it.");
+ return null;
+ }
+ return new BrowserToolboxLauncher(args);
+ }
+
+ /**
+ * Figure out if there are any open Browser Toolboxes that'll need to be restored.
+ * @return {boolean}
+ */
+ static getBrowserToolboxSessionState() {
+ return processes.size !== 0;
+ }
+
+ #closed;
+ #devToolsServer;
+ #dbgProfilePath;
+ #dbgProcess;
+ #listener;
+ #loader;
+ #port;
+ #telemetry = new Telemetry();
+
+ /**
+ * Constructor for creating a process that will hold a chrome toolbox.
+ *
+ * @param {...BrowserToolboxLauncherArgs} args
+ */
+ constructor({ forceMultiprocess, onRun, overwritePreferences } = {}) {
+ super();
+
+ if (onRun) {
+ this.once("run", onRun);
+ }
+
+ this.close = this.close.bind(this);
+ Services.obs.addObserver(this.close, "quit-application");
+ this.#initServer();
+ this.#initProfile(overwritePreferences);
+ this.#create({ forceMultiprocess });
+
+ processes.add(this);
+ }
+
+ /**
+ * Initializes the devtools server.
+ */
+ #initServer() {
+ if (this.#devToolsServer) {
+ dumpn("The chrome toolbox server is already running.");
+ return;
+ }
+
+ dumpn("Initializing the chrome toolbox server.");
+
+ // Create a separate loader instance, so that we can be sure to receive a
+ // separate instance of the DebuggingServer from the rest of the devtools.
+ // This allows us to safely use the tools against even the actors and
+ // DebuggingServer itself, especially since we can mark this loader as
+ // invisible to the debugger (unlike the usual loader settings).
+ this.#loader = useDistinctSystemPrincipalLoader(this);
+ const { DevToolsServer } = this.#loader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+ const { SocketListener } = this.#loader.require(
+ "resource://devtools/shared/security/socket.js"
+ );
+ this.#devToolsServer = DevToolsServer;
+ dumpn("Created a separate loader instance for the DevToolsServer.");
+
+ this.#devToolsServer.init();
+ // We mainly need a root actor and target actors for opening a toolbox, even
+ // against chrome/content. But the "no auto hide" button uses the
+ // preference actor, so also register the browser actors.
+ this.#devToolsServer.registerAllActors();
+ this.#devToolsServer.allowChromeProcess = true;
+ dumpn("initialized and added the browser actors for the DevToolsServer.");
+
+ const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
+ Ci.nsIBackgroundTasks
+ );
+ if (bts?.isBackgroundTaskMode) {
+ // A special root actor, just for background tasks invoked with
+ // `--backgroundtask TASK --jsdebugger`.
+ const { createRootActor } = this.#loader.require(
+ "resource://gre/modules/backgroundtasks/dbg-actors.js"
+ );
+ this.#devToolsServer.setRootActor(createRootActor);
+ }
+
+ const chromeDebuggingWebSocket = Services.prefs.getBoolPref(
+ "devtools.debugger.chrome-debugging-websocket"
+ );
+ const socketOptions = {
+ fromBrowserToolbox: true,
+ portOrPath: -1,
+ webSocket: chromeDebuggingWebSocket,
+ };
+ const listener = new SocketListener(this.#devToolsServer, socketOptions);
+ listener.open();
+ this.#listener = listener;
+ this.#port = listener.port;
+
+ if (!this.#port) {
+ throw new Error("No devtools server port");
+ }
+
+ dumpn("Finished initializing the chrome toolbox server.");
+ dump(
+ `DevTools Server for Browser Toolbox listening on port: ${this.#port}\n`
+ );
+ }
+
+ /**
+ * Initializes a profile for the remote debugger process.
+ */
+ #initProfile(overwritePreferences) {
+ dumpn("Initializing the chrome toolbox user profile.");
+
+ const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
+ Ci.nsIBackgroundTasks
+ );
+
+ let debuggingProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ if (bts?.isBackgroundTaskMode) {
+ // Background tasks run with a temporary ephemeral profile. We move the
+ // browser toolbox profile out of that ephemeral profile so that it has
+ // alonger life then the background task profile. This preserves
+ // breakpoints, etc, across repeated debugging invocations. This
+ // directory is close to the background task temporary profile name(s),
+ // but doesn't match the prefix that will get purged by the stale
+ // ephemeral profile cleanup mechanism.
+ //
+ // For example, the invocation
+ // `firefox --backgroundtask success --jsdebugger --wait-for-jsdebugger`
+ // might run with ephemeral profile
+ // `/tmp/MozillaBackgroundTask-<HASH>-success`
+ // and sibling directory browser toolbox profile
+ // `/tmp/MozillaBackgroundTask-<HASH>-chrome_debugger_profile-success`
+ //
+ // See `BackgroundTasks::Shutdown` for ephemeral profile cleanup details.
+ debuggingProfileDir = debuggingProfileDir.parent;
+ debuggingProfileDir.append(
+ `${Services.appinfo.vendor}BackgroundTask-` +
+ `${lazy.XreDirProvider.getInstallHash()}-${CHROME_DEBUGGER_PROFILE_NAME}-${bts.backgroundTaskName()}`
+ );
+ } else {
+ debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
+ }
+ try {
+ debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (ex) {
+ if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ if (!overwritePreferences) {
+ this.#dbgProfilePath = debuggingProfileDir.path;
+ return;
+ }
+ // Fall through and copy the current set of prefs to the profile.
+ } else {
+ dumpn("Error trying to create a profile directory, failing.");
+ dumpn("Error: " + (ex.message || ex));
+ return;
+ }
+ }
+
+ this.#dbgProfilePath = debuggingProfileDir.path;
+
+ // We would like to copy prefs into this new profile...
+ const prefsFile = debuggingProfileDir.clone();
+ prefsFile.append("prefs.js");
+
+ if (bts?.isBackgroundTaskMode) {
+ // Background tasks run under a temporary profile. In order to set
+ // preferences for the launched browser toolbox, take the preferences from
+ // the default profile. This is the standard pattern for controlling
+ // background task settings. Without this, there'd be no way to increase
+ // logging in the browser toolbox process, etc.
+ const defaultProfile = lazy.BackgroundTasksUtils.getDefaultProfile();
+ if (!defaultProfile) {
+ throw new Error(
+ "Cannot start Browser Toolbox from background task with no default profile"
+ );
+ }
+
+ const defaultPrefsFile = defaultProfile.rootDir.clone();
+ defaultPrefsFile.append("prefs.js");
+ defaultPrefsFile.copyTo(prefsFile.parent, prefsFile.leafName);
+
+ dumpn(
+ `Copied browser toolbox prefs at '${prefsFile.path}'` +
+ ` from default profiles prefs at '${defaultPrefsFile.path}'`
+ );
+ } else {
+ // ... but unfortunately, when we run tests, it seems the starting profile
+ // clears out the prefs file before re-writing it, and in practice the
+ // file is empty when we get here. So just copying doesn't work in that
+ // case.
+ // We could force a sync pref flush and then copy it... but if we're doing
+ // that, we might as well just flush directly to the new profile, which
+ // always works:
+ Services.prefs.savePrefFile(prefsFile);
+ }
+
+ dumpn(
+ "Finished creating the chrome toolbox user profile at: " +
+ this.#dbgProfilePath
+ );
+ }
+
+ /**
+ * Creates and initializes the profile & process for the remote debugger.
+ *
+ * @param {Object} options
+ * @param {boolean} options.forceMultiprocess: Set to true to force the Browser Toolbox to be in
+ * multiprocess mode.
+ */
+ #create({ forceMultiprocess } = {}) {
+ dumpn("Initializing chrome debugging process.");
+
+ let command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
+ let profilePath = this.#dbgProfilePath;
+
+ // MOZ_BROWSER_TOOLBOX_BINARY is an absolute file path to a custom firefox binary.
+ // This is especially useful when debugging debug builds which are really slow
+ // so that you could pass an optimized build for the browser toolbox.
+ // This is also useful when debugging a patch that break devtools,
+ // so that you could use a build that works for the browser toolbox.
+ const customBinaryPath = Services.env.get("MOZ_BROWSER_TOOLBOX_BINARY");
+ if (customBinaryPath) {
+ command = customBinaryPath;
+ profilePath = PathUtils.join(PathUtils.tempDir, "browserToolboxProfile");
+ }
+
+ dumpn("Running chrome debugging process.");
+ const args = [
+ "-no-remote",
+ "-foreground",
+ "-profile",
+ profilePath,
+ "-chrome",
+ BROWSER_TOOLBOX_WINDOW_URL,
+ ];
+
+ const isInputContextEnabled = Services.prefs.getBoolPref(
+ "devtools.webconsole.input.context",
+ false
+ );
+ const environment = {
+ // Allow recording the startup of the browser toolbox when setting
+ // MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP=1 when running firefox.
+ MOZ_PROFILER_STARTUP: Services.env.get(
+ "MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP"
+ ),
+ // And prevent profiling any subsequent toolbox
+ MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP: "0",
+
+ MOZ_BROWSER_TOOLBOX_FORCE_MULTIPROCESS: forceMultiprocess ? "1" : "0",
+ // Similar, but for the WebConsole input context dropdown.
+ MOZ_BROWSER_TOOLBOX_INPUT_CONTEXT: isInputContextEnabled ? "1" : "0",
+ // Disable safe mode for the new process in case this was opened via the
+ // keyboard shortcut.
+ MOZ_DISABLE_SAFE_MODE_KEY: "1",
+ MOZ_BROWSER_TOOLBOX_PORT: String(this.#port),
+ MOZ_HEADLESS: null,
+ // Never enable Marionette for the new process.
+ MOZ_MARIONETTE: null,
+ // Don't inherit debug settings from the process launching us. This can
+ // cause errors when log files collide.
+ MOZ_LOG: null,
+ MOZ_LOG_FILE: null,
+ XPCOM_MEM_BLOAT_LOG: null,
+ XPCOM_MEM_LEAK_LOG: null,
+ XPCOM_MEM_LOG_CLASSES: null,
+ XPCOM_MEM_REFCNT_LOG: null,
+ XRE_PROFILE_PATH: null,
+ XRE_PROFILE_LOCAL_PATH: null,
+ };
+
+ // During local development, incremental builds can trigger the main process
+ // to clear its startup cache with the "flag file" .purgecaches, but this
+ // file is removed during app startup time, so we aren't able to know if it
+ // was present in order to also clear the child profile's startup cache as
+ // well.
+ //
+ // As an approximation of "isLocalBuild", check for an unofficial build.
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ args.push("-purgecaches");
+ }
+
+ dump(`Starting Browser Toolbox ${command} ${args.join(" ")}\n`);
+ IOUtils.makeDirectory(profilePath, { ignoreExisting: true })
+ .then(() =>
+ Subprocess.call({
+ command,
+ arguments: args,
+ environmentAppend: true,
+ stderr: "stdout",
+ environment,
+ })
+ )
+ .then(proc => {
+ this.#dbgProcess = proc;
+
+ this.#telemetry.toolOpened("jsbrowserdebugger", this);
+
+ dumpn("Chrome toolbox is now running...");
+ this.emit("run", this, proc, this.#dbgProfilePath);
+
+ proc.stdin.close();
+ const dumpPipe = async pipe => {
+ let leftover = "";
+ let data = await pipe.readString();
+ while (data) {
+ data = leftover + data;
+ const lines = data.split(/\r\n|\r|\n/);
+ if (lines.length) {
+ for (const line of lines.slice(0, -1)) {
+ dump(`${proc.pid}> ${line}\n`);
+ }
+ leftover = lines[lines.length - 1];
+ }
+ data = await pipe.readString();
+ }
+ if (leftover) {
+ dump(`${proc.pid}> ${leftover}\n`);
+ }
+ };
+ dumpPipe(proc.stdout);
+
+ proc.wait().then(() => this.close());
+
+ return proc;
+ })
+ .catch(err => {
+ console.log(
+ `Error loading Browser Toolbox: ${command} ${args.join(" ")}`,
+ err
+ );
+ });
+ }
+
+ /**
+ * Closes the remote debugging server and kills the toolbox process.
+ */
+ async close() {
+ if (this.#closed) {
+ return;
+ }
+
+ this.#closed = true;
+
+ dumpn("Cleaning up the chrome debugging process.");
+
+ Services.obs.removeObserver(this.close, "quit-application");
+
+ // We tear down before killing the browser toolbox process to avoid leaking
+ // socket connection objects.
+ if (this.#listener) {
+ this.#listener.close();
+ }
+
+ // Note that the DevToolsServer can be shared with the DevToolsServer
+ // spawned by DevToolsFrameChild. We shouldn't destroy it from here.
+ // Instead we should let it auto-destroy itself once the last connection is closed.
+ this.#devToolsServer = null;
+
+ this.#dbgProcess.stdout.close();
+ await this.#dbgProcess.kill();
+
+ this.#telemetry.toolClosed("jsbrowserdebugger", this);
+
+ dumpn("Chrome toolbox is now closed...");
+ processes.delete(this);
+
+ this.#dbgProcess = null;
+ if (this.#loader) {
+ releaseDistinctSystemPrincipalLoader(this);
+ }
+ this.#loader = null;
+ this.#telemetry = null;
+ }
+}
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+const prefObserver = {
+ observe: (...args) => {
+ wantLogging = Services.prefs.getBoolPref(args.pop());
+ },
+};
+Services.prefs.addObserver("devtools.debugger.log", prefObserver);
+const unloadObserver = function (subject) {
+ if (subject.wrappedJSObject == require("@loader/unload")) {
+ Services.prefs.removeObserver("devtools.debugger.log", prefObserver);
+ Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
+ }
+};
+Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");