ToolboxProcess.jsm 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
  7. const DBG_XUL = "chrome://devtools/content/framework/toolbox-process-window.xul";
  8. const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
  9. const { require, DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  10. const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
  11. XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
  12. return require("devtools/client/shared/telemetry");
  13. });
  14. XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
  15. return require("devtools/shared/event-emitter");
  16. });
  17. const promise = require("promise");
  18. const Services = require("Services");
  19. this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
  20. var processes = new Set();
  21. /**
  22. * Constructor for creating a process that will hold a chrome toolbox.
  23. *
  24. * @param function aOnClose [optional]
  25. * A function called when the process stops running.
  26. * @param function aOnRun [optional]
  27. * A function called when the process starts running.
  28. * @param object aOptions [optional]
  29. * An object with properties for configuring BrowserToolboxProcess.
  30. */
  31. this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
  32. let emitter = new EventEmitter();
  33. this.on = emitter.on.bind(emitter);
  34. this.off = emitter.off.bind(emitter);
  35. this.once = emitter.once.bind(emitter);
  36. // Forward any events to the shared emitter.
  37. this.emit = function (...args) {
  38. emitter.emit(...args);
  39. BrowserToolboxProcess.emit(...args);
  40. };
  41. // If first argument is an object, use those properties instead of
  42. // all three arguments
  43. if (typeof aOnClose === "object") {
  44. if (aOnClose.onClose) {
  45. this.once("close", aOnClose.onClose);
  46. }
  47. if (aOnClose.onRun) {
  48. this.once("run", aOnClose.onRun);
  49. }
  50. this._options = aOnClose;
  51. } else {
  52. if (aOnClose) {
  53. this.once("close", aOnClose);
  54. }
  55. if (aOnRun) {
  56. this.once("run", aOnRun);
  57. }
  58. this._options = aOptions || {};
  59. }
  60. this._telemetry = new Telemetry();
  61. this.close = this.close.bind(this);
  62. Services.obs.addObserver(this.close, "quit-application", false);
  63. this._initServer();
  64. this._initProfile();
  65. this._create();
  66. processes.add(this);
  67. };
  68. EventEmitter.decorate(BrowserToolboxProcess);
  69. /**
  70. * Initializes and starts a chrome toolbox process.
  71. * @return object
  72. */
  73. BrowserToolboxProcess.init = function (aOnClose, aOnRun, aOptions) {
  74. return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
  75. };
  76. /**
  77. * Passes a set of options to the BrowserAddonActors for the given ID.
  78. *
  79. * @param aId string
  80. * The ID of the add-on to pass the options to
  81. * @param aOptions object
  82. * The options.
  83. * @return a promise that will be resolved when complete.
  84. */
  85. BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
  86. let promises = [];
  87. for (let process of processes.values()) {
  88. promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
  89. }
  90. return promise.all(promises);
  91. };
  92. BrowserToolboxProcess.prototype = {
  93. /**
  94. * Initializes the debugger server.
  95. */
  96. _initServer: function () {
  97. if (this.debuggerServer) {
  98. dumpn("The chrome toolbox server is already running.");
  99. return;
  100. }
  101. dumpn("Initializing the chrome toolbox server.");
  102. // Create a separate loader instance, so that we can be sure to receive a
  103. // separate instance of the DebuggingServer from the rest of the devtools.
  104. // This allows us to safely use the tools against even the actors and
  105. // DebuggingServer itself, especially since we can mark this loader as
  106. // invisible to the debugger (unlike the usual loader settings).
  107. this.loader = new DevToolsLoader();
  108. this.loader.invisibleToDebugger = true;
  109. let { DebuggerServer } = this.loader.require("devtools/server/main");
  110. this.debuggerServer = DebuggerServer;
  111. dumpn("Created a separate loader instance for the DebuggerServer.");
  112. // Forward interesting events.
  113. this.debuggerServer.on("connectionchange", this.emit);
  114. this.debuggerServer.init();
  115. this.debuggerServer.addBrowserActors();
  116. this.debuggerServer.allowChromeProcess = true;
  117. dumpn("initialized and added the browser actors for the DebuggerServer.");
  118. let chromeDebuggingPort =
  119. Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
  120. let chromeDebuggingWebSocket =
  121. Services.prefs.getBoolPref("devtools.debugger.chrome-debugging-websocket");
  122. let listener = this.debuggerServer.createListener();
  123. listener.portOrPath = chromeDebuggingPort;
  124. listener.webSocket = chromeDebuggingWebSocket;
  125. listener.open();
  126. dumpn("Finished initializing the chrome toolbox server.");
  127. dumpn("Started listening on port: " + chromeDebuggingPort);
  128. },
  129. /**
  130. * Initializes a profile for the remote debugger process.
  131. */
  132. _initProfile: function () {
  133. dumpn("Initializing the chrome toolbox user profile.");
  134. let debuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
  135. debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
  136. try {
  137. debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
  138. } catch (ex) {
  139. // Don't re-copy over the prefs again if this profile already exists
  140. if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
  141. this._dbgProfilePath = debuggingProfileDir.path;
  142. } else {
  143. dumpn("Error trying to create a profile directory, failing.");
  144. dumpn("Error: " + (ex.message || ex));
  145. }
  146. return;
  147. }
  148. this._dbgProfilePath = debuggingProfileDir.path;
  149. // We would like to copy prefs into this new profile...
  150. let prefsFile = debuggingProfileDir.clone();
  151. prefsFile.append("prefs.js");
  152. // ... but unfortunately, when we run tests, it seems the starting profile
  153. // clears out the prefs file before re-writing it, and in practice the
  154. // file is empty when we get here. So just copying doesn't work in that
  155. // case.
  156. // We could force a sync pref flush and then copy it... but if we're doing
  157. // that, we might as well just flush directly to the new profile, which
  158. // always works:
  159. Services.prefs.savePrefFile(prefsFile);
  160. dumpn("Finished creating the chrome toolbox user profile at: " + this._dbgProfilePath);
  161. },
  162. /**
  163. * Creates and initializes the profile & process for the remote debugger.
  164. */
  165. _create: function () {
  166. dumpn("Initializing chrome debugging process.");
  167. let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
  168. process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile));
  169. let xulURI = DBG_XUL;
  170. if (this._options.addonID) {
  171. xulURI += "?addonID=" + this._options.addonID;
  172. }
  173. dumpn("Running chrome debugging process.");
  174. let args = ["-no-remote", "-foreground", "-profile", this._dbgProfilePath, "-chrome", xulURI];
  175. // During local development, incremental builds can trigger the main process
  176. // to clear its startup cache with the "flag file" .purgecaches, but this
  177. // file is removed during app startup time, so we aren't able to know if it
  178. // was present in order to also clear the child profile's startup cache as
  179. // well.
  180. //
  181. // As an approximation of "isLocalBuild", check for an unofficial build.
  182. if (!Services.appinfo.isOfficial) {
  183. args.push("-purgecaches");
  184. }
  185. // Disable safe mode for the new process in case this was opened via the
  186. // keyboard shortcut.
  187. let nsIEnvironment = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
  188. let originalValue = nsIEnvironment.get("MOZ_DISABLE_SAFE_MODE_KEY");
  189. nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", "1");
  190. process.runwAsync(args, args.length, { observe: () => this.close() });
  191. // Now that the process has started, it's safe to reset the env variable.
  192. nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", originalValue);
  193. this._telemetry.toolOpened("jsbrowserdebugger");
  194. dumpn("Chrome toolbox is now running...");
  195. this.emit("run", this);
  196. },
  197. /**
  198. * Closes the remote debugging server and kills the toolbox process.
  199. */
  200. close: function () {
  201. if (this.closed) {
  202. return;
  203. }
  204. dumpn("Cleaning up the chrome debugging process.");
  205. Services.obs.removeObserver(this.close, "quit-application");
  206. if (this._dbgProcess.isRunning) {
  207. this._dbgProcess.kill();
  208. }
  209. this._telemetry.toolClosed("jsbrowserdebugger");
  210. if (this.debuggerServer) {
  211. this.debuggerServer.off("connectionchange", this.emit);
  212. this.debuggerServer.destroy();
  213. this.debuggerServer = null;
  214. }
  215. dumpn("Chrome toolbox is now closed...");
  216. this.closed = true;
  217. this.emit("close", this);
  218. processes.delete(this);
  219. this._dbgProcess = null;
  220. this._options = null;
  221. if (this.loader) {
  222. this.loader.destroy();
  223. }
  224. this.loader = null;
  225. this._telemetry = null;
  226. }
  227. };
  228. /**
  229. * Helper method for debugging.
  230. * @param string
  231. */
  232. function dumpn(str) {
  233. if (wantLogging) {
  234. dump("DBG-FRONTEND: " + str + "\n");
  235. }
  236. }
  237. var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
  238. Services.prefs.addObserver("devtools.debugger.log", {
  239. observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
  240. }, false);
  241. Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);