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) {
who += " " + aName;
}
exports.reportException(who, ex);
return exports.reportException(who, ex);
}
}
}
/**
* 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

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,35 +33,44 @@ function RegisteredActorFactory(options, prefix) {
// By default the actor name will also be used for the actorID prefix.
this._prefix = prefix;
if (typeof(options) != "function") {
// Lazy actor definition, where options contains all the information
// required to load the actor lazily.
this._getConstructor = function () {
// Load the module
let mod;
try {
mod = require(options.id);
} catch(e) {
throw new Error("Unable to load actor module '" + options.id + "'.\n" +
e.message + "\n" + e.stack + "\n");
}
// Fetch the actor constructor
let c = mod[options.constructorName];
if (!c) {
throw new Error("Unable to find actor constructor named '" +
options.constructorName + "'. (Is it exported?)");
}
return c;
};
// actors definition registered by actorRegistryActor
if (options.constructorFun) {
this._getConstructor = () => options.constructorFun;
} else {
// Lazy actor definition, where options contains all the information
// required to load the actor lazily.
this._getConstructor = function () {
// Load the module
let mod;
try {
mod = require(options.id);
} catch(e) {
throw new Error("Unable to load actor module '" + options.id + "'.\n" +
e.message + "\n" + e.stack + "\n");
}
// Fetch the actor constructor
let c = mod[options.constructorName];
if (!c) {
throw new Error("Unable to find actor constructor named '" +
options.constructorName + "'. (Is it exported?)");
}
return c;
};
}
// Exposes `name` attribute in order to allow removeXXXActor to match
// the actor by its actor constructor name.
this.name = options.constructorName;
} else {
// Old actor case, where options is a function that is the actor constructor.
this._getConstructor = () => options;
// 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;
// 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.
// (Used by ChromeDebuggerActor)
if (options.prototype.actorPrefix) {
if (options.prototype && options.prototype.actorPrefix) {
this._prefix = options.prototype.actorPrefix;
}
}
@ -79,9 +88,9 @@ exports.RegisteredActorFactory = RegisteredActorFactory;
*
* ObservedActorFactory fakes the following actors attributes:
* 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
* instanciated
* instantiated
* And exposes the following method:
* createActor (function) Instantiate an actor that is going to replace
* this factory in the actor pool.
@ -123,7 +132,7 @@ exports.ObservedActorFactory = ObservedActorFactory;
* |aPool|.
*
* 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.
*
* @param aFactories
@ -158,11 +167,16 @@ exports.createExtraActors = function createExtraActors(aFactories, aPool) {
// Register another factory, but this time specific to this connection.
// It creates a fake actor that looks like an regular actor in the pool,
// 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);
this._extraActors[name] = actor;
}
aPool.addActor(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);
}
}
}
@ -270,7 +284,13 @@ ActorPool.prototype = {
actor.disconnect();
}
this._cleanups = {};
}
},
forEach: function(callback) {
for (let name in this._actors) {
callback(this._actors[name]);
}
},
}
exports.ActorPool = ActorPool;

View File

@ -280,14 +280,12 @@ RootActor.prototype = {
newActorPool.addActor(tabActor);
tabActorList.push(tabActor);
}
/* DebuggerServer.addGlobalActor support: create actors. */
if (!this._globalActorPool) {
this._globalActorPool = new ActorPool(this.conn);
this._createExtraActors(this._parameters.globalActorFactories, 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
* 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.
*
* @param DOMStyleSheet styleSheet
* The style sheet to creat an actor for.
* The style sheet to create an actor for.
* @return StyleSheetActor actor
* The actor for this style sheet.
*
@ -451,7 +449,28 @@ RootActor.prototype = {
this._globalActorPool.addActor(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];
}
}
};
RootActor.prototype.requestTypes = {

View File

@ -856,7 +856,7 @@ TabActor.prototype = {
_appendExtraActors: appendExtraActors,
/**
* Does the actual work of attching to a tab.
* Does the actual work of attaching to a tab.
*/
_attach: function BTA_attach() {
if (this._attached) {
@ -1512,7 +1512,7 @@ TabActor.prototype = {
* is here because the Style Editor and Inspector share style sheet actors.
*
* @param DOMStyleSheet styleSheet
* The style sheet to creat an actor for.
* The style sheet to create an actor for.
* @return StyleSheetActor actor
* The actor for this style sheet.
*
@ -1527,7 +1527,17 @@ TabActor.prototype = {
this._tabPool.addActor(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.
*
* @param aConnection DebuggerServerConnection
* The conection to the client.
* The connection to the client.
* @param aBrowser browser
* The browser instance that contains this tab.
* @param aTabBrowser tabbrowser

View File

@ -324,7 +324,7 @@ var DebuggerServer = {
* - constructor (string):
* the name of the exported symbol to be used as the actor
* constructor.
* - type (a dictionnary of booleans with following attribute names):
* - type (a dictionary of booleans with following attribute names):
* - "global"
* registers a global actor instance, if true.
* 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.");
}
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 mod = {
@ -433,6 +433,11 @@ var DebuggerServer = {
constructor: "PreferenceActor",
type: { global: true }
});
this.registerModule("devtools/server/actors/actor-registry", {
prefix: "actorRegistry",
constructor: "ActorRegistryActor",
type: { global: true }
});
}
this.registerModule("devtools/server/actors/webapps", {
@ -979,6 +984,8 @@ var DebuggerServer = {
/**
* Unregisters the handler for the specified tab-scoped request type.
* 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
* In case of function:
@ -992,6 +999,9 @@ var DebuggerServer = {
if ((handler.name && handler.name == aActor.name) ||
(handler.id && handler.id == aActor.id)) {
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.
* 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
* In case of function:
@ -1046,6 +1058,9 @@ var DebuggerServer = {
if ((handler.name && handler.name == aActor.name) ||
(handler.id && handler.id == aActor.id)) {
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 += [
'actors/actor-registry.js',
'actors/call-watcher.js',
'actors/canvas.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) {
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)
{
let root = new RootActor(aConnection,
{ tabList: new TestTabList(aConnection) });
let root = new RootActor(aConnection, {
tabList: new TestTabList(aConnection),
globalActorFactories: DebuggerServer.globalActorFactories,
});
root.applicationType = "xpcshell-tests";
return root;
}
@ -126,6 +129,14 @@ TestTabActor.prototype = {
return {};
},
removeActorByName: function(aName) {
const actor = this._extraActors[aName];
if (this._tabActorPool) {
this._tabActorPool.removeActor(actor);
}
delete this._extraActors[aName];
},
/* Support for DebuggerServer.addTabActor. */
_createExtraActors: createExtraActors,
_appendExtraActors: appendExtraActors

View File

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