Bug 1145049 - Prevent caching tab actors in child processes. r=jryans

This commit is contained in:
Alexandre Poirot 2015-04-18 09:39:07 +02:00
parent 1461bbcedd
commit 40e37fad19
7 changed files with 126 additions and 133 deletions

View File

@ -21,10 +21,14 @@ let { TabActor } = require("devtools/server/actors/webbrowser");
* The conection to the client. * The conection to the client.
* @param chromeGlobal * @param chromeGlobal
* The content script global holding |content| and |docShell| properties for a tab. * The content script global holding |content| and |docShell| properties for a tab.
* @param prefix
* the prefix used in protocol to create IDs for each actor.
* Used as ID identifying this particular TabActor from the parent process.
*/ */
function ContentActor(connection, chromeGlobal) function ContentActor(connection, chromeGlobal, prefix)
{ {
this._chromeGlobal = chromeGlobal; this._chromeGlobal = chromeGlobal;
this._prefix = prefix;
TabActor.call(this, connection, chromeGlobal); TabActor.call(this, connection, chromeGlobal);
this.traits.reconfigure = false; this.traits.reconfigure = false;
this._sendForm = this._sendForm.bind(this); this._sendForm = this._sendForm.bind(this);
@ -49,32 +53,11 @@ Object.defineProperty(ContentActor.prototype, "title", {
}); });
ContentActor.prototype.exit = function() { ContentActor.prototype.exit = function() {
this._chromeGlobal.removeMessageListener("debug:form", this._sendForm); if (this._sendForm) {
this._sendForm = null; this._chromeGlobal.removeMessageListener("debug:form", this._sendForm);
TabActor.prototype.exit.call(this); this._sendForm = null;
};
// Override form just to rename this._tabActorPool to this._tabActorPool2
// in order to prevent it to be cleaned on detach.
// We have to keep tab actors alive as we keep the ContentActor
// alive after detach and reuse it for multiple debug sessions.
ContentActor.prototype.form = function () {
let response = {
"actor": this.actorID,
"title": this.title,
"url": this.url
};
// Walk over tab actors added by extensions and add them to a new ActorPool.
let actorPool = new ActorPool(this.conn);
this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
if (!actorPool.isEmpty()) {
this._tabActorPool2 = actorPool;
this.conn.addActorPool(this._tabActorPool2);
} }
return TabActor.prototype.exit.call(this);
this._appendExtraActors(response);
return response;
}; };
/** /**

View File

@ -111,7 +111,7 @@ const DirectorRegistry = exports.DirectorRegistry = {
let gTrackedMessageManager = new Set(); let gTrackedMessageManager = new Set();
exports.setupParentProcess = function setupParentProcess({mm, childID}) { exports.setupParentProcess = function setupParentProcess({mm, prefix}) {
// prevents multiple subscriptions on the same messagemanager // prevents multiple subscriptions on the same messagemanager
if (gTrackedMessageManager.has(mm)) { if (gTrackedMessageManager.has(mm)) {
return; return;
@ -121,7 +121,7 @@ exports.setupParentProcess = function setupParentProcess({mm, childID}) {
// listen for director-script requests from the child process // listen for director-script requests from the child process
mm.addMessageListener("debug:director-registry-request", handleChildRequest); mm.addMessageListener("debug:director-registry-request", handleChildRequest);
DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected); DebuggerServer.once("disconnected-from-child:" + prefix, handleMessageManagerDisconnected);
/* parent process helpers */ /* parent process helpers */

View File

