Bug 753401 - The debugger server root and tab actors should be easily extensible; r=rcampbell

--HG--
rename : browser/devtools/debugger/test/browser_dbg_contextactor-02.js => browser/devtools/debugger/test/browser_dbg_globalactor-01.js
This commit is contained in:
Panos Astithas 2012-09-20 09:36:32 +03:00
parent e092c900f4
commit 6658636165
12 changed files with 311 additions and 186 deletions

View File

@ -469,7 +469,6 @@ ChromeDebuggerProcess.prototype = {
DebuggerServer.init(this._allowConnection);
DebuggerServer.addBrowserActors();
}
DebuggerServer.closeListener();
DebuggerServer.openListener(DebuggerPreferences.remotePort);
},
@ -493,7 +492,7 @@ ChromeDebuggerProcess.prototype = {
return true;
}
if (result == 2) {
DebuggerServer.closeListener();
DebuggerServer.closeListener(true);
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
}
return false;

View File

@ -20,8 +20,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_dbg_listtabs.js \
browser_dbg_tabactor-01.js \
browser_dbg_tabactor-02.js \
browser_dbg_contextactor-01.js \
browser_dbg_contextactor-02.js \
browser_dbg_globalactor-01.js \
testactors.js \
browser_dbg_nav-01.js \
browser_dbg_propertyview-01.js \

View File

@ -1,49 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check extension-added context actor lifetimes.
*/
var gTab1 = null;
var gTab1Actor = null;
var gClient = null;
function test()
{
DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
let transport = DebuggerServer.connectPipe();
gClient = new DebuggerClient(transport);
gClient.connect(function(aType, aTraits) {
is(aType, "browser", "Root actor should identify itself as a browser.");
get_tab();
});
}
function get_tab()
{
gTab1 = addTab(TAB1_URL, function() {
attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
gTab1Actor = aGrip.actor;
gClient.request({ to: aGrip.actor, type: "testContextActor1" }, function(aResponse) {
ok(aResponse.actor, "testContextActor1 request should return an actor.");
ok(aResponse.actor.indexOf("testone") >= 0,
"testContextActor's actorPrefix should be used.");
gClient.request({ to: aResponse.actor, type: "ping" }, function(aResponse) {
is(aResponse.pong, "pong", "Actor should response to requests.");
finish_test();
});
});
});
});
}
function finish_test()
{
gClient.close(function() {
removeTab(gTab1);
finish();
});
};

View File

@ -1,59 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check extension-added context actor lifetimes.
*/
var gTab1 = null;
var gTab1Actor = null;
var gClient = null;
function test()
{
DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
let transport = DebuggerServer.connectPipe();
gClient = new DebuggerClient(transport);
gClient.connect(function(aType, aTraits) {
is(aType, "browser", "Root actor should identify itself as a browser.");
get_tab();
});
}
function get_tab()
{
gTab1 = addTab(TAB1_URL, function() {
get_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
gTab1Actor = aGrip.actor;
gClient.request({ to: gTab1Actor, type: "attach" }, function(aResponse) {
gClient.request({ to: gTab1Actor, type: "testContextActor1" }, function(aResponse) {
navigate_tab(aResponse.actor);
});
});
});
});
}
function navigate_tab(aTestActor)
{
gClient.addOneTimeListener("tabNavigated", function(aEvent, aResponse) {
gClient.request({ to: aTestActor, type: "ping" }, function(aResponse) {
// TODO: Currently the client is supposed to clean up after tabNavigated
// events. We should remove this check, or even better, remove the whole
// test.
todo(aResponse.error, "noSuchActor", "testContextActor1 should have gone away with the navigation.");
finish_test();
});
});
gTab1.linkedBrowser.loadURI(TAB2_URL);
}
function finish_test()
{
gClient.close(function() {
removeTab(gTab1);
finish();
});
}

