michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: const DBG_XUL = "chrome://browser/content/devtools/framework/toolbox-process-window.xul"; michael@0: const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger"; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: michael@0: Cu.import("resource://gre/modules/devtools/Loader.jsm"); michael@0: let require = devtools.require; michael@0: let Telemetry = require("devtools/shared/telemetry"); michael@0: let EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"]; michael@0: michael@0: let processes = Set(); michael@0: michael@0: /** michael@0: * Constructor for creating a process that will hold a chrome toolbox. michael@0: * michael@0: * @param function aOnClose [optional] michael@0: * A function called when the process stops running. michael@0: * @param function aOnRun [optional] michael@0: * A function called when the process starts running. michael@0: * @param object aOptions [optional] michael@0: * An object with properties for configuring BrowserToolboxProcess. michael@0: */ michael@0: this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) { michael@0: let emitter = new EventEmitter(); michael@0: this.on = emitter.on.bind(emitter); michael@0: this.off = emitter.off.bind(emitter); michael@0: this.once = emitter.once.bind(emitter); michael@0: // Forward any events to the shared emitter. michael@0: this.emit = function(...args) { michael@0: emitter.emit(...args); michael@0: BrowserToolboxProcess.emit(...args); michael@0: } michael@0: michael@0: // If first argument is an object, use those properties instead of michael@0: // all three arguments michael@0: if (typeof aOnClose === "object") { michael@0: if (aOnClose.onClose) { michael@0: this.on("close", aOnClose.onClose); michael@0: } michael@0: if (aOnClose.onRun) { michael@0: this.on("run", aOnClose.onRun); michael@0: } michael@0: this._options = aOnClose; michael@0: } else { michael@0: if (aOnClose) { michael@0: this.on("close", aOnClose); michael@0: } michael@0: if (aOnRun) { michael@0: this.on("run", aOnRun); michael@0: } michael@0: this._options = aOptions || {}; michael@0: } michael@0: michael@0: this._telemetry = new Telemetry(); michael@0: michael@0: this.close = this.close.bind(this); michael@0: Services.obs.addObserver(this.close, "quit-application", false); michael@0: this._initServer(); michael@0: this._initProfile(); michael@0: this._create(); michael@0: michael@0: processes.add(this); michael@0: }; michael@0: michael@0: EventEmitter.decorate(BrowserToolboxProcess); michael@0: michael@0: /** michael@0: * Initializes and starts a chrome toolbox process. michael@0: * @return object michael@0: */ michael@0: BrowserToolboxProcess.init = function(aOnClose, aOnRun, aOptions) { michael@0: return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions); michael@0: }; michael@0: michael@0: /** michael@0: * Passes a set of options to the BrowserAddonActors for the given ID. michael@0: * michael@0: * @param aId string michael@0: * The ID of the add-on to pass the options to michael@0: * @param aOptions object michael@0: * The options. michael@0: * @return a promise that will be resolved when complete. michael@0: */ michael@0: BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) { michael@0: let promises = []; michael@0: michael@0: for (let process of processes.values()) { michael@0: promises.push(process.debuggerServer.setAddonOptions(aId, aOptions)); michael@0: } michael@0: michael@0: return promise.all(promises); michael@0: }; michael@0: michael@0: BrowserToolboxProcess.prototype = { michael@0: /** michael@0: * Initializes the debugger server. michael@0: */ michael@0: _initServer: function() { michael@0: dumpn("Initializing the chrome toolbox server."); michael@0: michael@0: if (!this.loader) { michael@0: // Create a separate loader instance, so that we can be sure to receive a michael@0: // separate instance of the DebuggingServer from the rest of the devtools. michael@0: // This allows us to safely use the tools against even the actors and michael@0: // DebuggingServer itself, especially since we can mark this loader as michael@0: // invisible to the debugger (unlike the usual loader settings). michael@0: this.loader = new DevToolsLoader(); michael@0: this.loader.invisibleToDebugger = true; michael@0: this.loader.main("devtools/server/main"); michael@0: this.debuggerServer = this.loader.DebuggerServer; michael@0: dumpn("Created a separate loader instance for the DebuggerServer."); michael@0: michael@0: // Forward interesting events. michael@0: this.debuggerServer.on("connectionchange", this.emit.bind(this)); michael@0: } michael@0: michael@0: if (!this.debuggerServer.initialized) { michael@0: this.debuggerServer.init(); michael@0: this.debuggerServer.addBrowserActors(); michael@0: dumpn("initialized and added the browser actors for the DebuggerServer."); michael@0: } michael@0: michael@0: this.debuggerServer.openListener(Prefs.chromeDebuggingPort); michael@0: michael@0: dumpn("Finished initializing the chrome toolbox server."); michael@0: dumpn("Started listening on port: " + Prefs.chromeDebuggingPort); michael@0: }, michael@0: michael@0: /** michael@0: * Initializes a profile for the remote debugger process. michael@0: */ michael@0: _initProfile: function() { michael@0: dumpn("Initializing the chrome toolbox user profile."); michael@0: michael@0: let profileService = Cc["@mozilla.org/toolkit/profile-service;1"] michael@0: .createInstance(Ci.nsIToolkitProfileService); michael@0: michael@0: let profileName; michael@0: try { michael@0: // Attempt to get the required chrome debugging profile name string. michael@0: profileName = profileService.selectedProfile.name + CHROME_DEBUGGER_PROFILE_NAME; michael@0: dumpn("Using chrome toolbox profile name: " + profileName); michael@0: } catch (e) { michael@0: // Requested profile string could not be retrieved. michael@0: profileName = CHROME_DEBUGGER_PROFILE_NAME; michael@0: let msg = "Querying the current profile failed. " + e.name + ": " + e.message; michael@0: dumpn(msg); michael@0: Cu.reportError(msg); michael@0: } michael@0: michael@0: let profileObject; michael@0: try { michael@0: // Attempt to get the required chrome debugging profile toolkit object. michael@0: profileObject = profileService.getProfileByName(profileName); michael@0: dumpn("Using chrome toolbox profile object: " + profileObject); michael@0: michael@0: // The profile exists but the corresponding folder may have been deleted. michael@0: var enumerator = Services.dirsvc.get("ProfD", Ci.nsIFile).parent.directoryEntries; michael@0: while (enumerator.hasMoreElements()) { michael@0: let profileDir = enumerator.getNext().QueryInterface(Ci.nsIFile); michael@0: if (profileDir.leafName.contains(profileName)) { michael@0: // Requested profile was found and the folder exists. michael@0: this._dbgProfile = profileObject; michael@0: return; michael@0: } michael@0: } michael@0: // Requested profile was found but the folder was deleted. Cleanup needed. michael@0: profileObject.remove(true); michael@0: dumpn("The already existing chrome toolbox profile was invalid."); michael@0: } catch (e) { michael@0: // Requested profile object was not found. michael@0: let msg = "Creating a profile failed. " + e.name + ": " + e.message; michael@0: dumpn(msg); michael@0: Cu.reportError(msg); michael@0: } michael@0: michael@0: // Create a new chrome debugging profile. michael@0: this._dbgProfile = profileService.createProfile(null, profileName); michael@0: profileService.flush(); michael@0: michael@0: dumpn("Finished creating the chrome toolbox user profile."); michael@0: dumpn("Flushed profile service with: " + profileName); michael@0: }, michael@0: michael@0: /** michael@0: * Creates and initializes the profile & process for the remote debugger. michael@0: */ michael@0: _create: function() { michael@0: dumpn("Initializing chrome debugging process."); michael@0: let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); michael@0: process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile)); michael@0: michael@0: let xulURI = DBG_XUL; michael@0: michael@0: if (this._options.addonID) { michael@0: xulURI += "?addonID=" + this._options.addonID; michael@0: } michael@0: michael@0: dumpn("Running chrome debugging process."); michael@0: let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", xulURI]; michael@0: michael@0: process.runwAsync(args, args.length, { observe: () => this.close() }); michael@0: michael@0: this._telemetry.toolOpened("jsbrowserdebugger"); michael@0: michael@0: dumpn("Chrome toolbox is now running..."); michael@0: this.emit("run", this); michael@0: }, michael@0: michael@0: /** michael@0: * Closes the remote debugging server and kills the toolbox process. michael@0: */ michael@0: close: function() { michael@0: if (this.closed) { michael@0: return; michael@0: } michael@0: michael@0: dumpn("Cleaning up the chrome debugging process."); michael@0: Services.obs.removeObserver(this.close, "quit-application"); michael@0: michael@0: if (this._dbgProcess.isRunning) { michael@0: this._dbgProcess.kill(); michael@0: } michael@0: michael@0: this._telemetry.toolClosed("jsbrowserdebugger"); michael@0: if (this.debuggerServer) { michael@0: this.debuggerServer.destroy(); michael@0: } michael@0: michael@0: dumpn("Chrome toolbox is now closed..."); michael@0: this.closed = true; michael@0: this.emit("close", this); michael@0: processes.delete(this); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Shortcuts for accessing various debugger preferences. michael@0: */ michael@0: let Prefs = new ViewHelpers.Prefs("devtools.debugger", { michael@0: chromeDebuggingHost: ["Char", "chrome-debugging-host"], michael@0: chromeDebuggingPort: ["Int", "chrome-debugging-port"] michael@0: }); michael@0: michael@0: /** michael@0: * Helper method for debugging. michael@0: * @param string michael@0: */ michael@0: function dumpn(str) { michael@0: if (wantLogging) { michael@0: dump("DBG-FRONTEND: " + str + "\n"); michael@0: } michael@0: } michael@0: michael@0: let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); michael@0: michael@0: Services.prefs.addObserver("devtools.debugger.log", { michael@0: observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop()) michael@0: }, false);