Source code

Revision control

Other Tools

1
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
"use strict";
8
9
const DBG_XUL =
11
const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
12
13
const { require, DevToolsLoader } = ChromeUtils.import(
15
);
16
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
17
18
ChromeUtils.defineModuleGetter(
19
this,
20
"Subprocess",
22
);
23
ChromeUtils.defineModuleGetter(
24
this,
25
"AppConstants",
27
);
28
29
XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
30
return require("devtools/client/shared/telemetry");
31
});
32
XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
33
return require("devtools/shared/event-emitter");
34
});
35
36
const Services = require("Services");
37
38
this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
39
40
var processes = new Set();
41
42
/**
43
* Constructor for creating a process that will hold a chrome toolbox.
44
*
45
* @param function onClose [optional]
46
* A function called when the process stops running.
47
* @param function onRun [optional]
48
* A function called when the process starts running.
49
*/
50
this.BrowserToolboxProcess = function BrowserToolboxProcess(onClose, onRun) {
51
const emitter = new EventEmitter();
52
this.on = emitter.on.bind(emitter);
53
this.off = emitter.off.bind(emitter);
54
this.once = emitter.once.bind(emitter);
55
// Forward any events to the shared emitter.
56
this.emit = function(...args) {
57
emitter.emit(...args);
58
BrowserToolboxProcess.emit(...args);
59
};
60
61
if (onClose) {
62
this.once("close", onClose);
63
}
64
if (onRun) {
65
this.once("run", onRun);
66
}
67
68
this._telemetry = new Telemetry();
69
70
this.close = this.close.bind(this);
71
Services.obs.addObserver(this.close, "quit-application");
72
this._initServer();
73
this._initProfile();
74
this._create();
75
76
processes.add(this);
77
};
78
79
EventEmitter.decorate(BrowserToolboxProcess);
80
81
/**
82
* Initializes and starts a chrome toolbox process.
83
* @return object
84
*/
85
BrowserToolboxProcess.init = function(onClose, onRun) {
86
if (
87
!Services.prefs.getBoolPref("devtools.chrome.enabled") ||
88
!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")
89
) {
90
console.error("Could not start Browser Toolbox, you need to enable it.");
91
return null;
92
}
93
return new BrowserToolboxProcess(onClose, onRun);
94
};
95
96
/**
97
* Figure out if there are any open Browser Toolboxes that'll need to be restored.
98
* @return bool
99
*/
100
BrowserToolboxProcess.getBrowserToolboxSessionState = function() {
101
return processes.size !== 0;
102
};
103
104
BrowserToolboxProcess.prototype = {
105
/**
106
* Initializes the debugger server.
107
*/
108
_initServer: function() {
109
if (this.debuggerServer) {
110
dumpn("The chrome toolbox server is already running.");
111
return;
112
}
113
114
dumpn("Initializing the chrome toolbox server.");
115
116
// Create a separate loader instance, so that we can be sure to receive a
117
// separate instance of the DebuggingServer from the rest of the devtools.
118
// This allows us to safely use the tools against even the actors and
119
// DebuggingServer itself, especially since we can mark this loader as
120
// invisible to the debugger (unlike the usual loader settings).
121
this.loader = new DevToolsLoader({
122
invisibleToDebugger: true,
123
});
124
const { DebuggerServer } = this.loader.require(
125
"devtools/server/debugger-server"
126
);
127
const { SocketListener } = this.loader.require(
128
"devtools/shared/security/socket"
129
);
130
this.debuggerServer = DebuggerServer;
131
dumpn("Created a separate loader instance for the DebuggerServer.");
132
133
this.debuggerServer.init();
134
// We mainly need a root actor and target actors for opening a toolbox, even
135
// against chrome/content. But the "no auto hide" button uses the
136
// preference actor, so also register the browser actors.
137
this.debuggerServer.registerAllActors();
138
this.debuggerServer.allowChromeProcess = true;
139
dumpn("initialized and added the browser actors for the DebuggerServer.");
140
141
const chromeDebuggingWebSocket = Services.prefs.getBoolPref(
142
"devtools.debugger.chrome-debugging-websocket"
143
);
144
const socketOptions = {
145
portOrPath: -1,
146
webSocket: chromeDebuggingWebSocket,
147
};
148
const listener = new SocketListener(this.debuggerServer, socketOptions);
149
listener.open();
150
this.listener = listener;
151
this.port = listener.port;
152
153
if (!this.port) {
154
throw new Error("No debugger server port");
155
}
156
157
dumpn("Finished initializing the chrome toolbox server.");
158
dump(
159
`Debugger Server for Browser Toolbox listening on port: ${this.port}\n`
160
);
161
},
162
163
/**
164
* Initializes a profile for the remote debugger process.
165
*/
166
_initProfile: function() {
167
dumpn("Initializing the chrome toolbox user profile.");
168
169
// We used to use `ProfLD` instead of `ProfD`, so migrate old profiles if they exist.
170
this._migrateProfileDir();
171
172
const debuggingProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
173
debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
174
try {
175
debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
176
} catch (ex) {
177
// Don't re-copy over the prefs again if this profile already exists
178
if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
179
this._dbgProfilePath = debuggingProfileDir.path;
180
} else {
181
dumpn("Error trying to create a profile directory, failing.");
182
dumpn("Error: " + (ex.message || ex));
183
}
184
return;
185
}
186
187
this._dbgProfilePath = debuggingProfileDir.path;
188
189
// We would like to copy prefs into this new profile...
190
const prefsFile = debuggingProfileDir.clone();
191
prefsFile.append("prefs.js");
192
// ... but unfortunately, when we run tests, it seems the starting profile
193
// clears out the prefs file before re-writing it, and in practice the
194
// file is empty when we get here. So just copying doesn't work in that
195
// case.
196
// We could force a sync pref flush and then copy it... but if we're doing
197
// that, we might as well just flush directly to the new profile, which
198
// always works:
199
Services.prefs.savePrefFile(prefsFile);
200
201
dumpn(
202
"Finished creating the chrome toolbox user profile at: " +
203
this._dbgProfilePath
204
);
205
},
206
207
/**
208
* Originally, the profile was placed in `ProfLD` instead of `ProfD`. On some systems,
209
* such as macOS, `ProfLD` is in the user's Caches directory, which is not an
210
* appropriate place to store supposedly persistent profile data.
211
*/
212
_migrateProfileDir() {
213
const oldDebuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
214
const newDebuggingProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
215
if (oldDebuggingProfileDir.path == newDebuggingProfileDir.path) {
216
// It's possible for these locations to be the same, such as running from
217
// a custom profile directory specified via CLI.
218
return;
219
}
220
oldDebuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
221
if (!oldDebuggingProfileDir.exists()) {
222
return;
223
}
224
dumpn(`Old debugging profile exists: ${oldDebuggingProfileDir.path}`);
225
try {
226
// Remove the directory from the target location, if it exists
227
newDebuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
228
if (newDebuggingProfileDir.exists()) {
229
dumpn(`Removing folder at destination: ${newDebuggingProfileDir.path}`);
230
newDebuggingProfileDir.remove(true);
231
}
232
// Move profile from old to new location
233
const newDebuggingProfileParent = Services.dirsvc.get(
234
"ProfD",
235
Ci.nsIFile
236
);
237
oldDebuggingProfileDir.moveTo(newDebuggingProfileParent, null);
238
dumpn("Debugging profile migrated successfully");
239
} catch (e) {
240
dumpn(`Debugging profile migration failed: ${e}`);
241
}
242
},
243
244
/**
245
* Creates and initializes the profile & process for the remote debugger.
246
*/
247
_create: function() {
248
dumpn("Initializing chrome debugging process.");
249
250
const command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
251
252
dumpn("Running chrome debugging process.");
253
const args = [
254
"-no-remote",
255
"-foreground",
256
"-profile",
257
this._dbgProfilePath,
258
"-chrome",
259
DBG_XUL,
260
];
261
const environment = {
262
// Disable safe mode for the new process in case this was opened via the
263
// keyboard shortcut.
264
MOZ_DISABLE_SAFE_MODE_KEY: "1",
265
MOZ_BROWSER_TOOLBOX_PORT: String(this.port),
266
};
267
268
// During local development, incremental builds can trigger the main process
269
// to clear its startup cache with the "flag file" .purgecaches, but this
270
// file is removed during app startup time, so we aren't able to know if it
271
// was present in order to also clear the child profile's startup cache as
272
// well.
273
//
274
// As an approximation of "isLocalBuild", check for an unofficial build.
275
if (!AppConstants.MOZILLA_OFFICIAL) {
276
args.push("-purgecaches");
277
}
278
279
this._dbgProcessPromise = Subprocess.call({
280
command,
281
arguments: args,
282
environmentAppend: true,
283
stderr: "stdout",
284
environment,
285
}).then(
286
proc => {
287
this._dbgProcess = proc;
288
289
// jsbrowserdebugger is not connected with a toolbox so we pass -1 as the
290
// toolbox session id.
291
this._telemetry.toolOpened("jsbrowserdebugger", -1, this);
292
293
dumpn("Chrome toolbox is now running...");
294
this.emit("run", this);
295
296
proc.stdin.close();
297
const dumpPipe = async pipe => {
298
let data = await pipe.readString();
299
while (data) {
300
dump(data);
301
data = await pipe.readString();
302
}
303
};
304
dumpPipe(proc.stdout);
305
306
proc.wait().then(() => this.close());
307
308
return proc;
309
},
310
err => {
311
console.log(
312
`Error loading Browser Toolbox: ${command} ${args.join(" ")}`,
313
err
314
);
315
}
316
);
317
},
318
319
/**
320
* Closes the remote debugging server and kills the toolbox process.
321
*/
322
close: async function() {
323
if (this.closed) {
324
return;
325
}
326
327
this.closed = true;
328
329
dumpn("Cleaning up the chrome debugging process.");
330
331
Services.obs.removeObserver(this.close, "quit-application");
332
333
this._dbgProcess.stdout.close();
334
await this._dbgProcess.kill();
335
336
// jsbrowserdebugger is not connected with a toolbox so we pass -1 as the
337
// toolbox session id.
338
this._telemetry.toolClosed("jsbrowserdebugger", -1, this);
339
340
if (this.listener) {
341
this.listener.close();
342
}
343
344
if (this.debuggerServer) {
345
this.debuggerServer.destroy();
346
this.debuggerServer = null;
347
}
348
349
dumpn("Chrome toolbox is now closed...");
350
this.emit("close", this);
351
processes.delete(this);
352
353
this._dbgProcess = null;
354
if (this.loader) {
355
this.loader.destroy();
356
}
357
this.loader = null;
358
this._telemetry = null;
359
},
360
};
361
362
/**
363
* Helper method for debugging.
364
* @param string
365
*/
366
function dumpn(str) {
367
if (wantLogging) {
368
dump("DBG-FRONTEND: " + str + "\n");
369
}
370
}
371
372
var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
373
374
Services.prefs.addObserver("devtools.debugger.log", {
375
observe: (...args) => {
376
wantLogging = Services.prefs.getBoolPref(args.pop());
377
},
378
});