summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/ToolboxProcess.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/ToolboxProcess.jsm')
-rw-r--r--devtools/client/framework/ToolboxProcess.jsm291
1 files changed, 291 insertions, 0 deletions
diff --git a/devtools/client/framework/ToolboxProcess.jsm b/devtools/client/framework/ToolboxProcess.jsm
new file mode 100644
index 000000000..cd12e92cd
--- /dev/null
+++ b/devtools/client/framework/ToolboxProcess.jsm
@@ -0,0 +1,291 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const DBG_XUL = "chrome://devtools/content/framework/toolbox-process-window.xul";
+const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
+
+const { require, DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
+ return require("devtools/client/shared/telemetry");
+});
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
+ return require("devtools/shared/event-emitter");
+});
+const promise = require("promise");
+const Services = require("Services");
+
+this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
+
+var processes = new Set();
+
+/**
+ * Constructor for creating a process that will hold a chrome toolbox.
+ *
+ * @param function aOnClose [optional]
+ * A function called when the process stops running.
+ * @param function aOnRun [optional]
+ * A function called when the process starts running.
+ * @param object aOptions [optional]
+ * An object with properties for configuring BrowserToolboxProcess.
+ */
+this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
+ let emitter = new EventEmitter();
+ this.on = emitter.on.bind(emitter);
+ this.off = emitter.off.bind(emitter);
+ this.once = emitter.once.bind(emitter);
+ // Forward any events to the shared emitter.
+ this.emit = function (...args) {
+ emitter.emit(...args);
+ BrowserToolboxProcess.emit(...args);
+ };
+
+ // If first argument is an object, use those properties instead of
+ // all three arguments
+ if (typeof aOnClose === "object") {
+ if (aOnClose.onClose) {
+ this.once("close", aOnClose.onClose);
+ }
+ if (aOnClose.onRun) {
+ this.once("run", aOnClose.onRun);
+ }
+ this._options = aOnClose;
+ } else {
+ if (aOnClose) {
+ this.once("close", aOnClose);
+ }
+ if (aOnRun) {
+ this.once("run", aOnRun);
+ }
+ this._options = aOptions || {};
+ }
+
+ this._telemetry = new Telemetry();
+
+ this.close = this.close.bind(this);
+ Services.obs.addObserver(this.close, "quit-application", false);
+ this._initServer();
+ this._initProfile();
+ this._create();
+
+ processes.add(this);
+};
+
+EventEmitter.decorate(BrowserToolboxProcess);
+
+/**
+ * Initializes and starts a chrome toolbox process.
+ * @return object
+ */
+BrowserToolboxProcess.init = function (aOnClose, aOnRun, aOptions) {
+ return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
+};
+
+/**
+ * Passes a set of options to the BrowserAddonActors for the given ID.
+ *
+ * @param aId string
+ * The ID of the add-on to pass the options to
+ * @param aOptions object
+ * The options.
+ * @return a promise that will be resolved when complete.
+ */
+BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
+ let promises = [];
+
+ for (let process of processes.values()) {
+ promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
+ }
+
+ return promise.all(promises);
+};
+
+BrowserToolboxProcess.prototype = {
+ /**
+ * Initializes the debugger server.
+ */
+ _initServer: function () {
+ if (this.debuggerServer) {
+ 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 = new DevToolsLoader();
+ this.loader.invisibleToDebugger = true;
+ let { DebuggerServer } = this.loader.require("devtools/server/main");
+ this.debuggerServer = DebuggerServer;
+ dumpn("Created a separate loader instance for the DebuggerServer.");
+
+ // Forward interesting events.
+ this.debuggerServer.on("connectionchange", this.emit);
+
+ this.debuggerServer.init();
+ this.debuggerServer.addBrowserActors();
+ this.debuggerServer.allowChromeProcess = true;
+ dumpn("initialized and added the browser actors for the DebuggerServer.");
+
+ let chromeDebuggingPort =
+ Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+ let chromeDebuggingWebSocket =
+ Services.prefs.getBoolPref("devtools.debugger.chrome-debugging-websocket");
+ let listener = this.debuggerServer.createListener();
+ listener.portOrPath = chromeDebuggingPort;
+ listener.webSocket = chromeDebuggingWebSocket;
+ listener.open();
+
+ dumpn("Finished initializing the chrome toolbox server.");
+ dumpn("Started listening on port: " + chromeDebuggingPort);
+ },
+
+ /**
+ * Initializes a profile for the remote debugger process.
+ */
+ _initProfile: function () {
+ dumpn("Initializing the chrome toolbox user profile.");
+
+ let debuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+ debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
+ try {
+ debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (ex) {
+ // Don't re-copy over the prefs again if this profile already exists
+ if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ this._dbgProfilePath = debuggingProfileDir.path;
+ } 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...
+ let prefsFile = debuggingProfileDir.clone();
+ prefsFile.append("prefs.js");
+ // ... 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.
+ */
+ _create: function () {
+ dumpn("Initializing chrome debugging process.");
+ let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile));
+
+ let xulURI = DBG_XUL;
+
+ if (this._options.addonID) {
+ xulURI += "?addonID=" + this._options.addonID;
+ }
+
+ dumpn("Running chrome debugging process.");
+ let args = ["-no-remote", "-foreground", "-profile", this._dbgProfilePath, "-chrome", xulURI];
+
+ // 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 (!Services.appinfo.isOfficial) {
+ args.push("-purgecaches");
+ }
+
+ // Disable safe mode for the new process in case this was opened via the
+ // keyboard shortcut.
+ let nsIEnvironment = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let originalValue = nsIEnvironment.get("MOZ_DISABLE_SAFE_MODE_KEY");
+ nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", "1");
+
+ process.runwAsync(args, args.length, { observe: () => this.close() });
+
+ // Now that the process has started, it's safe to reset the env variable.
+ nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", originalValue);
+
+ this._telemetry.toolOpened("jsbrowserdebugger");
+
+ dumpn("Chrome toolbox is now running...");
+ this.emit("run", this);
+ },
+
+ /**
+ * Closes the remote debugging server and kills the toolbox process.
+ */
+ close: function () {
+ if (this.closed) {
+ return;
+ }
+
+ dumpn("Cleaning up the chrome debugging process.");
+ Services.obs.removeObserver(this.close, "quit-application");
+
+ if (this._dbgProcess.isRunning) {
+ this._dbgProcess.kill();
+ }
+
+ this._telemetry.toolClosed("jsbrowserdebugger");
+ if (this.debuggerServer) {
+ this.debuggerServer.off("connectionchange", this.emit);
+ this.debuggerServer.destroy();
+ this.debuggerServer = null;
+ }
+
+ dumpn("Chrome toolbox is now closed...");
+ this.closed = true;
+ this.emit("close", this);
+ processes.delete(this);
+
+ this._dbgProcess = null;
+ this._options = null;
+ if (this.loader) {
+ this.loader.destroy();
+ }
+ 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");
+
+Services.prefs.addObserver("devtools.debugger.log", {
+ observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
+}, false);
+
+Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);