@ -213,9 +213,9 @@ function WebappsActor(aConnection) {
Cu.import("resource://gre/modules/AppsUtils.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm");
// Keep reference of already created app actors. // Keep reference of already connected app processes.
// key: app frame message manager, value: ContentActor's grip() value // values: app frame message manager
this._appActorsMap = new Map(); this._connectedApps = new Set();
this.conn = aConnection; this.conn = aConnection;
this._uploads = []; this._uploads = [];
@ -960,24 +960,33 @@ WebappsActor.prototype = {
// Only create a new actor, if we haven't already // Only create a new actor, if we haven't already
// instanciated one for this connection. // instanciated one for this connection.
let map = this._appActorsMap; let set = this._connectedApps;
let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner) let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner)
.frameLoader .frameLoader
.messageManager; .messageManager;
let actor = map.get(mm); if (!set.has(mm)) {
if (!actor) {
let onConnect = actor => { let onConnect = actor => {
map.set(mm, actor); set.add(mm);
return { actor: actor }; return { actor: actor };
}; };
let onDisconnect = mm => { let onDisconnect = mm => {
map.delete(mm); set.delete(mm);
}; };
return DebuggerServer.connectToChild(this.conn, appFrame, onDisconnect) return DebuggerServer.connectToChild(this.conn, appFrame, onDisconnect)
.then(onConnect); .then(onConnect);
} }
return { actor: actor }; // We have to update the form as it may have changed
// if we detached the TabActor
let deferred = promise.defer();
let onFormUpdate = msg => {
mm.removeMessageListener("debug:form", onFormUpdate);
deferred.resolve({ actor: msg.json });
};
mm.addMessageListener("debug:form", onFormUpdate);
mm.sendAsyncMessage("debug:form");
return deferred.promise;
}, },
watchApps: function () { watchApps: function () {

View File

@ -876,10 +876,7 @@ TabActor.prototype = {
* Called when the actor is removed from the connection. * Called when the actor is removed from the connection.
*/ */
disconnect: function BTA_disconnect() { disconnect: function BTA_disconnect() {
this._detach(); this.exit();
this._extraActors = null;
this._styleSheetActors.clear();
this._exited = true;
}, },
/** /**
@ -901,6 +898,14 @@ TabActor.prototype = {
type: "tabDetached" }); type: "tabDetached" });
} }
Object.defineProperty(this, "docShell", {
value: null,
configurable: true
});
this._extraActors = null;
this._styleSheetActors.clear();
this._exited = true; this._exited = true;
}, },
@ -1221,11 +1226,6 @@ TabActor.prototype = {
this._tabActorPool = null; this._tabActorPool = null;
} }
Object.defineProperty(this, "docShell", {
value: null,
configurable: true
});
this._attached = false; this._attached = false;
return true; return true;
}, },
@ -1822,7 +1822,10 @@ function RemoteBrowserTabActor(aConnection, aBrowser)
RemoteBrowserTabActor.prototype = { RemoteBrowserTabActor.prototype = {
connect: function() { connect: function() {
let connect = DebuggerServer.connectToChild(this._conn, this._browser); let onDestroy = () => {
this._form = null;
};
let connect = DebuggerServer.connectToChild(this._conn, this._browser, onDestroy);
return connect.then(form => { return connect.then(form => {
this._form = form; this._form = form;
return this; return this;
@ -1835,15 +1838,21 @@ RemoteBrowserTabActor.prototype = {
}, },
update: function() { update: function() {
let deferred = promise.defer(); // If the child happens to be crashed/close/detach, it won't have _form set,
let onFormUpdate = msg => { // so only request form update if some code is still listening on the other side.
this._mm.removeMessageListener("debug:form", onFormUpdate); if (this._form) {
this._form = msg.json; let deferred = promise.defer();
deferred.resolve(this); let onFormUpdate = msg => {
}; this._mm.removeMessageListener("debug:form", onFormUpdate);
this._mm.addMessageListener("debug:form", onFormUpdate); this._form = msg.json;
this._mm.sendAsyncMessage("debug:form"); deferred.resolve(this);
return deferred.promise; };
this._mm.addMessageListener("debug:form", onFormUpdate);
this._mm.sendAsyncMessage("debug:form");
return deferred.promise;
} else {
return this.connect();
}
}, },
form: function() { form: function() {

View File

@ -17,10 +17,6 @@ let chromeGlobal = this;
const { dumpn } = DevToolsUtils; const { dumpn } = DevToolsUtils;
const { DebuggerServer, ActorPool } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); const { DebuggerServer, ActorPool } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
if (!DebuggerServer.childID) {
DebuggerServer.childID = 1;
}
if (!DebuggerServer.initialized) { if (!DebuggerServer.initialized) {
DebuggerServer.init(); DebuggerServer.init();
@ -44,17 +40,16 @@ let chromeGlobal = this;
let mm = msg.target; let mm = msg.target;
let prefix = msg.data.prefix; let prefix = msg.data.prefix;
let id = DebuggerServer.childID++;
let conn = DebuggerServer.connectToParent(prefix, mm); let conn = DebuggerServer.connectToParent(prefix, mm);
connections.set(id, conn); connections.set(prefix, conn);
let actor = new DebuggerServer.ContentActor(conn, chromeGlobal); let actor = new DebuggerServer.ContentActor(conn, chromeGlobal, prefix);
let actorPool = new ActorPool(conn); let actorPool = new ActorPool(conn);
actorPool.addActor(actor); actorPool.addActor(actor);
conn.addActorPool(actorPool); conn.addActorPool(actorPool);
sendAsyncMessage("debug:actor", {actor: actor.form(), childID: id}); sendAsyncMessage("debug:actor", {actor: actor.form(), prefix: prefix});
}); });
addMessageListener("debug:connect", onConnect); addMessageListener("debug:connect", onConnect);
@ -95,11 +90,11 @@ let chromeGlobal = this;
// Call DebuggerServerConnection.close to destroy all child actors // Call DebuggerServerConnection.close to destroy all child actors
// (It should end up calling DebuggerServerConnection.onClosed // (It should end up calling DebuggerServerConnection.onClosed
// that would actually cleanup all actor pools) // that would actually cleanup all actor pools)
let childID = msg.data.childID; let prefix = msg.data.prefix;
let conn = connections.get(childID); let conn = connections.get(prefix);
if (conn) { if (conn) {
conn.close(); conn.close();
connections.delete(childID); connections.delete(prefix);
} }
}); });
addMessageListener("debug:disconnect", onDisconnect); addMessageListener("debug:disconnect", onDisconnect);

View File

@ -76,7 +76,7 @@ connected to the child process as parameter, e.g. in the **director-registry**:
let gTrackedMessageManager = new Set(); let gTrackedMessageManager = new Set();
exports.setupParentProcess = function setupParentProcess({ mm, childID }) { exports.setupParentProcess = function setupParentProcess({ mm, prefix }) {
if (gTrackedMessageManager.has(mm)) { return; } if (gTrackedMessageManager.has(mm)) { return; }
gTrackedMessageManager.add(mm); gTrackedMessageManager.add(mm);
@ -84,7 +84,7 @@ exports.setupParentProcess = function setupParentProcess({ mm, childID }) {
mm.addMessageListener("debug:director-registry-request", handleChildRequest); mm.addMessageListener("debug:director-registry-request", handleChildRequest);
// time to unsubscribe from the disconnected message manager // time to unsubscribe from the disconnected message manager
DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected); DebuggerServer.once("disconnected-from-child:" + prefix, handleMessageManagerDisconnected);
function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) { function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) {
... ...
@ -109,8 +109,8 @@ In the child process:
In the parent process: In the parent process:
- The DebuggerServer receives the DebuggerServer.setupInParent request - The DebuggerServer receives the DebuggerServer.setupInParent request
- it tries to load the required module - it tries to load the required module
- it tries to call the **mod[setupParent]** method with the frame message manager and the childID - it tries to call the **mod[setupParent]** method with the frame message manager and the prefix
in the json parameter **{ mm, childID }** in the json parameter **{ mm, prefix }**
- the module setupParent helper use the mm to subscribe the messagemanager events - the module setupParent helper use the mm to subscribe the messagemanager events
- the module setupParent helper use the DebuggerServer object to subscribe *once* the - the module setupParent helper use the DebuggerServer object to subscribe *once* the
**"disconnected-from-child:CHILDID"** event (needed to unsubscribe the messagemanager events) **"disconnected-from-child:PREFIX"** event (needed to unsubscribe the messagemanager events)

View File

@ -809,13 +809,15 @@ var DebuggerServer = {
* The debugger server connection to use. * The debugger server connection to use.
* @param nsIDOMElement aFrame * @param nsIDOMElement aFrame
* The browser element that holds the child process. * The browser element that holds the child process.
* @param function [aOnDisconnect] * @param function [aOnDestroy]
* Optional function to invoke when the child is disconnected. * Optional function to invoke when the child process closes
* or the connection shuts down. (Need to forget about the
* related TabActor)
* @return object * @return object
* A promise object that is resolved once the connection is * A promise object that is resolved once the connection is
* established. * established.
*/ */
connectToChild: function(aConnection, aFrame, aOnDisconnect) { connectToChild: function(aConnection, aFrame, aOnDestroy) {
let deferred = defer(); let deferred = defer();
let mm = aFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader let mm = aFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader
@ -824,7 +826,6 @@ var DebuggerServer = {
let actor, childTransport; let actor, childTransport;
let prefix = aConnection.allocID("child"); let prefix = aConnection.allocID("child");
let childID = null;
let netMonitor = null; let netMonitor = null;
// provides hook to actor modules that need to exchange messages // provides hook to actor modules that need to exchange messages
@ -841,7 +842,7 @@ var DebuggerServer = {
return false; return false;
} }
m[setupParent]({ mm: mm, childID: childID }); m[setupParent]({ mm: mm, prefix: prefix });
return true; return true;
} catch(e) { } catch(e) {
@ -855,10 +856,11 @@ var DebuggerServer = {
mm.addMessageListener("debug:setup-in-parent", onSetupInParent); mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
let onActorCreated = DevToolsUtils.makeInfallible(function (msg) { let onActorCreated = DevToolsUtils.makeInfallible(function (msg) {
if (msg.json.prefix != prefix) {
return;
}
mm.removeMessageListener("debug:actor", onActorCreated); mm.removeMessageListener("debug:actor", onActorCreated);
childID = msg.json.childID;
// Pipe Debugger message from/to parent/child via the message manager // Pipe Debugger message from/to parent/child via the message manager
childTransport = new ChildDebuggerTransport(mm, prefix); childTransport = new ChildDebuggerTransport(mm, prefix);
childTransport.hooks = { childTransport.hooks = {
@ -882,70 +884,65 @@ var DebuggerServer = {
}).bind(this); }).bind(this);
mm.addMessageListener("debug:actor", onActorCreated); mm.addMessageListener("debug:actor", onActorCreated);
let onMessageManagerClose = DevToolsUtils.makeInfallible(function (subject, topic, data) { let destroy = DevToolsUtils.makeInfallible(function () {
if (subject == mm) { // provides hook to actor modules that need to exchange messages
Services.obs.removeObserver(onMessageManagerClose, topic); // between e10s parent and child processes
DebuggerServer.emit("disconnected-from-child:" + prefix, { mm: mm, prefix: prefix });
// provides hook to actor modules that need to exchange messages
// between e10s parent and child processes
this.emit("disconnected-from-child:" + childID, { mm: mm, childID: childID });
mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
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.
mm.sendAsyncMessage("debug:disconnect", { childID: childID });
} else {
// Otherwise, the app has been closed before the actor
// had a chance to be created, so we are not able to create
// the actor.
deferred.resolve(null);
}
if (actor) {
// The ContentActor within the child process doesn't necessary
// have to time to uninitialize itself when the app is closed/killed.
// So ensure telling the client that the related actor is detached.
aConnection.send({ from: actor.actor, type: "tabDetached" });
actor = null;
}
if (netMonitor) {
netMonitor.destroy();
netMonitor = null;
}
if (aOnDisconnect) {
aOnDisconnect(mm);
}
}
}).bind(this);
Services.obs.addObserver(onMessageManagerClose,
"message-manager-close", false);
events.once(aConnection, "closed", () => {
if (childTransport) { if (childTransport) {
// When the client disconnects, we have to unplug the dedicated // If we have a child transport, the actor has already
// ChildDebuggerTransport... // been created. We need to stop using this message manager.
childTransport.close(); childTransport.close();
childTransport = null; childTransport = null;
aConnection.cancelForwarding(prefix); aConnection.cancelForwarding(prefix);
// ... and notify the child process to clean the tab actors. // ... and notify the child process to clean the tab actors.
mm.sendAsyncMessage("debug:disconnect", { childID: childID }); mm.sendAsyncMessage("debug:disconnect", { prefix: prefix });
} else {
if (netMonitor) { // Otherwise, the app has been closed before the actor
netMonitor.destroy(); // had a chance to be created, so we are not able to create
netMonitor = null; // the actor.
} deferred.resolve(null);
} }
if (actor) {
// The ContentActor within the child process doesn't necessary
// have time to uninitialize itself when the app is closed/killed.
// So ensure telling the client that the related actor is detached.
aConnection.send({ from: actor.actor, type: "tabDetached" });
actor = null;
}
if (netMonitor) {
netMonitor.destroy();
netMonitor = null;
}
if (aOnDestroy) {
aOnDestroy(mm);
}
// Cleanup all listeners
Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
if (!actor) {
mm.removeMessageListener("debug:actor", onActorCreated);
}
events.off(aConnection, "closed", destroy);
}); });
// Listen for app process exit
let onMessageManagerClose = function (subject, topic, data) {
if (subject == mm) {
destroy();
}
};
Services.obs.addObserver(onMessageManagerClose,
"message-manager-close", false);
// Listen for connection close to cleanup things
// when user unplug the device or we lose the connection somehow.
events.on(aConnection, "closed", destroy);
mm.sendAsyncMessage("debug:connect", { prefix: prefix }); mm.sendAsyncMessage("debug:connect", { prefix: prefix });
return deferred.promise; return deferred.promise;