Bug 977443 - Implement an actor that defines new actors. r=ochameau

This commit is contained in:
Jan Odvarko 2014-10-03 12:49:00 +01:00
parent 52706a954d
commit 113d163096
12 changed files with 369 additions and 42 deletions

View File

@ -86,11 +86,10 @@ exports.makeInfallible = function makeInfallible(aHandler, aName) {
if (aName) { if (aName) {
who += " " + aName; who += " " + aName;
} }
exports.reportException(who, ex); return exports.reportException(who, ex);
} }
} }
} }
/** /**
* Interleaves two arrays element by element, returning the combined array, like * Interleaves two arrays element by element, returning the combined array, like
* a zip. In the case of arrays with different sizes, undefined values will be * a zip. In the case of arrays with different sizes, undefined values will be

View File

@ -0,0 +1,153 @@
/* 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 protocol = require("devtools/server/protocol");
const { method, custom, Arg, Option, RetVal } = protocol;
const { Cu, CC, components } = require("chrome");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const Services = require("Services");
const { DebuggerServer } = require("devtools/server/main");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
/**
* The ActorActor gives you a handle to an actor you've dynamically
* registered and allows you to unregister it.
*/
const ActorActor = protocol.ActorClass({
typeName: "actorActor",
initialize: function (conn, options) {
protocol.Actor.prototype.initialize.call(this, conn);
this.options = options;
},
unregister: method(function () {
if (this.options.tab) {
DebuggerServer.removeTabActor(this.options);
}
if (this.options.global) {
DebuggerServer.removeGlobalActor(this.options);
}
}, {
request: {},
response: {}
})
});
const ActorActorFront = protocol.FrontClass(ActorActor, {
initialize: function (client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
}
});
exports.ActorActorFront = ActorActorFront;
/*
* The ActorRegistryActor allows clients to define new actors on the
* server. This is particularly useful for addons.
*/
const ActorRegistryActor = protocol.ActorClass({
typeName: "actorRegistry",
initialize: function (conn) {
protocol.Actor.prototype.initialize.call(this, conn);
},
registerActor: method(function (sourceText, fileName, options) {
const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
const sandbox = Cu.Sandbox(principal);
const exports = sandbox.exports = {};
sandbox.require = require;
Cu.evalInSandbox(sourceText, sandbox, "1.8", fileName, 1);
let { prefix, constructor, type } = options;
if (type.global) {
DebuggerServer.addGlobalActor({
constructorName: constructor,
constructorFun: sandbox[constructor]
}, prefix);
}
if (type.tab) {
DebuggerServer.addTabActor({
constructorName: constructor,
constructorFun: sandbox[constructor]
}, prefix);
}
return ActorActor(this.conn, {
name: constructor,
tab: type.tab,
global: type.global
});
}, {
request: {
sourceText: Arg(0, "string"),
filename: Arg(1, "string"),
options: Arg(2, "json")
},
response: {
actorActor: RetVal("actorActor")
}
})
});
exports.ActorRegistryActor = ActorRegistryActor;
function request(uri) {
return new Promise((resolve, reject) => {
try {
uri = Services.io.newURI(uri, null, null);
} catch (e) {
reject(e);
}
if (uri.scheme != "resource") {
reject(new Error(
"Can only register actors whose URI scheme is 'resource'."));
}
NetUtil.asyncFetch(uri, (stream, status, req) => {
if (!components.isSuccessCode(status)) {
reject(new Error("Request failed with status code = "
+ status
+ " after NetUtil.asyncFetch for url = "
+ uri));
return;
}
let source = NetUtil.readInputStreamToString(stream, stream.available());
stream.close();
resolve(source);
});
});
}
const ActorRegistryFront = protocol.FrontClass(ActorRegistryActor, {
initialize: function (client, form) {
protocol.Front.prototype.initialize.call(this, client,
{ actor: form.actorRegistryActor });
this.manage(this);
},
registerActor: custom(function (uri, options) {
return request(uri, options)
.then(sourceText => {
return this._registerActor(sourceText, uri, options);
});
}, {
impl: "_registerActor"
})
});
exports.ActorRegistryFront = ActorRegistryFront;

