Bug 1060093 - Implement toolbox to debug chrome of the content process. r=jryans

This commit is contained in:
Alexandre Poirot 2014-11-07 14:24:47 -06:00
Родитель 9df3eb992d
Коммит 64aef65e17
16 изменённых файлов: 486 добавлений и 7 удалений

Просмотреть файл

@ -513,6 +513,8 @@
<menuitem id="menu_browserToolbox"
observes="devtoolsMenuBroadcaster_BrowserToolbox"
accesskey="&browserToolboxMenu.accesskey;"/>
<menuitem id="menu_browserContentToolbox"
observes="devtoolsMenuBroadcaster_BrowserContentToolbox"/>
<menuitem id="menu_browserConsole"
observes="devtoolsMenuBroadcaster_BrowserConsole"
accesskey="&browserConsoleCmd.accesskey;"/>

Просмотреть файл

@ -100,6 +100,7 @@
<command id="Tools:DevAppMgr" oncommand="gDevToolsBrowser.openAppManager(gBrowser);" disabled="true" hidden="true"/>
<command id="Tools:WebIDE" oncommand="gDevToolsBrowser.openWebIDE();" disabled="true" hidden="true"/>
<command id="Tools:BrowserToolbox" oncommand="BrowserToolboxProcess.init();" disabled="true" hidden="true"/>
<command id="Tools:BrowserContentToolbox" oncommand="gDevToolsBrowser.openContentProcessToolbox();" disabled="true" hidden="true"/>
<command id="Tools:BrowserConsole" oncommand="HUDService.toggleBrowserConsole();"/>
<command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();"/>
<command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();"/>
@ -207,6 +208,9 @@
<broadcaster id="devtoolsMenuBroadcaster_BrowserToolbox"
label="&browserToolboxMenu.label;"
command="Tools:BrowserToolbox"/>
<broadcaster id="devtoolsMenuBroadcaster_BrowserContentToolbox"
label="&browserContentToolboxMenu.label;"
command="Tools:BrowserContentToolbox"/>
<broadcaster id="devtoolsMenuBroadcaster_BrowserConsole"
label="&browserConsoleCmd.label;"
key="key_browserConsole"

Просмотреть файл

@ -20,11 +20,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
"resource://gre/modules/devtools/dbg-server.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
"resource://gre/modules/devtools/dbg-client.jsm");
const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
const bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
/**
* The method name to use for ES6 iteration. If symbols are enabled in this
* build, use Symbol.iterator; otherwise "@@iterator".
@ -593,6 +600,7 @@ let gDevToolsBrowser = {
let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled &&
Services.prefs.getBoolPref("devtools.debugger.chrome-enabled");
toggleCmd("Tools:BrowserToolbox", remoteEnabled);
toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
// Enable Error Console?
let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
@ -688,6 +696,61 @@ let gDevToolsBrowser = {
}
},
_getContentProcessTarget: function () {
// Create a DebuggerServer in order to connect locally to it
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
let deferred = promise.defer();
client.connect(() => {
client.mainRoot.listProcesses(response => {
// Do nothing if there is only one process, the parent process.
let contentProcesses = response.processes.filter(p => (!p.parent));
if (contentProcesses.length < 1) {
let msg = bundle.GetStringFromName("toolbox.noContentProcess.message");
Services.prompt.alert(null, "", msg);
deferred.reject("No content processes available.");
return;
}
// Otherwise, arbitrary connect to the unique content process.
client.attachProcess(contentProcesses[0].id)
.then(response => {
let options = {
form: response.form,
client: client,
chrome: true
};
return devtools.TargetFactory.forRemoteTab(options);
})
.then(target => {
// Ensure closing the connection in order to cleanup
// the debugger client and also the server created in the
// content process
target.on("close", () => {
client.close();
});
deferred.resolve(target);
});
});
});
return deferred.promise;
},
openContentProcessToolbox: function () {
this._getContentProcessTarget()
.then(target => {
// Display a new toolbox, in a new window, with debugger by default
return gDevTools.showToolbox(target, "jsdebugger",
devtools.Toolbox.HostType.WINDOW);
});
},
/**
* Install WebIDE widget
*/

Просмотреть файл

@ -268,6 +268,11 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY browserToolboxMenu.label "Browser Toolbox">
<!ENTITY browserToolboxMenu.accesskey "e">
<!-- LOCALIZATION NOTE (browserContentToolboxMenu.label): This is the label for the
- application menu item that opens the browser content toolbox UI in the Tools menu.
- This toolbox allows to debug the chrome of the content process in multiprocess builds. -->
<!ENTITY browserContentToolboxMenu.label "Browser Content Toolbox">
<!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
<!ENTITY devToolbarMenu.label "Developer Toolbar">
<!ENTITY devToolbarMenu.accesskey "v">

