Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | "use strict"; |
michael@0 | 7 | |
michael@0 | 8 | const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
michael@0 | 9 | |
michael@0 | 10 | const DBG_XUL = "chrome://browser/content/devtools/framework/toolbox-process-window.xul"; |
michael@0 | 11 | const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger"; |
michael@0 | 12 | |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | Cu.import("resource://gre/modules/devtools/Loader.jsm"); |
michael@0 | 17 | let require = devtools.require; |
michael@0 | 18 | let Telemetry = require("devtools/shared/telemetry"); |
michael@0 | 19 | let EventEmitter = require("devtools/toolkit/event-emitter"); |
michael@0 | 20 | const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); |
michael@0 | 21 | |
michael@0 | 22 | this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"]; |
michael@0 | 23 | |
michael@0 | 24 | let processes = Set(); |
michael@0 | 25 | |
michael@0 | 26 | /** |
michael@0 | 27 | * Constructor for creating a process that will hold a chrome toolbox. |
michael@0 | 28 | * |
michael@0 | 29 | * @param function aOnClose [optional] |
michael@0 | 30 | * A function called when the process stops running. |
michael@0 | 31 | * @param function aOnRun [optional] |
michael@0 | 32 | * A function called when the process starts running. |
michael@0 | 33 | * @param object aOptions [optional] |
michael@0 | 34 | * An object with properties for configuring BrowserToolboxProcess. |
michael@0 | 35 | */ |
michael@0 | 36 | this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) { |
michael@0 | 37 | let emitter = new EventEmitter(); |
michael@0 | 38 | this.on = emitter.on.bind(emitter); |
michael@0 | 39 | this.off = emitter.off.bind(emitter); |
michael@0 | 40 | this.once = emitter.once.bind(emitter); |
michael@0 | 41 | // Forward any events to the shared emitter. |
michael@0 | 42 | this.emit = function(...args) { |
michael@0 | 43 | emitter.emit(...args); |
michael@0 | 44 | BrowserToolboxProcess.emit(...args); |
michael@0 | 45 | } |
michael@0 | 46 | |
michael@0 | 47 | // If first argument is an object, use those properties instead of |
michael@0 | 48 | // all three arguments |
michael@0 | 49 | if (typeof aOnClose === "object") { |
michael@0 | 50 | if (aOnClose.onClose) { |
michael@0 | 51 | this.on("close", aOnClose.onClose); |
michael@0 | 52 | } |
michael@0 | 53 | if (aOnClose.onRun) { |
michael@0 | 54 | this.on("run", aOnClose.onRun); |
michael@0 | 55 | } |
michael@0 | 56 | this._options = aOnClose; |
michael@0 | 57 | } else { |
michael@0 | 58 | if (aOnClose) { |
michael@0 | 59 | this.on("close", aOnClose); |
michael@0 | 60 | } |
michael@0 | 61 | if (aOnRun) { |
michael@0 | 62 | this.on("run", aOnRun); |
michael@0 | 63 | } |
michael@0 | 64 | this._options = aOptions || {}; |
michael@0 | 65 | } |
michael@0 | 66 | |
michael@0 | 67 | this._telemetry = new Telemetry(); |
michael@0 | 68 | |
michael@0 | 69 | this.close = this.close.bind(this); |
michael@0 | 70 | Services.obs.addObserver(this.close, "quit-application", false); |
michael@0 | 71 | this._initServer(); |
michael@0 | 72 | this._initProfile(); |
michael@0 | 73 | this._create(); |
michael@0 | 74 | |
michael@0 | 75 | processes.add(this); |
michael@0 | 76 | }; |
michael@0 | 77 | |
michael@0 | 78 | EventEmitter.decorate(BrowserToolboxProcess); |
michael@0 | 79 | |
michael@0 | 80 | /** |
michael@0 | 81 | * Initializes and starts a chrome toolbox process. |
michael@0 | 82 | * @return object |
michael@0 | 83 | */ |
michael@0 | 84 | BrowserToolboxProcess.init = function(aOnClose, aOnRun, aOptions) { |
michael@0 | 85 | return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions); |
michael@0 | 86 | }; |
michael@0 | 87 | |
michael@0 | 88 | /** |
michael@0 | 89 | * Passes a set of options to the BrowserAddonActors for the given ID. |
michael@0 | 90 | * |
michael@0 | 91 | * @param aId string |
michael@0 | 92 | * The ID of the add-on to pass the options to |
michael@0 | 93 | * @param aOptions object |
michael@0 | 94 | * The options. |
michael@0 | 95 | * @return a promise that will be resolved when complete. |
michael@0 | 96 | */ |
michael@0 | 97 | BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) { |
michael@0 | 98 | let promises = []; |
michael@0 | 99 | |
michael@0 | 100 | for (let process of processes.values()) { |
michael@0 | 101 | promises.push(process.debuggerServer.setAddonOptions(aId, aOptions)); |
michael@0 | 102 | } |
michael@0 | 103 | |
michael@0 | 104 | return promise.all(promises); |
michael@0 | 105 | }; |
michael@0 | 106 | |
michael@0 | 107 | BrowserToolboxProcess.prototype = { |
michael@0 | 108 | /** |
michael@0 | 109 | * Initializes the debugger server. |
michael@0 | 110 | */ |
michael@0 | 111 | _initServer: function() { |
michael@0 | 112 | dumpn("Initializing the chrome toolbox server."); |
michael@0 | 113 | |
michael@0 | 114 | if (!this.loader) { |
michael@0 | 115 | // Create a separate loader instance, so that we can be sure to receive a |
michael@0 | 116 | // separate instance of the DebuggingServer from the rest of the devtools. |
michael@0 | 117 | // This allows us to safely use the tools against even the actors and |
michael@0 | 118 | // DebuggingServer itself, especially since we can mark this loader as |
michael@0 | 119 | // invisible to the debugger (unlike the usual loader settings). |
michael@0 | 120 | this.loader = new DevToolsLoader(); |
michael@0 | 121 | this.loader.invisibleToDebugger = true; |
michael@0 | 122 | this.loader.main("devtools/server/main"); |
michael@0 | 123 | this.debuggerServer = this.loader.DebuggerServer; |
michael@0 | 124 | dumpn("Created a separate loader instance for the DebuggerServer."); |
michael@0 | 125 | |
michael@0 | 126 | // Forward interesting events. |
michael@0 | 127 | this.debuggerServer.on("connectionchange", this.emit.bind(this)); |
michael@0 | 128 | } |
michael@0 | 129 | |
michael@0 | 130 | if (!this.debuggerServer.initialized) { |
michael@0 | 131 | this.debuggerServer.init(); |
michael@0 | 132 | this.debuggerServer.addBrowserActors(); |
michael@0 | 133 | dumpn("initialized and added the browser actors for the DebuggerServer."); |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | this.debuggerServer.openListener(Prefs.chromeDebuggingPort); |
michael@0 | 137 | |
michael@0 | 138 | dumpn("Finished initializing the chrome toolbox server."); |
michael@0 | 139 | dumpn("Started listening on port: " + Prefs.chromeDebuggingPort); |
michael@0 | 140 | }, |
michael@0 | 141 | |
michael@0 | 142 | /** |
michael@0 | 143 | * Initializes a profile for the remote debugger process. |
michael@0 | 144 | */ |
michael@0 | 145 | _initProfile: function() { |
michael@0 | 146 | dumpn("Initializing the chrome toolbox user profile."); |
michael@0 | 147 | |
michael@0 | 148 | let profileService = Cc["@mozilla.org/toolkit/profile-service;1"] |
michael@0 | 149 | .createInstance(Ci.nsIToolkitProfileService); |
michael@0 | 150 | |
michael@0 | 151 | let profileName; |
michael@0 | 152 | try { |
michael@0 | 153 | // Attempt to get the required chrome debugging profile name string. |
michael@0 | 154 | profileName = profileService.selectedProfile.name + CHROME_DEBUGGER_PROFILE_NAME; |
michael@0 | 155 | dumpn("Using chrome toolbox profile name: " + profileName); |
michael@0 | 156 | } catch (e) { |
michael@0 | 157 | // Requested profile string could not be retrieved. |
michael@0 | 158 | profileName = CHROME_DEBUGGER_PROFILE_NAME; |
michael@0 | 159 | let msg = "Querying the current profile failed. " + e.name + ": " + e.message; |
michael@0 | 160 | dumpn(msg); |
michael@0 | 161 | Cu.reportError(msg); |
michael@0 | 162 | } |
michael@0 | 163 | |
michael@0 | 164 | let profileObject; |
michael@0 | 165 | try { |
michael@0 | 166 | // Attempt to get the required chrome debugging profile toolkit object. |
michael@0 | 167 | profileObject = profileService.getProfileByName(profileName); |
michael@0 | 168 | dumpn("Using chrome toolbox profile object: " + profileObject); |
michael@0 | 169 | |
michael@0 | 170 | // The profile exists but the corresponding folder may have been deleted. |
michael@0 | 171 | var enumerator = Services.dirsvc.get("ProfD", Ci.nsIFile).parent.directoryEntries; |
michael@0 | 172 | while (enumerator.hasMoreElements()) { |
michael@0 | 173 | let profileDir = enumerator.getNext().QueryInterface(Ci.nsIFile); |
michael@0 | 174 | if (profileDir.leafName.contains(profileName)) { |
michael@0 | 175 | // Requested profile was found and the folder exists. |
michael@0 | 176 | this._dbgProfile = profileObject; |
michael@0 | 177 | return; |
michael@0 | 178 | } |
michael@0 | 179 | } |
michael@0 | 180 | // Requested profile was found but the folder was deleted. Cleanup needed. |
michael@0 | 181 | profileObject.remove(true); |
michael@0 | 182 | dumpn("The already existing chrome toolbox profile was invalid."); |
michael@0 | 183 | } catch (e) { |
michael@0 | 184 | // Requested profile object was not found. |
michael@0 | 185 | let msg = "Creating a profile failed. " + e.name + ": " + e.message; |
michael@0 | 186 | dumpn(msg); |
michael@0 | 187 | Cu.reportError(msg); |
michael@0 | 188 | } |
michael@0 | 189 | |
michael@0 | 190 | // Create a new chrome debugging profile. |
michael@0 | 191 | this._dbgProfile = profileService.createProfile(null, profileName); |
michael@0 | 192 | profileService.flush(); |
michael@0 | 193 | |
michael@0 | 194 | dumpn("Finished creating the chrome toolbox user profile."); |
michael@0 | 195 | dumpn("Flushed profile service with: " + profileName); |
michael@0 | 196 | }, |
michael@0 | 197 | |
michael@0 | 198 | /** |
michael@0 | 199 | * Creates and initializes the profile & process for the remote debugger. |
michael@0 | 200 | */ |
michael@0 | 201 | _create: function() { |
michael@0 | 202 | dumpn("Initializing chrome debugging process."); |
michael@0 | 203 | let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); |
michael@0 | 204 | process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile)); |
michael@0 | 205 | |
michael@0 | 206 | let xulURI = DBG_XUL; |
michael@0 | 207 | |
michael@0 | 208 | if (this._options.addonID) { |
michael@0 | 209 | xulURI += "?addonID=" + this._options.addonID; |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | dumpn("Running chrome debugging process."); |
michael@0 | 213 | let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", xulURI]; |
michael@0 | 214 | |
michael@0 | 215 | process.runwAsync(args, args.length, { observe: () => this.close() }); |
michael@0 | 216 | |
michael@0 | 217 | this._telemetry.toolOpened("jsbrowserdebugger"); |
michael@0 | 218 | |
michael@0 | 219 | dumpn("Chrome toolbox is now running..."); |
michael@0 | 220 | this.emit("run", this); |
michael@0 | 221 | }, |
michael@0 | 222 | |
michael@0 | 223 | /** |
michael@0 | 224 | * Closes the remote debugging server and kills the toolbox process. |
michael@0 | 225 | */ |
michael@0 | 226 | close: function() { |
michael@0 | 227 | if (this.closed) { |
michael@0 | 228 | return; |
michael@0 | 229 | } |
michael@0 | 230 | |
michael@0 | 231 | dumpn("Cleaning up the chrome debugging process."); |
michael@0 | 232 | Services.obs.removeObserver(this.close, "quit-application"); |
michael@0 | 233 | |
michael@0 | 234 | if (this._dbgProcess.isRunning) { |
michael@0 | 235 | this._dbgProcess.kill(); |
michael@0 | 236 | } |
michael@0 | 237 | |
michael@0 | 238 | this._telemetry.toolClosed("jsbrowserdebugger"); |
michael@0 | 239 | if (this.debuggerServer) { |
michael@0 | 240 | this.debuggerServer.destroy(); |
michael@0 | 241 | } |
michael@0 | 242 | |
michael@0 | 243 | dumpn("Chrome toolbox is now closed..."); |
michael@0 | 244 | this.closed = true; |
michael@0 | 245 | this.emit("close", this); |
michael@0 | 246 | processes.delete(this); |
michael@0 | 247 | } |
michael@0 | 248 | }; |
michael@0 | 249 | |
michael@0 | 250 | /** |
michael@0 | 251 | * Shortcuts for accessing various debugger preferences. |
michael@0 | 252 | */ |
michael@0 | 253 | let Prefs = new ViewHelpers.Prefs("devtools.debugger", { |
michael@0 | 254 | chromeDebuggingHost: ["Char", "chrome-debugging-host"], |
michael@0 | 255 | chromeDebuggingPort: ["Int", "chrome-debugging-port"] |
michael@0 | 256 | }); |
michael@0 | 257 | |
michael@0 | 258 | /** |
michael@0 | 259 | * Helper method for debugging. |
michael@0 | 260 | * @param string |
michael@0 | 261 | */ |
michael@0 | 262 | function dumpn(str) { |
michael@0 | 263 | if (wantLogging) { |
michael@0 | 264 | dump("DBG-FRONTEND: " + str + "\n"); |
michael@0 | 265 | } |
michael@0 | 266 | } |
michael@0 | 267 | |
michael@0 | 268 | let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); |
michael@0 | 269 | |
michael@0 | 270 | Services.prefs.addObserver("devtools.debugger.log", { |
michael@0 | 271 | observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop()) |
michael@0 | 272 | }, false); |