View File

@ -33,6 +33,10 @@ function RegisteredActorFactory(options, prefix) {
// By default the actor name will also be used for the actorID prefix. // By default the actor name will also be used for the actorID prefix.
this._prefix = prefix; this._prefix = prefix;
if (typeof(options) != "function") { if (typeof(options) != "function") {
// actors definition registered by actorRegistryActor
if (options.constructorFun) {
this._getConstructor = () => options.constructorFun;
} else {
// Lazy actor definition, where options contains all the information // Lazy actor definition, where options contains all the information
// required to load the actor lazily. // required to load the actor lazily.
this._getConstructor = function () { this._getConstructor = function () {
@ -52,16 +56,21 @@ function RegisteredActorFactory(options, prefix) {
} }
return c; return c;
}; };
}
// Exposes `name` attribute in order to allow removeXXXActor to match
// the actor by its actor constructor name.
this.name = options.constructorName;
} else { } else {
// Old actor case, where options is a function that is the actor constructor. // Old actor case, where options is a function that is the actor constructor.
this._getConstructor = () => options; this._getConstructor = () => options;
// Exposes `name` attribute in order to allow removeXXXActor to match // Exposes `name` attribute in order to allow removeXXXActor to match
// the actor by its actor contructor name. // the actor by its actor constructor name.
this.name = options.name; this.name = options.name;
// For old actors, we allow the use of a different prefix for actorID // For old actors, we allow the use of a different prefix for actorID
// than for listTabs actor names, by fetching a prefix on the actor prototype. // than for listTabs actor names, by fetching a prefix on the actor prototype.
// (Used by ChromeDebuggerActor) // (Used by ChromeDebuggerActor)
if (options.prototype.actorPrefix) { if (options.prototype && options.prototype.actorPrefix) {
this._prefix = options.prototype.actorPrefix; this._prefix = options.prototype.actorPrefix;
} }
} }
@ -79,9 +88,9 @@ exports.RegisteredActorFactory = RegisteredActorFactory;
* *
* ObservedActorFactory fakes the following actors attributes: * ObservedActorFactory fakes the following actors attributes:
* actorPrefix (string) Used by ActorPool.addActor to compute the actor id * actorPrefix (string) Used by ActorPool.addActor to compute the actor id
* actorID (string) Set by ActorPool.addActor just after being instanciated * actorID (string) Set by ActorPool.addActor just after being instantiated
* registeredPool (object) Set by ActorPool.addActor just after being * registeredPool (object) Set by ActorPool.addActor just after being
* instanciated * instantiated
* And exposes the following method: * And exposes the following method:
* createActor (function) Instantiate an actor that is going to replace * createActor (function) Instantiate an actor that is going to replace
* this factory in the actor pool. * this factory in the actor pool.
@ -123,7 +132,7 @@ exports.ObservedActorFactory = ObservedActorFactory;
* |aPool|. * |aPool|.
* *
* The root actor and the tab actor use this to instantiate actors that other * The root actor and the tab actor use this to instantiate actors that other
* parts of the browser have specified with DebuggerServer.addTabActor antd * parts of the browser have specified with DebuggerServer.addTabActor and
* DebuggerServer.addGlobalActor. * DebuggerServer.addGlobalActor.
* *
* @param aFactories * @param aFactories
@ -158,12 +167,17 @@ exports.createExtraActors = function createExtraActors(aFactories, aPool) {
// Register another factory, but this time specific to this connection. // Register another factory, but this time specific to this connection.
// It creates a fake actor that looks like an regular actor in the pool, // It creates a fake actor that looks like an regular actor in the pool,
// but without actually instantiating the actor. // but without actually instantiating the actor.
// It will only be instanciated on the first request made to the actor. // It will only be instantiated on the first request made to the actor.
actor = aFactories[name].createObservedActorFactory(this.conn, this); actor = aFactories[name].createObservedActorFactory(this.conn, this);
this._extraActors[name] = actor; this._extraActors[name] = actor;
} }
// If the actor already exists in the pool, it may have been instantiated,
// so make sure not to overwrite it by a non-instantiated version.
if (!aPool.has(actor.actorID)) {
aPool.addActor(actor); aPool.addActor(actor);
} }
}
} }
/** /**
@ -270,7 +284,13 @@ ActorPool.prototype = {
actor.disconnect(); actor.disconnect();
} }
this._cleanups = {}; this._cleanups = {};
},
forEach: function(callback) {
for (let name in this._actors) {
callback(this._actors[name]);
} }
},
} }
exports.ActorPool = ActorPool; exports.ActorPool = ActorPool;

View File

@ -280,14 +280,12 @@ RootActor.prototype = {
newActorPool.addActor(tabActor); newActorPool.addActor(tabActor);
tabActorList.push(tabActor); tabActorList.push(tabActor);
} }
/* DebuggerServer.addGlobalActor support: create actors. */ /* DebuggerServer.addGlobalActor support: create actors. */
if (!this._globalActorPool) { if (!this._globalActorPool) {
this._globalActorPool = new ActorPool(this.conn); this._globalActorPool = new ActorPool(this.conn);
this._createExtraActors(this._parameters.globalActorFactories, this._globalActorPool);
this.conn.addActorPool(this._globalActorPool); this.conn.addActorPool(this._globalActorPool);
} }
this._createExtraActors(this._parameters.globalActorFactories, this._globalActorPool);
/* /*
* Drop the old actorID -> actor map. Actors that still mattered were * Drop the old actorID -> actor map. Actors that still mattered were
* added to the new map; others will go away. * added to the new map; others will go away.
@ -436,7 +434,7 @@ RootActor.prototype = {
* is here because the Style Editor and Inspector share style sheet actors. * is here because the Style Editor and Inspector share style sheet actors.
* *
* @param DOMStyleSheet styleSheet * @param DOMStyleSheet styleSheet
* The style sheet to creat an actor for. * The style sheet to create an actor for.
* @return StyleSheetActor actor * @return StyleSheetActor actor
* The actor for this style sheet. * The actor for this style sheet.
* *
@ -451,6 +449,27 @@ RootActor.prototype = {
this._globalActorPool.addActor(actor); this._globalActorPool.addActor(actor);
return actor; return actor;
},
/**
* Remove the extra actor (added by DebuggerServer.addGlobalActor or
* DebuggerServer.addTabActor) name |aName|.
*/
removeActorByName: function(aName) {
if (aName in this._extraActors) {
const actor = this._extraActors[aName];
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(aName);
});
}
delete this._extraActors[aName];
}
} }
}; };

