gecko-dev/devtools/server/actors/root.js
Sebastian Hengst 3a10644021 Backed out 6 changesets (bug 888600) for beta simulation failures: build bustage on Linux and Windows opt (bug 1442036) and devtools failure browser_net_view-source-debugger.js (bug 1441961). a=backout
Backed out changeset 83c87140dc3d (bug 888600)
Backed out changeset 2efb9b1753f6 (bug 888600)
Backed out changeset af5303781961 (bug 888600)
Backed out changeset 79ef59047e63 (bug 888600)
Backed out changeset 30d568d628dd (bug 888600)
Backed out changeset c7bd4c6c9741 (bug 888600)

--HG--
extra : histedit_source : 791b22f6770f4fead2f909478a93d65d85829fe0%2Cbb387309e90f53e1dde45dcf8cf4ebedcc6e5c5e
2018-03-01 11:51:09 +02:00

622 lines
22 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 { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
loader.lazyGetter(this, "ppmm", () => {
return Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(
Ci.nsIMessageBroadcaster);
});
loader.lazyRequireGetter(this, "WindowActor",
"devtools/server/actors/window", true);
/* Root actor for the remote debugging protocol. */
/**
* Create a remote debugging protocol root actor.
*
* @param connection
* The DebuggerServerConnection whose root actor we are constructing.
*
* @param parameters
* The properties of |parameters| provide backing objects for the root
* actor's requests; if a given property is omitted from |parameters|, the
* root actor won't implement the corresponding requests or notifications.
* Supported properties:
*
* - tabList: a live list (see below) of tab actors. If present, the
* new root actor supports the 'listTabs' request, providing the live
* list's elements as its tab actors, and sending 'tabListChanged'
* notifications when the live list's contents change. One actor in
* this list must have a true '.selected' property.
*
* - addonList: a live list (see below) of addon actors. If present, the
* new root actor supports the 'listAddons' request, providing the live
* list's elements as its addon actors, and sending 'addonListchanged'
* notifications when the live list's contents change.
*
* - globalActorFactories: an object |A| describing further actors to
* attach to the 'listTabs' reply. This is the type accumulated by
* DebuggerServer.addGlobalActor. For each own property |P| of |A|,
* the root actor adds a property named |P| to the 'listTabs'
* reply whose value is the name of an actor constructed by
* |A[P]|.
*
* - onShutdown: a function to call when the root actor is destroyed.
*
* Instance properties:
*
* - applicationType: the string the root actor will include as the
* "applicationType" property in the greeting packet. By default, this
* is "browser".
*
* Live lists:
*
* A "live list", as used for the |tabList|, is an object that presents a
* list of actors, and also notifies its clients of changes to the list. A
* live list's interface is two properties:
*
* - getList: a method that returns a promise to the contents of the list.
*
* - onListChanged: a handler called, with no arguments, when the set of
* values the iterator would produce has changed since the last
* time 'iterator' was called. This may only be set to null or a
* callable value (one for which the typeof operator returns
* 'function'). (Note that the live list will not call the
* onListChanged handler until the list has been iterated over
* once; if nobody's seen the list in the first place, nobody
* should care if its contents have changed!)
*
* When the list changes, the list implementation should ensure that any
* actors yielded in previous iterations whose referents (tabs) still exist
* get yielded again in subsequent iterations. If the underlying referent
* is the same, the same actor should be presented for it.
*
* The root actor registers an 'onListChanged' handler on the appropriate
* list when it may need to send the client 'tabListChanged' notifications,
* and is careful to remove the handler whenever it does not need to send
* such notifications (including when it is destroyed). This means that
* live list implementations can use the state of the handler property (set
* or null) to install and remove observers and event listeners.
*
* Note that, as the only way for the root actor to see the members of the
* live list is to begin an iteration over the list, the live list need not
* actually produce any actors until they are reached in the course of
* iteration: alliterative lazy live lists.
*/
function RootActor(connection, parameters) {
this.conn = connection;
this._parameters = parameters;
this._onTabListChanged = this.onTabListChanged.bind(this);
this._onAddonListChanged = this.onAddonListChanged.bind(this);
this._onWorkerListChanged = this.onWorkerListChanged.bind(this);
this._onServiceWorkerRegistrationListChanged =
this.onServiceWorkerRegistrationListChanged.bind(this);
this._onProcessListChanged = this.onProcessListChanged.bind(this);
this._extraActors = {};
this._globalActorPool = new ActorPool(this.conn);
this.conn.addActorPool(this._globalActorPool);
this._chromeActor = null;
this._processActors = new Map();
}
RootActor.prototype = {
constructor: RootActor,
applicationType: "browser",
traits: {
sources: true,
// Whether the inspector actor allows modifying outer HTML.
editOuterHTML: true,
// Whether the inspector actor allows modifying innerHTML and inserting
// adjacent HTML.
pasteHTML: true,
// Whether the server-side highlighter actor exists and can be used to
// remotely highlight nodes (see server/actors/highlighters.js)
highlightable: true,
// Which custom highlighter does the server-side highlighter actor supports?
// (see server/actors/highlighters.js)
customHighlighters: true,
// Whether the inspector actor implements the getImageDataFromURL
// method that returns data-uris for image URLs. This is used for image
// tooltips for instance
urlToImageDataResolver: true,
networkMonitor: true,
// Whether the storage inspector actor to inspect cookies, etc.
storageInspector: true,
// Whether storage inspector is read only
storageInspectorReadOnly: true,
// Whether conditional breakpoints are supported
conditionalBreakpoints: true,
// Whether the server supports full source actors (breakpoints on
// eval scripts, etc)
debuggerSourceActors: true,
// Whether the server can return wasm binary source
wasmBinarySource: true,
bulk: true,
// Whether the style rule actor implements the modifySelector method
// that modifies the rule's selector
selectorEditable: true,
// Whether the page style actor implements the addNewRule method that
// adds new rules to the page
addNewRule: true,
// Whether the dom node actor implements the getUniqueSelector method
getUniqueSelector: true,
// Whether the dom node actor implements the getCssPath method
getCssPath: true,
// Whether the dom node actor implements the getXPath method
getXPath: true,
// Whether the director scripts are supported
directorScripts: true,
// Whether the debugger server supports
// blackboxing/pretty-printing (not supported in Fever Dream yet)
noBlackBoxing: false,
noPrettyPrinting: false,
// Whether the page style actor implements the getUsedFontFaces method
// that returns the font faces used on a node
getUsedFontFaces: true,
// Trait added in Gecko 38, indicating that all features necessary for
// grabbing allocations from the MemoryActor are available for the performance tool
memoryActorAllocations: true,
// Added in Firefox 40. Indicates that the backend supports registering custom
// commands through the WebConsoleCommands API.
webConsoleCommands: true,
// Whether root actor exposes tab actors and access to any window.
// If allowChromeProcess is true, you can:
// * get a ChromeActor instance to debug chrome and any non-content
// resource via getProcess requests
// * get a WindowActor instance to debug windows which could be chrome,
// like browser windows via getWindow requests
// If allowChromeProcess is defined, but not true, it means that root actor
// no longer expose tab actors, but also that the above requests are
// forbidden for security reasons.
get allowChromeProcess() {
return DebuggerServer.allowChromeProcess;
},
// Whether or not `getProfile()` supports specifying a `startTime`
// and `endTime` to filter out samples. Fx40+
profilerDataFilterable: true,
// Whether or not the MemoryActor's heap snapshot abilities are
// fully equipped to handle heap snapshots for the memory tool. Fx44+
heapSnapshots: true,
// Whether or not the timeline actor can emit DOMContentLoaded and Load
// markers, currently in use by the network monitor. Fx45+
documentLoadingMarkers: true,
// Whether or not the webextension addon actor have to be connected
// to retrieve the extension child process tab actors.
webExtensionAddonConnect: true,
},
/**
* Return a 'hello' packet as specified by the Remote Debugging Protocol.
*/
sayHello: function () {
return {
from: this.actorID,
applicationType: this.applicationType,
/* This is not in the spec, but it's used by tests. */
testConnectionPrefix: this.conn.prefix,
traits: this.traits
};
},
forwardingCancelled: function (prefix) {
return {
from: this.actorID,
type: "forwardingCancelled",
prefix,
};
},
/**
* Destroys the actor from the browser window.
*/
destroy: function () {
/* Tell the live lists we aren't watching any more. */
if (this._parameters.tabList) {
this._parameters.tabList.onListChanged = null;
}
if (this._parameters.addonList) {
this._parameters.addonList.onListChanged = null;
}
if (this._parameters.workerList) {
this._parameters.workerList.onListChanged = null;
}
if (this._parameters.serviceWorkerRegistrationList) {
this._parameters.serviceWorkerRegistrationList.onListChanged = null;
}
if (this._parameters.processList) {
this._parameters.processList.onListChanged = null;
}
if (typeof this._parameters.onShutdown === "function") {
this._parameters.onShutdown();
}
this._extraActors = null;
this.conn = null;
this._tabActorPool = null;
this._globalActorPool = null;
this._windowActorPool = null;
this._parameters = null;
this._chromeActor = null;
this._processActors.clear();
},
/**
* Gets the "root" form, which lists all the global actors that affect the entire
* browser. This can replace usages of `listTabs` that only wanted the global actors
* and didn't actually care about tabs.
*/
onGetRoot: function () {
let reply = {
from: this.actorID,
};
// Create global actors
if (!this._globalActorPool) {
this._globalActorPool = new ActorPool(this.conn);
this.conn.addActorPool(this._globalActorPool);
}
this._createExtraActors(this._parameters.globalActorFactories, this._globalActorPool);
// List the global actors
this._appendExtraActors(reply);
return reply;
},
/* The 'listTabs' request and the 'tabListChanged' notification. */
/**
* Handles the listTabs request. The actors will survive until at least
* the next listTabs request.
*
* ⚠ WARNING ⚠ This can be a very expensive operation, especially if there are many
* open tabs. It will cause us to visit every tab, load a frame script, start a
* debugger server, and read some data. With lazy tab support (bug 906076), this
* would trigger any lazy tabs to be loaded, greatly increasing resource usage. Avoid
* this method whenever possible.
*/
onListTabs: async function (request) {
let tabList = this._parameters.tabList;
if (!tabList) {
return { from: this.actorID, error: "noTabs",
message: "This root actor has no browser tabs." };
}
// Now that a client has requested the list of tabs, we reattach the onListChanged
// listener in order to be notified if the list of tabs changes again in the future.
tabList.onListChanged = this._onTabListChanged;
// Walk the tab list, accumulating the array of tab actors for the reply, and moving
// all the actors to a new ActorPool. We'll replace the old tab actor pool with the
// one we build here, thus retiring any actors that didn't get listed again, and
// preparing any new actors to receive packets.
let newActorPool = new ActorPool(this.conn);
let tabActorList = [];
let selected;
let options = request.options || {};
let tabActors = await tabList.getList(options);
for (let tabActor of tabActors) {
if (tabActor.exited) {
// Tab actor may have exited while we were gathering the list.
continue;
}
if (tabActor.selected) {
selected = tabActorList.length;
}
tabActor.parentID = this.actorID;
newActorPool.addActor(tabActor);
tabActorList.push(tabActor);
}
// Start with the root reply, which includes the global actors for the whole browser.
let reply = this.onGetRoot();
// Drop the old actorID -> actor map. Actors that still mattered were added to the
// new map; others will go away.
if (this._tabActorPool) {
this.conn.removeActorPool(this._tabActorPool);
}
this._tabActorPool = newActorPool;
this.conn.addActorPool(this._tabActorPool);
// We'll extend the reply here to also mention all the tabs.
Object.assign(reply, {
selected: selected || 0,
tabs: tabActorList.map(actor => actor.form()),
});
return reply;
},
onGetTab: async function (options) {
let tabList = this._parameters.tabList;
if (!tabList) {
return { error: "noTabs",
message: "This root actor has no browser tabs." };
}
if (!this._tabActorPool) {
this._tabActorPool = new ActorPool(this.conn);
this.conn.addActorPool(this._tabActorPool);
}
let tabActor;
try {
tabActor = await tabList.getTab(options);
} catch (error) {
if (error.error) {
// Pipe expected errors as-is to the client
return error;
}
return {
error: "noTab",
message: "Unexpected error while calling getTab(): " + error
};
}
tabActor.parentID = this.actorID;
this._tabActorPool.addActor(tabActor);
return { tab: tabActor.form() };
},
onGetWindow: function ({ outerWindowID }) {
if (!DebuggerServer.allowChromeProcess) {
return {
from: this.actorID,
error: "forbidden",
message: "You are not allowed to debug windows."
};
}
let window = Services.wm.getOuterWindowWithId(outerWindowID);
if (!window) {
return {
from: this.actorID,
error: "notFound",
message: `No window found with outerWindowID ${outerWindowID}`,
};
}
if (!this._windowActorPool) {
this._windowActorPool = new ActorPool(this.conn);
this.conn.addActorPool(this._windowActorPool);
}
let actor = new WindowActor(this.conn, window);
actor.parentID = this.actorID;
this._windowActorPool.addActor(actor);
return {
from: this.actorID,
window: actor.form(),
};
},
onTabListChanged: function () {
this.conn.send({ from: this.actorID, type: "tabListChanged" });
/* It's a one-shot notification; no need to watch any more. */
this._parameters.tabList.onListChanged = null;
},
onListAddons: function () {
let addonList = this._parameters.addonList;
if (!addonList) {
return { from: this.actorID, error: "noAddons",
message: "This root actor has no browser addons." };
}
// Reattach the onListChanged listener now that a client requested the list.
addonList.onListChanged = this._onAddonListChanged;
return addonList.getList().then((addonActors) => {
let addonActorPool = new ActorPool(this.conn);
for (let addonActor of addonActors) {
addonActorPool.addActor(addonActor);
}
if (this._addonActorPool) {
this.conn.removeActorPool(this._addonActorPool);
}
this._addonActorPool = addonActorPool;
this.conn.addActorPool(this._addonActorPool);
return {
"from": this.actorID,
"addons": addonActors.map(addonActor => addonActor.form())
};
});
},
onAddonListChanged: function () {
this.conn.send({ from: this.actorID, type: "addonListChanged" });
this._parameters.addonList.onListChanged = null;
},
onListWorkers: function () {
let workerList = this._parameters.workerList;
if (!workerList) {
return { from: this.actorID, error: "noWorkers",
message: "This root actor has no workers." };
}
// Reattach the onListChanged listener now that a client requested the list.
workerList.onListChanged = this._onWorkerListChanged;
return workerList.getList().then(actors => {
let pool = new ActorPool(this.conn);
for (let actor of actors) {
pool.addActor(actor);
}
this.conn.removeActorPool(this._workerActorPool);
this._workerActorPool = pool;
this.conn.addActorPool(this._workerActorPool);
return {
"from": this.actorID,
"workers": actors.map(actor => actor.form())
};
});
},
onWorkerListChanged: function () {
this.conn.send({ from: this.actorID, type: "workerListChanged" });
this._parameters.workerList.onListChanged = null;
},
onListServiceWorkerRegistrations: function () {
let registrationList = this._parameters.serviceWorkerRegistrationList;
if (!registrationList) {
return { from: this.actorID, error: "noServiceWorkerRegistrations",
message: "This root actor has no service worker registrations." };
}
// Reattach the onListChanged listener now that a client requested the list.
registrationList.onListChanged = this._onServiceWorkerRegistrationListChanged;
return registrationList.getList().then(actors => {
let pool = new ActorPool(this.conn);
for (let actor of actors) {
pool.addActor(actor);
}
this.conn.removeActorPool(this._serviceWorkerRegistrationActorPool);
this._serviceWorkerRegistrationActorPool = pool;
this.conn.addActorPool(this._serviceWorkerRegistrationActorPool);
return {
"from": this.actorID,
"registrations": actors.map(actor => actor.form())
};
});
},
onServiceWorkerRegistrationListChanged: function () {
this.conn.send({ from: this.actorID, type: "serviceWorkerRegistrationListChanged" });
this._parameters.serviceWorkerRegistrationList.onListChanged = null;
},
onListProcesses: function () {
let { processList } = this._parameters;
if (!processList) {
return { from: this.actorID, error: "noProcesses",
message: "This root actor has no processes." };
}
processList.onListChanged = this._onProcessListChanged;
return {
processes: processList.getList()
};
},
onProcessListChanged: function () {
this.conn.send({ from: this.actorID, type: "processListChanged" });
this._parameters.processList.onListChanged = null;
},
onGetProcess: function (request) {
if (!DebuggerServer.allowChromeProcess) {
return { error: "forbidden",
message: "You are not allowed to debug chrome." };
}
if (("id" in request) && typeof (request.id) != "number") {
return { error: "wrongParameter",
message: "getProcess requires a valid `id` attribute." };
}
// If the request doesn't contains id parameter or id is 0
// (id == 0, based on onListProcesses implementation)
if ((!("id" in request)) || request.id === 0) {
if (!this._chromeActor) {
// Create a ChromeActor for the parent process
let { ChromeActor } = require("devtools/server/actors/chrome");
this._chromeActor = new ChromeActor(this.conn);
this._globalActorPool.addActor(this._chromeActor);
}
return { form: this._chromeActor.form() };
}
let { id } = request;
let mm = ppmm.getChildAt(id);
if (!mm) {
return { error: "noProcess",
message: "There is no process with id '" + id + "'." };
}
let form = this._processActors.get(id);
if (form) {
return { form };
}
let onDestroy = () => {
this._processActors.delete(id);
};
return DebuggerServer.connectToContent(this.conn, mm, onDestroy).then(formResult => {
this._processActors.set(id, formResult);
return { form: formResult };
});
},
/* This is not in the spec, but it's used by tests. */
onEcho: function (request) {
/*
* Request packets are frozen. Copy request, so that
* DebuggerServerConnection.onPacket can attach a 'from' property.
*/
return Cu.cloneInto(request, {});
},
onProtocolDescription: function () {
return require("devtools/shared/protocol").dumpProtocolSpec();
},
/* Support for DebuggerServer.addGlobalActor. */
_createExtraActors: createExtraActors,
_appendExtraActors: appendExtraActors,
/**
* Remove the extra actor (added by DebuggerServer.addGlobalActor or
* DebuggerServer.addTabActor) name |name|.
*/
removeActorByName: function (name) {
if (name in this._extraActors) {
const actor = this._extraActors[name];
if (this._globalActorPool.has(actor)) {
this._globalActorPool.removeActor(actor);
}
if (this._tabActorPool) {
// Iterate over TabActor instances to also remove tab actors
// created during listTabs for each document.
this._tabActorPool.forEach(tab => {
tab.removeActorByName(name);
});
}
delete this._extraActors[name];
}
}
};
RootActor.prototype.requestTypes = {
getRoot: RootActor.prototype.onGetRoot,
listTabs: RootActor.prototype.onListTabs,
getTab: RootActor.prototype.onGetTab,
getWindow: RootActor.prototype.onGetWindow,
listAddons: RootActor.prototype.onListAddons,
listWorkers: RootActor.prototype.onListWorkers,
listServiceWorkerRegistrations: RootActor.prototype.onListServiceWorkerRegistrations,
listProcesses: RootActor.prototype.onListProcesses,
getProcess: RootActor.prototype.onGetProcess,
echo: RootActor.prototype.onEcho,
protocolDescription: RootActor.prototype.onProtocolDescription
};
exports.RootActor = RootActor;