Просмотреть файл

@ -78,3 +78,8 @@ options.darkTheme.label=Dark theme
# LOCALIZATION NOTE (options.lightTheme.label)
# Used as a label for light theme
options.lightTheme.label=Light theme
# LOCALIZATION NOTE (toolbox.noContentProcess.message)
# Used as a message in the alert displayed when trying to open a browser
# content toolbox and there is no content process running
toolbox.noContentProcess.message=No content process running.

Просмотреть файл

@ -25,6 +25,7 @@ ContentProcessSingleton.prototype = {
case "app-startup": {
Services.obs.addObserver(this, "console-api-log-event", false);
Services.obs.addObserver(this, "xpcom-shutdown", false);
cpmm.addMessageListener("DevTools:InitDebuggerServer", this);
break;
}
case "console-api-log-event": {
@ -56,9 +57,19 @@ ContentProcessSingleton.prototype = {
case "xpcom-shutdown":
Services.obs.removeObserver(this, "console-api-log-event");
Services.obs.removeObserver(this, "xpcom-shutdown");
cpmm.removeMessageListener("DevTools:InitDebuggerServer", this);
break;
}
},
receiveMessage: function (message) {
// load devtools component on-demand
// Only reply if we are in a real content process
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
let {init} = Cu.import("resource://gre/modules/devtools/content-server.jsm", {});
init(message);
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentProcessSingleton]);

Просмотреть файл

@ -5191,6 +5191,20 @@
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'listAddons' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_LOCAL_LISTPROCESSES_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000",
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'listProcesses' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_REMOTE_LISTPROCESSES_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000",
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'listProcesses' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_LOCAL_DELETE_MS": {
"expires_in_version": "never",
"kind": "exponential",

Просмотреть файл