View File

@ -856,7 +856,7 @@ TabActor.prototype = {
_appendExtraActors: appendExtraActors, _appendExtraActors: appendExtraActors,
/** /**
* Does the actual work of attching to a tab. * Does the actual work of attaching to a tab.
*/ */
_attach: function BTA_attach() { _attach: function BTA_attach() {
if (this._attached) { if (this._attached) {
@ -1512,7 +1512,7 @@ TabActor.prototype = {
* is here because the Style Editor and Inspector share style sheet actors. * is here because the Style Editor and Inspector share style sheet actors.
* *
* @param DOMStyleSheet styleSheet * @param DOMStyleSheet styleSheet
* The style sheet to creat an actor for. * The style sheet to create an actor for.
* @return StyleSheetActor actor * @return StyleSheetActor actor
* The actor for this style sheet. * The actor for this style sheet.
* *
@ -1527,7 +1527,17 @@ TabActor.prototype = {
this._tabPool.addActor(actor); this._tabPool.addActor(actor);
return actor; return actor;
},
removeActorByName: function BTA_removeActor(aName) {
if (aName in this._extraActors) {
const actor = this._extraActors[aName];
if (this._tabActorPool.has(actor)) {
this._tabActorPool.removeActor(actor);
} }
delete this._extraActors[aName];
}
},
}; };
/** /**
@ -1550,7 +1560,7 @@ exports.TabActor = TabActor;
* <browser> tab. Most of the implementation comes from TabActor. * <browser> tab. Most of the implementation comes from TabActor.
* *
* @param aConnection DebuggerServerConnection * @param aConnection DebuggerServerConnection
* The conection to the client. * The connection to the client.
* @param aBrowser browser * @param aBrowser browser
* The browser instance that contains this tab. * The browser instance that contains this tab.
* @param aTabBrowser tabbrowser * @param aTabBrowser tabbrowser

View File

@ -324,7 +324,7 @@ var DebuggerServer = {
* - constructor (string): * - constructor (string):
* the name of the exported symbol to be used as the actor * the name of the exported symbol to be used as the actor
* constructor. * constructor.
* - type (a dictionnary of booleans with following attribute names): * - type (a dictionary of booleans with following attribute names):
* - "global" * - "global"
* registers a global actor instance, if true. * registers a global actor instance, if true.
* A global actor has the root actor as its parent. * A global actor has the root actor as its parent.
@ -347,7 +347,7 @@ var DebuggerServer = {
throw new Error("Lazy actor definition for '" + id + "' requires a string 'constructor' option."); throw new Error("Lazy actor definition for '" + id + "' requires a string 'constructor' option.");
} }
if (!("global" in type) && !("tab" in type)) { if (!("global" in type) && !("tab" in type)) {
throw new Error("Lazy actor definition for '" + id + "' requires a dictionnary 'type' option whose attributes can be 'global' or 'tab'."); throw new Error("Lazy actor definition for '" + id + "' requires a dictionary 'type' option whose attributes can be 'global' or 'tab'.");
} }
let name = prefix + "Actor"; let name = prefix + "Actor";
let mod = { let mod = {
@ -433,6 +433,11 @@ var DebuggerServer = {
constructor: "PreferenceActor", constructor: "PreferenceActor",
type: { global: true } type: { global: true }
}); });
this.registerModule("devtools/server/actors/actor-registry", {
prefix: "actorRegistry",
constructor: "ActorRegistryActor",
type: { global: true }
});
} }
this.registerModule("devtools/server/actors/webapps", { this.registerModule("devtools/server/actors/webapps", {
@ -979,6 +984,8 @@ var DebuggerServer = {
/** /**
* Unregisters the handler for the specified tab-scoped request type. * Unregisters the handler for the specified tab-scoped request type.
* This may be used for example by add-ons when shutting down or upgrading. * This may be used for example by add-ons when shutting down or upgrading.
* When unregistering an existing tab actor remove related tab factory
* as well as all existing instances of the actor.
* *
* @param aActor function, object * @param aActor function, object
* In case of function: * In case of function:
@ -992,6 +999,9 @@ var DebuggerServer = {
if ((handler.name && handler.name == aActor.name) || if ((handler.name && handler.name == aActor.name) ||
(handler.id && handler.id == aActor.id)) { (handler.id && handler.id == aActor.id)) {
delete DebuggerServer.tabActorFactories[name]; delete DebuggerServer.tabActorFactories[name];
for (let connID of Object.getOwnPropertyNames(this._connections)) {
this._connections[connID].rootActor.removeActorByName(name);
}
} }
} }
}, },
@ -1033,6 +1043,8 @@ var DebuggerServer = {
/** /**
* Unregisters the handler for the specified browser-scoped request type. * Unregisters the handler for the specified browser-scoped request type.
* This may be used for example by add-ons when shutting down or upgrading. * This may be used for example by add-ons when shutting down or upgrading.
* When unregistering an existing global actor remove related global factory
* as well as all existing instances of the actor.
* *
* @param aActor function, object * @param aActor function, object
* In case of function: * In case of function:
@ -1046,6 +1058,9 @@ var DebuggerServer = {
if ((handler.name && handler.name == aActor.name) || if ((handler.name && handler.name == aActor.name) ||
(handler.id && handler.id == aActor.id)) { (handler.id && handler.id == aActor.id)) {
delete DebuggerServer.globalActorFactories[name]; delete DebuggerServer.globalActorFactories[name];
for (let connID of Object.getOwnPropertyNames(this._connections)) {
this._connections[connID].rootActor.removeActorByName(name);
}
} }
} }
} }

View File

@ -33,6 +33,7 @@ EXTRA_JS_MODULES.devtools.server += [
] ]
EXTRA_JS_MODULES.devtools.server.actors += [ EXTRA_JS_MODULES.devtools.server.actors += [
'actors/actor-registry.js',
'actors/call-watcher.js', 'actors/call-watcher.js',
'actors/canvas.js', 'actors/canvas.js',
'actors/child-process.js', 'actors/child-process.js',

View File

@ -0,0 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const protocol = require("devtools/server/protocol");
const HelloActor = protocol.ActorClass({
typeName: "helloActor",
hello: protocol.method(function () {
return;
}, {
request: {},
response: {}
})
});

View File

@ -9,5 +9,7 @@ exports.register = function(handle) {
} }
exports.unregister = function(handle) { exports.unregister = function(handle) {
handle.removeTabActor(Actor);
handle.removeGlobalActor(Actor);
} }

View File

@ -0,0 +1,80 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check that you can register new actors via the ActorRegistrationActor.
*/
var gClient;
var gRegistryFront;
var gActorFront;
var gOldPref;
const { ActorRegistryFront } = devtools.require("devtools/server/actors/actor-registry");
function run_test()
{
gOldPref = Services.prefs.getBoolPref("devtools.debugger.forbid-certified-apps");
Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", false);
initTestDebuggerServer();
DebuggerServer.addBrowserActors();
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(getRegistry);
do_test_pending();
}
function getRegistry() {
gClient.listTabs((response) => {
gRegistryFront = ActorRegistryFront(gClient, response);
registerNewActor();
});
}
function registerNewActor() {
let options = {
prefix: "helloActor",
constructor: "HelloActor",
type: { global: true }
};
gRegistryFront
.registerActor("resource://test/hello-actor.js", options)
.then(actorFront => gActorFront = actorFront)
.then(talkToNewActor)
.then(null, e => {
DevToolsUtils.reportException("registerNewActor", e)
do_check_true(false);
});
}
function talkToNewActor() {
gClient.listTabs(({ helloActor }) => {
do_check_true(!!helloActor);
gClient.request({
to: helloActor,
type: "hello"
}, response => {
do_check_true(!response.error);
unregisterNewActor();
});
});
}
function unregisterNewActor() {
gActorFront
.unregister()
.then(testActorIsUnregistered)
.then(null, e => {
DevToolsUtils.reportException("registerNewActor", e)
do_check_true(false);
});
}
function testActorIsUnregistered() {
gClient.listTabs(({ helloActor }) => {
do_check_true(!helloActor);
Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", gOldPref);
finishClient(gClient);
});
}

View File

@ -52,8 +52,11 @@ TestTabList.prototype = {
function createRootActor(aConnection) function createRootActor(aConnection)
{ {
let root = new RootActor(aConnection, let root = new RootActor(aConnection, {
{ tabList: new TestTabList(aConnection) }); tabList: new TestTabList(aConnection),
globalActorFactories: DebuggerServer.globalActorFactories,
});
root.applicationType = "xpcshell-tests"; root.applicationType = "xpcshell-tests";
return root; return root;
} }
@ -126,6 +129,14 @@ TestTabActor.prototype = {
return {}; return {};
}, },
removeActorByName: function(aName) {
const actor = this._extraActors[aName];
if (this._tabActorPool) {
this._tabActorPool.removeActor(actor);
}
delete this._extraActors[aName];
},
/* Support for DebuggerServer.addTabActor. */ /* Support for DebuggerServer.addTabActor. */
_createExtraActors: createExtraActors, _createExtraActors: createExtraActors,
_appendExtraActors: appendExtraActors _appendExtraActors: appendExtraActors

View File

@ -16,7 +16,9 @@ support-files =
sourcemapped.js sourcemapped.js
testactors.js testactors.js
tracerlocations.js tracerlocations.js
hello-actor.js
[test_actor-registry-actor.js]
[test_nesting-01.js] [test_nesting-01.js]
[test_nesting-02.js] [test_nesting-02.js]
[test_nesting-03.js] [test_nesting-03.js]