View File

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check extension-added global actor API.
*/
var gClient = null;
function test()
{
DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
let transport = DebuggerServer.connectPipe();
gClient = new DebuggerClient(transport);
gClient.connect(function(aType, aTraits) {
is(aType, "browser", "Root actor should identify itself as a browser.");
gClient.listTabs(function(aResponse) {
let globalActor = aResponse.testGlobalActor1;
ok(globalActor, "Found the test tab actor.")
ok(globalActor.indexOf("testone") >= 0,
"testTabActor's actorPrefix should be used.");
gClient.request({ to: globalActor, type: "ping" }, function(aResponse) {
is(aResponse.pong, "pong", "Actor should respond to requests.");
finish_test();
});
});
});
}
function finish_test()
{
gClient.close(function() {
finish();
});
}

View File

@ -24,17 +24,15 @@ function test()
function get_tab()
{
gTab1 = addTab(TAB1_URL, function () {
attach_tab_actor_for_url(gClient, TAB1_URL, function (aGrip) {
gTab1 = addTab(TAB1_URL, function() {
attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
gTab1Actor = aGrip.actor;
gClient.request({ to: aGrip.actor, type: "testTabActor1" }, function (aResponse) {
ok(aResponse.actor, "testTabActor1 request should return an actor.");
ok(aResponse.actor.indexOf("testone") >= 0,
"testTabActor's actorPrefix should be used.");
gClient.request({ to: aResponse.actor, type: "ping" }, function (aResponse) {
is(aResponse.pong, "pong", "Actor should response to requests.");
finish_test();
});
ok(aGrip.testTabActor1, "Found the test tab actor.")
ok(aGrip.testTabActor1.indexOf("testone") >= 0,
"testTabActor's actorPrefix should be used.");
gClient.request({ to: aGrip.testTabActor1, type: "ping" }, function(aResponse) {
is(aResponse.pong, "pong", "Actor should respond to requests.");
finish_test();
});
});
});

View File

@ -24,10 +24,14 @@ function test()
function get_tab()
{
gTab1 = addTab(TAB1_URL, function () {
attach_tab_actor_for_url(gClient, TAB1_URL, function (aGrip) {
gTab1 = addTab(TAB1_URL, function() {
attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
gTab1Actor = aGrip.actor;
gClient.request({ to: aGrip.actor, type: "testTabActor1" }, function (aResponse) {
ok(aGrip.testTabActor1, "Found the test tab actor.")
ok(aGrip.testTabActor1.indexOf("testone") >= 0,
"testTabActor's actorPrefix should be used.");
gClient.request({ to: aGrip.testTabActor1, type: "ping" }, function(aResponse) {
is(aResponse.pong, "pong", "Actor should respond to requests.");
close_tab(aResponse.actor);
});
});
@ -37,10 +41,16 @@ function get_tab()
function close_tab(aTestActor)
{
removeTab(gTab1);
gClient.request({ to: aTestActor, type: "ping" }, function (aResponse) {
is(aResponse.error, "noSuchActor", "testTabActor1 should have gone away with the tab.");
try {
gClient.request({ to: aTestActor, type: "ping" }, function (aResponse) {
is(aResponse, undefined, "testTabActor1 didn't go away with the tab.");
finish_test();
});
} catch (e) {
is(e.message, "'ping' request packet has no destination.",
"testTabActor1 should have gone away with the tab.");
finish_test();
});
}
}
function finish_test()

View File

@ -24,6 +24,9 @@ let gEnableRemote = Services.prefs.getBoolPref("devtools.debugger.remote-enabled
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
registerCleanupFunction(function() {
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", gEnableRemote);
// Properly shut down the server to avoid memory leaks.
DebuggerServer.destroy();
});
if (!DebuggerServer.initialized) {

View File

@ -1,20 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function TestActor1(aConnection, aTab, aOnDisconnect)
function TestActor1(aConnection, aTab)
{
this.conn = aConnection;
this.tab = aTab;
this.onDisconnect = aOnDisconnect;
}
TestActor1.prototype = {
actorPrefix: "testone",
disconnect: function TA1_disconnect() {
this.onDisconnect();
},
grip: function TA1_grip() {
return { actor: this.actorID,
test: "TestActor1" };
@ -29,30 +24,8 @@ TestActor1.prototype.requestTypes = {
"ping": TestActor1.prototype.onPing
};
DebuggerServer.addTabRequest("testTabActor1", function (aTab) {
if (aTab._testTabActor1) {
return aTab._testTabActor1.grip();
}
let actor = new TestActor1(aTab.conn, aTab.browser, function () {
delete aTab._testTabActor1;
});
aTab.tabActorPool.addActor(actor);
aTab._testTabActor1 = actor;
return actor.grip();
});
DebuggerServer.addTabRequest("testContextActor1", function (aTab, aRequest) {
if (aTab._testContextActor1) {
return aTab._testContextActor1.grip();
}
let actor = new TestActor1(aTab.conn, aTab.browser, function () {
delete aTab._testContextActor1;
});
aTab.contextActorPool.addActor(actor);
aTab._testContextActor1 = actor;
return actor.grip();
});
DebuggerServer.removeTabActor(TestActor1);
DebuggerServer.removeGlobalActor(TestActor1);
DebuggerServer.addTabActor(TestActor1, "testTabActor1");
DebuggerServer.addGlobalActor(TestActor1, "testGlobalActor1");

View File

@ -31,20 +31,24 @@ function BrowserRootActor(aConnection)
this.conn = aConnection;
this._tabActors = new WeakMap();
this._tabActorPool = null;
this._actorFactories = null;
// A map of actor names to actor instances provided by extensions.
this._extraActors = {};
this.onTabClosed = this.onTabClosed.bind(this);
windowMediator.addListener(this);
}
BrowserRootActor.prototype = {
/**
* Return a 'hello' packet as specified by the Remote Debugging Protocol.
*/
sayHello: function BRA_sayHello() {
return { from: "root",
applicationType: "browser",
traits: [] };
return {
from: "root",
applicationType: "browser",
traits: {}
};
},
/**
@ -52,6 +56,7 @@ BrowserRootActor.prototype = {
*/
disconnect: function BRA_disconnect() {
windowMediator.removeListener(this);
this._extraActors = null;
// We may have registered event listeners on browser windows to
// watch for tab closes, remove those.
@ -77,7 +82,7 @@ BrowserRootActor.prototype = {
// an ActorPool.
let actorPool = new ActorPool(this.conn);
let actorList = [];
let tabActorList = [];
// Walk over open browser windows.
let e = windowMediator.getEnumerator("navigator:browser");
@ -95,7 +100,7 @@ BrowserRootActor.prototype = {
let browsers = win.getBrowser().browsers;
for each (let browser in browsers) {
if (browser == selectedBrowser && win == top) {
selected = actorList.length;
selected = tabActorList.length;
}
let actor = this._tabActors.get(browser);
if (!actor) {
@ -104,10 +109,22 @@ BrowserRootActor.prototype = {
this._tabActors.set(browser, actor);
}
actorPool.addActor(actor);
actorList.push(actor);
tabActorList.push(actor);
}
}
// Walk over global actors added by extensions.
for (let name in DebuggerServer.globalActorFactories) {
let actor = this._extraActors[name];
if (!actor) {
actor = DebuggerServer.globalActorFactories[name].bind(null, this.conn);
actor.prototype = DebuggerServer.globalActorFactories[name].prototype;
actor.parentID = this.actorID;
this._extraActors[name] = actor;
}
actorPool.addActor(actor);
}
// Now drop the old actorID -> actor map. Actors that still
// mattered were added to the new map, others will go
// away.
@ -117,10 +134,16 @@ BrowserRootActor.prototype = {
this._tabActorPool = actorPool;
this.conn.addActorPool(this._tabActorPool);
return { "from": "root",
"selected": selected,
"tabs": [actor.grip()
for each (actor in actorList)] };
let response = {
"from": "root",
"selected": selected,
"tabs": [actor.grip() for (actor of tabActorList)]
};
for (let name in this._extraActors) {
let actor = this._extraActors[name];
response[name] = actor.actorID;
}
return response;
},
/**
@ -203,6 +226,9 @@ function BrowserTabActor(aConnection, aBrowser, aTabBrowser)
this.conn = aConnection;
this._browser = aBrowser;
this._tabbrowser = aTabBrowser;
this._tabActorPool = null;
// A map of actor names to actor instances provided by extensions.
this._extraActors = {};
this._onWindowCreated = this.onWindowCreated.bind(this);
}
@ -245,6 +271,7 @@ BrowserTabActor.prototype = {
this.conn.removeActor(aActor);
},
// A constant prefix that will be used to form the actor ID by the server.
actorPrefix: "tab",
grip: function BTA_grip() {
@ -252,9 +279,35 @@ BrowserTabActor.prototype = {
"grip() shouldn't be called on exited browser actor.");
dbg_assert(this.actorID,
"tab should have an actorID.");
return { actor: this.actorID,
title: this.browser.contentTitle,
url: this.browser.currentURI.spec }
let response = {
actor: this.actorID,
title: this.browser.contentTitle,
url: this.browser.currentURI.spec
};
// Walk over tab actors added by extensions and add them to a new ActorPool.
let actorPool = new ActorPool(this.conn);
for (let name in DebuggerServer.tabActorFactories) {
let actor = this._extraActors[name];
if (!actor) {
actor = DebuggerServer.tabActorFactories[name].bind(null, this.conn);
actor.prototype = DebuggerServer.tabActorFactories[name].prototype;
actor.parentID = this.actorID;
this._extraActors[name] = actor;
}
actorPool.addActor(actor);
}
if (!actorPool.isEmpty()) {
this._tabActorPool = actorPool;
this.conn.addActorPool(this._tabActorPool);
}
for (let name in this._extraActors) {
let actor = this._extraActors[name];
response[name] = actor.actorID;
}
return response;
},
/**
@ -266,6 +319,7 @@ BrowserTabActor.prototype = {
if (this._progressListener) {
this._progressListener.destroy();
}
this._extraActors = null;
},
/**
@ -370,6 +424,10 @@ BrowserTabActor.prototype = {
// Shut down actors that belong to this tab's pool.
this.conn.removeActorPool(this._tabPool);
this._tabPool = null;
if (this._tabActorPool) {
this.conn.removeActorPool(this._tabActorPool);
this._tabActorPool = null;
}
this._attached = false;
},
@ -535,9 +593,14 @@ DebuggerProgressListener.prototype = {
}
};
// DebuggerServer extension API.
/**
* Registers handlers for new request types defined dynamically. This is used
* for example by add-ons to augment the functionality of the tab actor.
* Registers handlers for new tab-scoped request types defined dynamically.
* This is used for example by add-ons to augment the functionality of the tab
* actor.
* TODO: remove this API in the next release after bug 753401 lands, once all
* our experimental add-ons have been converted to the new API.
*
* @param aName string
* The name of the new request type.
@ -552,3 +615,79 @@ DebuggerServer.addTabRequest = function DS_addTabRequest(aName, aFunction) {
return aFunction(this, aRequest);
}
};
/**
* Registers handlers for new tab-scoped request types defined dynamically.
* This is used for example by add-ons to augment the functionality of the tab
* actor.
*
* @param aFunction function
* The constructor function for this request type.
* @param aName string [optional]
* The name of the new request type. If this is not present, the
* actorPrefix property of the constructor prototype is used.
*/
DebuggerServer.addTabActor = function DS_addTabActor(aFunction, aName) {
let name = aName ? aName : aFunction.prototype.actorPrefix;
if (["title", "url", "actor"].indexOf(name) != -1) {
throw Error(name + " is not allowed");
}
if (DebuggerServer.tabActorFactories.hasOwnProperty(name)) {
throw Error(name + " already exists");
}
DebuggerServer.tabActorFactories[name] = aFunction;
};
/**
* Unregisters the handler for the specified tab-scoped request type.
* This may be used for example by add-ons when shutting down or upgrading.
*
* @param aFunction function
* The constructor function for this request type.
*/
DebuggerServer.removeTabActor = function DS_removeTabActor(aFunction) {
for (let name in DebuggerServer.tabActorFactories) {
let handler = DebuggerServer.tabActorFactories[name];
if (handler.name == aFunction.name) {
delete DebuggerServer.tabActorFactories[name];
}
}
};
/**
* Registers handlers for new browser-scoped request types defined dynamically.
* This is used for example by add-ons to augment the functionality of the root
* actor.
*
* @param aFunction function
* The constructor function for this request type.
* @param aName string [optional]
* The name of the new request type. If this is not present, the
* actorPrefix property of the constructor prototype is used.
*/
DebuggerServer.addGlobalActor = function DS_addGlobalActor(aFunction, aName) {
let name = aName ? aName : aFunction.prototype.actorPrefix;
if (["from", "tabs", "selected"].indexOf(name) != -1) {
throw Error(name + " is not allowed");
}
if (DebuggerServer.globalActorFactories.hasOwnProperty(name)) {
throw Error(name + " already exists");
}
DebuggerServer.globalActorFactories[name] = aFunction;
};
/**
* Unregisters the handler for the specified browser-scoped request type.
* This may be used for example by add-ons when shutting down or upgrading.
*
* @param aFunction function
* The constructor function for this request type.
*/
DebuggerServer.removeGlobalActor = function DS_removeGlobalActor(aFunction) {
for (let name in DebuggerServer.globalActorFactories) {
let handler = DebuggerServer.globalActorFactories[name];
if (handler.name == aFunction.name) {
delete DebuggerServer.globalActorFactories[name];
}
}
};

View File

@ -60,6 +60,12 @@ var DebuggerServer = {
_transportInitialized: false,
xpcInspector: null,
_allowConnection: null,
// Number of currently open TCP connections.
_socketConnections: 0,
// Map of global actor names to actor constructors provided by extensions.
globalActorFactories: null,
// Map of tab actor names to actor constructors provided by extensions.
tabActorFactories: null,
LONG_STRING_LENGTH: 10000,
LONG_STRING_INITIAL_LENGTH: 1000,
@ -79,6 +85,9 @@ var DebuggerServer = {
this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
this.initTransport(aAllowConnectionCallback);
this.addActors("chrome://global/content/devtools/dbg-script-actors.js");
this.globalActorFactories = {};
this.tabActorFactories = {};
},
/**
@ -100,7 +109,22 @@ var DebuggerServer = {
this._allowConnection = aAllowConnectionCallback;
},
get initialized() { return !!this.xpcInspector; },
get initialized() { return !!this.globalActorFactories; },
/**
* Performs cleanup tasks before shutting down the debugger server, if no
* connections are currently open. Such tasks include clearing any actor
* constructors added at runtime. This method should be called whenever a
* debugger server is no longer useful, to avoid memory leaks. After this
* method returns, the debugger server must be initialized again before use.
*/
destroy: function DH_destroy() {
if (Object.keys(this._connections).length == 0) {
dumpn("Shutting down debugger server.");
delete this.globalActorFactories;
delete this.tabActorFactories;
}
},
/**
* Load a subscript into the debugging global.
@ -133,8 +157,9 @@ var DebuggerServer = {
}
this._checkInit();
// Return early if the server is already listening.
if (this._listener) {
throw "Debugging listener already open.";
return true;
}
let localOnly = false;
@ -151,22 +176,32 @@ var DebuggerServer = {
dumpn("Could not start debugging listener on port " + aPort + ": " + e);
throw Cr.NS_ERROR_NOT_AVAILABLE;
}
this._socketConnections++;
return true;
},
/**
* Close a previously-opened TCP listener.
*
* @param aForce boolean [optional]
* If set to true, then the socket will be closed, regardless of the
* number of open connections.
*/
closeListener: function DH_closeListener() {
closeListener: function DH_closeListener(aForce) {
this._checkInit();
if (!this._listener) {
if (!this._listener || this._socketConnections == 0) {
return false;
}
this._listener.close();
this._listener = null;
// Only close the listener when the last connection is closed, or if the
// aForce flag is passed.
if (--this._socketConnections == 0 || aForce) {
this._listener.close();
this._listener = null;
this._socketConnections = 0;
}
return true;
},
@ -244,10 +279,12 @@ var DebuggerServer = {
},
/**
* Remove the connection from the debugging server.
* Remove the connection from the debugging server and shut down the server
* if no other connections are open.
*/
_connectionClosed: function DH_connectionClosed(aConnection) {
delete this._connections[aConnection.prefix];
this.destroy();
}
};
@ -278,7 +315,11 @@ ActorPool.prototype = {
addActor: function AP_addActor(aActor) {
aActor.conn = this.conn;
if (!aActor.actorID) {
aActor.actorID = this.conn.allocID(aActor.actorPrefix || undefined);
let prefix = aActor.actorPrefix;
if (typeof aActor == "function") {
prefix = aActor.prototype.actorPrefix;
}
aActor.actorID = this.conn.allocID(prefix || undefined);
}
if (aActor.registeredPool) {
@ -300,6 +341,13 @@ ActorPool.prototype = {
return aActorID in this._actors;
},
/**
* Returns true if the pool is empty.
*/
isEmpty: function AP_isEmpty() {
return Object.keys(this._actors).length == 0;
},
/**
* Remove an actor from the actor pool.
*/
@ -437,6 +485,24 @@ DebuggerServerConnection.prototype = {
return;
}
// Dyamically-loaded actors have to be created lazily.
if (typeof actor == "function") {
let instance;
try {
instance = new actor();
} catch (e) {
Cu.reportError(e);
this.transport.send({
error: "unknownError",
message: ("error occurred while creating actor '" + actor.name +
"': " + safeErrorString(e))
});
}
actor.registeredPool.addActor(instance);
actor.registeredPool.removeActor(actor);
actor = instance;
}
var ret = null;
// Dispatch the request to the actor.

View File

@ -27,7 +27,12 @@ function really_long() {
function test_socket_conn()
{
DebuggerServer.openListener(2929);
do_check_eq(DebuggerServer._socketConnections, 0);
do_check_true(DebuggerServer.openListener(2929));
do_check_eq(DebuggerServer._socketConnections, 1);
// Make sure opening the listener twice does nothing.
do_check_true(DebuggerServer.openListener(2929));
do_check_eq(DebuggerServer._socketConnections, 1);
let unicodeString = "(╯°□°)╯︵ ┻━┻";
let transport = debuggerSocketConnect("127.0.0.1", 2929);
@ -54,7 +59,12 @@ function test_socket_conn()
function test_socket_shutdown()
{
DebuggerServer.closeListener();
do_check_eq(DebuggerServer._socketConnections, 1);
do_check_true(DebuggerServer.closeListener());
do_check_eq(DebuggerServer._socketConnections, 0);
// Make sure closing the listener twice does nothing.
do_check_false(DebuggerServer.closeListener());
do_check_eq(DebuggerServer._socketConnections, 0);
let transport = debuggerSocketConnect("127.0.0.1", 2929);
transport.hooks = {