gecko-dev/devtools/client/framework/ToolboxProcess.jsm
J. Ryan Stinnett be021da252 Bug 1172897 - Rename BrowserAddonActor to AddonTargetActor. r=ochameau
MozReview-Commit-ID: 4iI6BQaleJS

--HG--
rename : devtools/server/actors/addon.js => devtools/server/actors/targets/addon.js
extra : rebase_source : f48676bdda8b68226a00cb29c164180526389c4a
2018-06-04 18:37:02 -05:00

404 lines
13 KiB
JavaScript

/* -*- 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 DBG_XUL = "chrome://devtools/content/framework/toolbox-process-window.xul";
const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
const { require, DevToolsLoader } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "Subprocess", "resource://gre/modules/Subprocess.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.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 onClose [optional]
* A function called when the process stops running.
* @param function onRun [optional]
* A function called when the process starts running.
* @param object options [optional]
* An object with properties for configuring BrowserToolboxProcess.
*/
this.BrowserToolboxProcess = function BrowserToolboxProcess(onClose, onRun, options) {
const 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 onClose === "object") {
if (onClose.onClose) {
this.once("close", onClose.onClose);
}
if (onClose.onRun) {
this.once("run", onClose.onRun);
}
this._options = onClose;
} else {
if (onClose) {
this.once("close", onClose);
}
if (onRun) {
this.once("run", onRun);
}
this._options = options || {};
}
this._telemetry = new Telemetry();
this._onConnectionChange = this._onConnectionChange.bind(this);
this.close = this.close.bind(this);
Services.obs.addObserver(this.close, "quit-application");
this._initServer();
this._initProfile();
this._create();
processes.add(this);
};
EventEmitter.decorate(BrowserToolboxProcess);
/**
* Initializes and starts a chrome toolbox process.
* @return object
*/
BrowserToolboxProcess.init = function(onClose, onRun, options) {
if (!Services.prefs.getBoolPref("devtools.chrome.enabled") ||
!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
console.error("Could not start Browser Toolbox, you need to enable it.");
return null;
}
return new BrowserToolboxProcess(onClose, onRun, options);
};
/**
* Figure out if there are any open Browser Toolboxes that'll need to be restored.
* @return bool
*/
BrowserToolboxProcess.getBrowserToolboxSessionState = function() {
for (const process of processes.values()) {
// Don't worry about addon toolboxes, we only want to restore the Browser Toolbox.
if (!process._options || !process._options.addonID) {
return true;
}
}
return false;
};
/**
* Passes a set of options to the AddonTargetActors for the given ID.
*
* @param id string
* The ID of the add-on to pass the options to
* @param options object
* The options.
* @return a promise that will be resolved when complete.
*/
BrowserToolboxProcess.setAddonOptions = function(id, options) {
const promises = [];
for (const process of processes.values()) {
promises.push(process.debuggerServer.setAddonOptions(id, options));
}
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;
const { 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._onConnectionChange);
this.debuggerServer.init();
// We mainly need a root actor and target actors for opening a toolbox, even
// against chrome/content/addon. But the "no auto hide" button uses the
// preference actor, so also register the browser actors.
this.debuggerServer.registerAllActors();
this.debuggerServer.allowChromeProcess = true;
dumpn("initialized and added the browser actors for the DebuggerServer.");
const chromeDebuggingWebSocket =
Services.prefs.getBoolPref("devtools.debugger.chrome-debugging-websocket");
const listener = this.debuggerServer.createListener();
listener.portOrPath = -1;
listener.webSocket = chromeDebuggingWebSocket;
listener.open();
this.port = listener.port;
if (!this.port) {
throw new Error("No debugger server port");
}
dumpn("Finished initializing the chrome toolbox server.");
dump(`Debugger Server for Browser Toolbox listening on port: ${this.port}\n`);
},
/**
* Initializes a profile for the remote debugger process.
*/
_initProfile: function() {
dumpn("Initializing the chrome toolbox user profile.");
// We used to use `ProfLD` instead of `ProfD`, so migrate old profiles if they exist.
this._migrateProfileDir();
const debuggingProfileDir = Services.dirsvc.get("ProfD", 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...
const 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);
},
/**
* Originally, the profile was placed in `ProfLD` instead of `ProfD`. On some systems,
* such as macOS, `ProfLD` is in the user's Caches directory, which is not an
* appropriate place to store supposedly persistent profile data.
*/
_migrateProfileDir() {
const oldDebuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
const newDebuggingProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
if (oldDebuggingProfileDir.path == newDebuggingProfileDir.path) {
// It's possible for these locations to be the same, such as running from
// a custom profile directory specified via CLI.
return;
}
oldDebuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
if (!oldDebuggingProfileDir.exists()) {
return;
}
dumpn(`Old debugging profile exists: ${oldDebuggingProfileDir.path}`);
try {
// Remove the directory from the target location, if it exists
newDebuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
if (newDebuggingProfileDir.exists()) {
dumpn(`Removing folder at destination: ${newDebuggingProfileDir.path}`);
newDebuggingProfileDir.remove(true);
}
// Move profile from old to new location
const newDebuggingProfileParent = Services.dirsvc.get("ProfD", Ci.nsIFile);
oldDebuggingProfileDir.moveTo(newDebuggingProfileParent, null);
dumpn("Debugging profile migrated successfully");
} catch (e) {
dumpn(`Debugging profile migration failed: ${e}`);
}
},
/**
* Creates and initializes the profile & process for the remote debugger.
*/
_create: function() {
dumpn("Initializing chrome debugging process.");
const command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
dumpn("Running chrome debugging process.");
const args = [
"-no-remote",
"-foreground",
"-profile", this._dbgProfilePath,
"-chrome", DBG_XUL
];
const environment = {
// Disable safe mode for the new process in case this was opened via the
// keyboard shortcut.
MOZ_DISABLE_SAFE_MODE_KEY: "1",
MOZ_BROWSER_TOOLBOX_PORT: String(this.port),
};
if (this._options.addonID) {
environment.MOZ_BROWSER_TOOLBOX_ADDONID = String(this._options.addonID);
}
// 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 (!AppConstants.MOZILLA_OFFICIAL) {
args.push("-purgecaches");
}
this._dbgProcessPromise = Subprocess.call({
command,
arguments: args,
environmentAppend: true,
stderr: "stdout",
environment,
}).then(proc => {
this._dbgProcess = proc;
this._telemetry.toolOpened("jsbrowserdebugger");
dumpn("Chrome toolbox is now running...");
this.emit("run", this);
proc.stdin.close();
const dumpPipe = async pipe => {
let data = await pipe.readString();
while (data) {
dump(data);
data = await pipe.readString();
}
};
dumpPipe(proc.stdout);
proc.wait().then(() => this.close());
return proc;
}, err => {
console.log(`Error loading Browser Toolbox: ${command} ${args.join(" ")}`, err);
});
},
/**
* Called upon receiving the connectionchange event from a debuggerServer.
*
* @param {String} what
* Type of connection change (can be either 'opened' or 'closed').
* @param {DebuggerServerConnection} connection
* The connection that was opened or closed.
*/
_onConnectionChange: function(what, connection) {
const wrappedJSObject = { what, connection };
Services.obs.notifyObservers({ wrappedJSObject }, "toolbox-connection-change");
},
/**
* Closes the remote debugging server and kills the toolbox process.
*/
close: async function() {
if (this.closed) {
return;
}
this.closed = true;
dumpn("Cleaning up the chrome debugging process.");
Services.obs.removeObserver(this.close, "quit-application");
this._dbgProcess.stdout.close();
await this._dbgProcess.kill();
this._telemetry.toolClosed("jsbrowserdebugger");
if (this.debuggerServer) {
this.debuggerServer.off("connectionchange", this._onConnectionChange);
this.debuggerServer.destroy();
this.debuggerServer = null;
}
dumpn("Chrome toolbox is now closed...");
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());
}
});
Services.prefs.addObserver("toolbox-update-addon-options", {
observe: (subject) => {
const {id, options} = subject.wrappedJSObject;
BrowserToolboxProcess.setAddonOptions(id, options);
}
});