@ -594,6 +594,21 @@ DebuggerClient.prototype = {
});
},
/**
* Attach to a process in order to get the form of a ChildProcessActor.
*
* @param string aId
* The ID for the process to attach (returned by `listProcesses`).
*/
attachProcess: function (aId) {
let packet = {
to: 'root',
type: 'attachProcess',
id: aId
}
return this.request(packet);
},
/**
* Release an object actor.
*
@ -1292,6 +1307,15 @@ RootClient.prototype = {
listAddons: DebuggerClient.requester({ type: "listAddons" },
{ telemetry: "LISTADDONS" }),
/**
* List the running processes.
*
* @param function aOnResponse
* Called with the response packet.
*/
listProcesses: DebuggerClient.requester({ type: "listProcesses" },
{ telemetry: "LISTPROCESSES" }),
/**
* Description of protocol's actors and methods.
*

Просмотреть файл

@ -0,0 +1,94 @@
/* 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 { Cc, Ci, Cu } = require("chrome");
const { ChromeDebuggerActor } = require("devtools/server/actors/script");
const { WebConsoleActor } = require("devtools/server/actors/webconsole");
const makeDebugger = require("devtools/server/actors/utils/make-debugger");
const { ActorPool } = require("devtools/server/main");
const Services = require("Services");
function ChildProcessActor(aConnection) {
this.conn = aConnection;
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this._threadActor = null;
// Use a see-everything debugger
this.makeDebugger = makeDebugger.bind(null, {
findDebuggees: dbg => dbg.findAllGlobals(),
shouldAddNewGlobalAsDebuggee: global => true
});
// Scope into which the webconsole executes:
// An empty sandbox with chrome privileges
let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
.createInstance(Ci.nsIPrincipal);
let sandbox = Cu.Sandbox(systemPrincipal);
this._consoleScope = sandbox;
}
exports.ChildProcessActor = ChildProcessActor;
ChildProcessActor.prototype = {
actorPrefix: "process",
get isRootActor() true,
get exited() {
return !this._contextPool;
},
get url() {
return undefined;
},
get window() {
return this._consoleScope;
},
form: function() {
if (!this._consoleActor) {
this._consoleActor = new WebConsoleActor(this.conn, this);
this._contextPool.addActor(this._consoleActor);
}
if (!this._threadActor) {
this._threadActor = new ChromeDebuggerActor(this.conn, this);
this._contextPool.addActor(this._threadActor);
}
return {
actor: this.actorID,
name: "Content process",
consoleActor: this._consoleActor.actorID,
chromeDebugger: this._threadActor.actorID,
traits: {
highlightable: false,
networkMonitor: false,
},
};
},
disconnect: function() {
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
},
preNest: function() {
// TODO: freeze windows
// window mediator doesn't work in child.
// it doesn't throw, but doesn't return any window
},
postNest: function() {
},
};
ChildProcessActor.prototype.requestTypes = {
};

Просмотреть файл

@ -6,7 +6,7 @@
"use strict";
const { Ci, Cu } = require("chrome");
const { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
@ -18,6 +18,10 @@ DevToolsUtils.defineLazyGetter(this, "StyleSheetActor", () => {
return require("devtools/server/actors/stylesheets").StyleSheetActor;
});
DevToolsUtils.defineLazyGetter(this, "ppmm", () => {
return Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
});
/* Root actor for the remote debugging protocol. */
/**
@ -358,6 +362,28 @@ RootActor.prototype = {
this._parameters.addonList.onListChanged = null;
},
onListProcesses: function () {
let processes = [];
for (let i = 0; i < ppmm.childCount; i++) {
processes.push({
id: i, // XXX: may not be a perfect id, but process message manager doesn't expose anything...
parent: i == 0, // XXX Weak, but appear to be stable
tabCount: undefined, // TODO: exposes process message manager on frameloaders in order to compute this
});
}
return { processes: processes };
},
onAttachProcess: function (aRequest) {
let mm = ppmm.getChildAt(aRequest.id);
if (!mm) {
return { error: "noProcess",
message: "There is no process with id '" + aRequest.id + "'." };
}
return DebuggerServer.connectToContent(this.conn, mm)
.then(form => ({ form: form }));
},
/* This is not in the spec, but it's used by tests. */
onEcho: function (aRequest) {
/*
@ -431,6 +457,8 @@ RootActor.prototype = {
RootActor.prototype.requestTypes = {
"listTabs": RootActor.prototype.onListTabs,
"listAddons": RootActor.prototype.onListAddons,
"listProcesses": RootActor.prototype.onListProcesses,
"attachProcess": RootActor.prototype.onAttachProcess,
"echo": RootActor.prototype.onEcho,
"protocolDescription": RootActor.prototype.onProtocolDescription
};

Просмотреть файл

@ -559,13 +559,15 @@ WebConsoleActor.prototype =
startedListeners.push(listener);
break;
case "FileActivity":
if (!this.consoleProgressListener) {
this.consoleProgressListener =
new ConsoleProgressListener(this.window, this);
if (this.window instanceof Ci.nsIDOMWindow) {
if (!this.consoleProgressListener) {
this.consoleProgressListener =
new ConsoleProgressListener(this.window, this);
}
this.consoleProgressListener.startMonitor(this.consoleProgressListener.
MONITOR_FILE_ACTIVITY);
startedListeners.push(listener);
}
this.consoleProgressListener.startMonitor(this.consoleProgressListener.
MONITOR_FILE_ACTIVITY);
startedListeners.push(listener);
break;
case "ReflowActivity":
if (!this.consoleReflowListener) {

Просмотреть файл

@ -0,0 +1,64 @@
/* 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 Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { DevToolsLoader } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
this.EXPORTED_SYMBOLS = ["init"];
let started = false;
function init(msg) {
if (started) {
return;
}
started = true;
// Init a custom, invisible DebuggerServer, in order to not pollute
// the debugger with all devtools modules, nor break the debugger itself with using it
// in the same process.
let devtools = new DevToolsLoader();
devtools.invisibleToDebugger = true;
devtools.main("devtools/server/main");
let { DebuggerServer, ActorPool } = devtools;
if (!DebuggerServer.initialized) {
DebuggerServer.init();
}
// In case of apps being loaded in parent process, DebuggerServer is already
// initialized, but child specific actors are not registered.
// Otherwise, for child process, we need to load actors the first
// time we load child.js
DebuggerServer.addChildActors();
let mm = msg.target;
mm.QueryInterface(Ci.nsISyncMessageSender);
let prefix = msg.data.prefix;
// Connect both parent/child processes debugger servers RDP via message managers
let conn = DebuggerServer.connectToParent(prefix, mm);
let { ChildProcessActor } = devtools.require("devtools/server/actors/child-process");
let actor = new ChildProcessActor(conn);
let actorPool = new ActorPool(conn);
actorPool.addActor(actor);
conn.addActorPool(actorPool);
let response = {actor: actor.form()};
mm.sendAsyncMessage("debug:content-process-actor", response);
mm.addMessageListener("debug:content-process-destroy", function onDestroy() {
mm.removeMessageListener("debug:content-process-destroy", onDestroy);
DebuggerServer.destroy();
started = false;
});
}

Просмотреть файл

@ -704,6 +704,69 @@ var DebuggerServer = {
return this._onConnection(transport, aPrefix, true);
},
connectToContent: function (aConnection, aMm) {
let deferred = defer();
let prefix = aConnection.allocID("content-process");
let actor, childTransport;
aMm.addMessageListener("debug:content-process-actor", function listener(msg) {
// Arbitrarily choose the first content process to reply
// XXX: This code needs to be updated if we use more than one content process
aMm.removeMessageListener("debug:content-process-actor", listener);
// Pipe Debugger message from/to parent/child via the message manager
childTransport = new ChildDebuggerTransport(aMm, prefix);
childTransport.hooks = {
onPacket: aConnection.send.bind(aConnection),
onClosed: function () {}
};
childTransport.ready();
aConnection.setForwarding(prefix, childTransport);
dumpn("establishing forwarding for process with prefix " + prefix);
actor = msg.json.actor;
deferred.resolve(actor);
});
aMm.sendAsyncMessage("DevTools:InitDebuggerServer", {
prefix: prefix
});
function onDisconnect() {
Services.obs.removeObserver(onMessageManagerDisconnect, "message-manager-disconnect");
events.off(aConnection, "closed", onDisconnect);
if (childTransport) {
// If we have a child transport, the actor has already
// been created. We need to stop using this message manager.
childTransport.close();
childTransport = null;
aConnection.cancelForwarding(prefix);
// ... and notify the child process to clean the tab actors.
try {
aMm.sendAsyncMessage("debug:content-process-destroy");
} catch(e) {}
}
}
let onMessageManagerDisconnect = DevToolsUtils.makeInfallible(function (subject, topic, data) {
if (subject == aMm) {
onDisconnect();
aConnection.send({ from: actor.actor, type: "tabDetached" });
}
}).bind(this);
Services.obs.addObserver(onMessageManagerDisconnect,
"message-manager-disconnect", false);
events.on(aConnection, "closed", onDisconnect);
return deferred.promise;
},
/**
* Connect to a child process.
*

Просмотреть файл

@ -21,6 +21,7 @@ SOURCES += [
FINAL_LIBRARY = 'xul'
EXTRA_JS_MODULES.devtools += [
'content-server.jsm',
'dbg-server.jsm',
]
@ -34,6 +35,7 @@ EXTRA_JS_MODULES.devtools.server += [
EXTRA_JS_MODULES.devtools.server.actors += [
'actors/call-watcher.js',
'actors/canvas.js',
'actors/child-process.js',
'actors/childtab.js',
'actors/common.js',
'actors/csscoverage.js',

Просмотреть файл

@ -73,3 +73,5 @@ skip-if = buildapp == 'mulet'
[test_preference.html]
[test_connectToChild.html]
skip-if = buildapp == 'mulet'
[test_attachProcess.html]
skip-if = buildapp == 'mulet'

Просмотреть файл

@ -0,0 +1,96 @@
<SDOCTYPv HTM.>
<html>
<!--
Bug 1060093 - Test DebuggerServer.attachProcess
-->
<head>
<meta charset="utf-8">
<title>Mozilla Bug</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script type="application/javascript;version=1.8">
let Cu = Components.utils;
let Cc = Components.classes;
let Ci = Components.interfaces;
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
window.onload = function() {
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv({
"set": [
// Always log packets when running tests.
["devtools.debugger.log", true],
["dom.mozBrowserFramesEnabled", true]
]
}, runTests);
}
function runTests() {
// Create a remote iframe with a message manager
let iframe = document.createElement("iframe");
iframe.mozbrowser = true;
iframe.setAttribute("remote", "true");
iframe.setAttribute("src", "data:text/html,foo");
document.body.appendChild(iframe);
let mm = iframe.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
// Instantiate a minimal server
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
}
if (!DebuggerServer.createRootActor) {
DebuggerServer.addBrowserActors();
}
function firstClient() {
// Fake a first connection to the content process
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
client.connect(() => {
client.mainRoot.listProcesses(response => {
ok(response.processes.length >= 2, "Got at least the parent process and one child");
// Connect to the first content processe available
let content = response.processes.filter(p => (!p.parent))[0];
client.attachProcess(content.id).then(response => {
let actor = response.form;
ok(actor.consoleActor, "Got the console actor");
ok(actor.chromeDebugger, "Got the thread actor");
// Ensure sending at least one request to an actor...
client.request({
to: actor.consoleActor,
type: "evaluateJS",
text: "var a = 42; a"
}, function (response) {
ok(response.result, 42, "console.eval worked");
client.close(cleanup);
});
});
});
});
}
function cleanup() {
DebuggerServer.destroy();
iframe.remove();
SimpleTest.finish()
}
firstClient();
}
</script>
</pre>
</body>
</html>