summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/ToolboxProcess.jsm
blob: cd12e92cd7271746ccba1a8047fe0b983ba705af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
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);