Bug 977043 - Tweak TabActor to support changing its targeted context to an iframe. r=bgrins,past

This commit is contained in:
Alexandre Poirot 2014-08-27 12:19:30 +02:00
parent 053c4183ad
commit f369af1400
3 changed files with 291 additions and 30 deletions

View File

@ -225,6 +225,7 @@ const UnsolicitedNotifications = {
"reflowActivity": "reflowActivity",
"addonListChanged": "addonListChanged",
"tabNavigated": "tabNavigated",
"frameUpdate": "frameUpdate",
"pageError": "pageError",
"documentLoad": "documentLoad",
"enteredFrame": "enteredFrame",

View File

@ -38,7 +38,7 @@ Object.defineProperty(ContentActor.prototype, "docShell", {
return this._chromeGlobal.docShell;
},
enumerable: true,
configurable: false
configurable: true
});
ContentActor.prototype.exit = function() {

View File

@ -33,9 +33,11 @@ XPCOMUtils.defineLazyGetter(this, "StyleSheetActor", () => {
return require("devtools/server/actors/stylesheets").StyleSheetActor;
});
// Also depends on following symbols, shared by common scope with main.js:
// DebuggerServer, CommonCreateExtraActors, CommonAppendExtraActors, ActorPool,
// ThreadActor
function getWindowID(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
}
/**
* Browser-specific actors.
@ -559,7 +561,7 @@ function TabActor(aConnection)
shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
});
this.traits = { reconfigure: true };
this.traits = { reconfigure: true, frames: true };
}
// XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
@ -620,7 +622,10 @@ TabActor.prototype = {
let docShells = [];
while (docShellsEnum.hasMoreElements()) {
docShells.push(docShellsEnum.getNext());
let docShell = docShellsEnum.getNext();
docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
docShells.push(docShell);
}
return docShells;
@ -807,11 +812,176 @@ TabActor.prototype = {
this._pushContext();
this._progressListener = new DebuggerProgressListener(this);
this._progressListener.watch(this.docShell);
// Save references to the original document we attached to
this._originalWindow = this.window;
// Ensure replying to attach() request first
// before notifying about new docshells.
DevToolsUtils.executeSoon(() => this._watchDocshells());
this._attached = true;
},
_watchDocshells: function BTA_watchDocshells() {
// In child processes, we watch all docshells living in the process.
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
Services.obs.addObserver(this, "webnavigation-create", false);
}
Services.obs.addObserver(this, "webnavigation-destroy", false);
// We watch for all child docshells under the current document,
this._progressListener.watch(this.docShell);
// And list all already existing ones.
this._updateChildDocShells();
},
onSwitchToFrame: function BTA_onSwitchToFrame(aRequest) {
let windowId = aRequest.windowId;
let win;
try {
win = Services.wm.getOuterWindowWithId(windowId);
} catch(e) {}
if (!win) {
return { error: "noWindow",
message: "The related docshell is destroyed or not found" };
} else if (win == this.window) {
return {};
}
// Reply first before changing the document
DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
return {};
},
onListFrames: function BTA_onListFrames(aRequest) {
let windows = this._docShellsToWindows(this.docShells);
return { frames: windows };
},
observe: function (aSubject, aTopic, aData) {
// Ignore any event that comes before/after the tab actor is attached
// That typically happens during firefox shutdown.
if (!this.attached) {
return;
}
if (aTopic == "webnavigation-create") {
aSubject.QueryInterface(Ci.nsIDocShell);
// webnavigation-create is fired very early during docshell construction.
// In new root docshells within child processes, involving TabChild,
// this event is from within this call:
// http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
// whereas the chromeEventHandler (and most likely other stuff) is set later:
// http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
// So wait a tick before watching it:
DevToolsUtils.executeSoon(() => {
// In child processes, we have new root docshells,
// let's watch them and all their child docshells.
if (this._isRootDocShell(aSubject)) {
this._progressListener.watch(aSubject);
}
this._notifyDocShellsUpdate([aSubject]);
});
} else if (aTopic == "webnavigation-destroy") {
let webProgress = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this._notifyDocShellDestroy(webProgress);
}
},
_isRootDocShell: function (docShell) {
// Root docshells like top level xul windows don't have chromeEventHandler.
// Root docshells in child processes have one, it is TabChildGlobal,
// which isn't a DOM Element.
// Non-root docshell have a chromeEventHandler that is either
// xul:iframe, xul:browser or html:iframe.
return !docShell.chromeEventHandler ||
!(docShell.chromeEventHandler instanceof Ci.nsIDOMElement);
},
// Convert docShell list to windows objects list being sent to the client
_docShellsToWindows: function (docshells) {
return docshells.map(docShell => {
let window = docShell.DOMWindow;
let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
let parentID = undefined;
// Ignore the parent of the original document on non-e10s firefox,
// as we get the xul window as parent and don't care about it.
if (window.parent && window != this._originalWindow) {
parentID = window.parent
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
return {
id: id,
url: window.location.href,
title: window.title,
parentID: parentID
};
});
},
_notifyDocShellsUpdate: function (docshells) {
let windows = this._docShellsToWindows(docshells);
this.conn.send({ from: this.actorID,
type: "frameUpdate",
frames: windows
});
},
_updateChildDocShells: function () {
this._notifyDocShellsUpdate(this.docShells);
},
_notifyDocShellDestroy: function (webProgress) {
let id = webProgress.DOMWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
this.conn.send({ from: this.actorID,
type: "frameUpdate",
frames: [{
id: id,
destroy: true
}]
});
// Stop watching this docshell if it's a root one.
// (child processes spawn new root docshells)
webProgress.QueryInterface(Ci.nsIDocShell);
if (this._isRootDocShell(webProgress)) {
this._progressListener.unwatch(webProgress);
}
if (webProgress.DOMWindow == this._originalWindow) {
// If for some reason (typically during Firefox shutdown), the original
// document is destroyed, we detach the tab actor to unregister all listeners
// and prevent any exception.
this.exit();
return;
}
// If the currently targeted context is destroyed,
// and we aren't on the top-level document,
// we have to switch to the top-level one.
if (webProgress.DOMWindow == this.window &&
this.window != this._originalWindow) {
this._changeTopLevelDocument(this._originalWindow);
}
},
_notifyDocShellDestroyAll: function () {
this.conn.send({ from: this.actorID,
type: "frameUpdate",
destroyAll: true
});
},
/**
* Creates a thread actor and a pool for context-lifetime actors. It then sets
* up the content window for debugging.
@ -856,6 +1026,13 @@ TabActor.prototype = {
}
this._progressListener.destroy();
this._progressListener = null;
this._originalWindow = null;
// Removes the observers being set in _watchDocShells
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
Services.obs.removeObserver(this, "webnavigation-create", false);
}
Services.obs.removeObserver(this, "webnavigation-destroy", false);
this._popContext();
@ -1043,17 +1220,64 @@ TabActor.prototype = {
}
},
_changeTopLevelDocument: function (window) {
// Fake a will-navigate on the previous document
// to let a chance to unregister it
this._willNavigate(this.window, window.location.href, null, true);
this._windowDestroyed(this.window);
DevToolsUtils.executeSoon(() => {
this._setWindow(window);
// Then fake window-ready and navigate on the given document
this._windowReady(window, true);
DevToolsUtils.executeSoon(() => {
this._navigate(window, true);
});
});
},
_setWindow: function (window) {
let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
// Here is the very important call where we switch the currently
// targeted context (it will indirectly update this.window and
// many other attributes defined from docShell).
Object.defineProperty(this, "docShell", {
value: docShell,
enumerable: true,
configurable: true
});
events.emit(this, "changed-toplevel-document");
let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
this.conn.send({ from: this.actorID,
type: "frameUpdate",
selected: id
});
},
/**
* Handle location changes, by clearing the previous debuggees and enabling
* debugging, which may have been disabled temporarily by the
* DebuggerProgressListener.
*/
_windowReady: function (window) {
_windowReady: function (window, isFrameSwitching = false) {
let isTopLevel = window == this.window;
// We just reset iframe list on WillNavigate, so we now list all existing
// frames when we load a new document in the original window
if (window == this._originalWindow && !isFrameSwitching) {
this._updateChildDocShells();
}
events.emit(this, "window-ready", {
window: window,
isTopLevel: isTopLevel
isTopLevel: isTopLevel,
id: getWindowID(window)
});
// TODO bug 997119: move that code to ThreadActor by listening to window-ready
@ -1062,9 +1286,11 @@ TabActor.prototype = {
threadActor.clearDebuggees();
if (threadActor.dbg) {
threadActor.dbg.enabled = true;
threadActor.global = window;
threadActor.maybePauseOnExceptions();
}
// Update the global no matter if the debugger is on or off,
// otherwise the global will be wrong when enabled later.
threadActor.global = window;
}
for (let sheetActor of this._styleSheetActors.values()) {
@ -1080,19 +1306,37 @@ TabActor.prototype = {
}
},
_windowDestroyed: function (window) {
_windowDestroyed: function (window, id = null) {
events.emit(this, "window-destroyed", {
window: window,
isTopLevel: window == this.window
isTopLevel: window == this.window,
id: id || getWindowID(window)
});
},
/**
* Start notifying server codebase and client about a new document
* Start notifying server and client about a new document
* being loaded in the currently targeted context.
*/
_willNavigate: function (window, newURI, request) {
_willNavigate: function (window, newURI, request, isFrameSwitching = false) {
let isTopLevel = window == this.window;
let reset = false;
if (window == this._originalWindow && !isFrameSwitching) {
// Clear the iframe list if the original top-level document changes.
this._notifyDocShellDestroyAll();
// If the top level document changes and we are targeting
// an iframe, we need to reset to the upcoming new top level document.
// But for this will-navigate event, we will dispatch on the old window.
// (The inspector codebase expect to receive will-navigate for the currently
// displayed document in order to cleanup the markup view)
if (this.window != this._originalWindow) {
reset=true;
window = this.window;
isTopLevel = true;
}
}
// will-navigate event needs to be dispatched synchronously,
// by calling the listeners in the order or registration.
@ -1128,15 +1372,20 @@ TabActor.prototype = {
type: "tabNavigated",
url: newURI,
nativeConsoleAPI: true,
state: "start"
state: "start",
isFrameSwitching: isFrameSwitching
});
if (reset) {
this._setWindow(this._originalWindow);
}
},
/**
* Notify server and client about a new document done loading in the current
* targeted context.
*/
_navigate: function (window) {
_navigate: function (window, isFrameSwitching = false) {
let isTopLevel = window == this.window;
// navigate event needs to be dispatched synchronously,
@ -1166,7 +1415,8 @@ TabActor.prototype = {
url: this.url,
title: this.title,
nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
state: "stop"
state: "stop",
isFrameSwitching: isFrameSwitching
});
},
@ -1222,7 +1472,9 @@ TabActor.prototype.requestTypes = {
"detach": TabActor.prototype.onDetach,
"reload": TabActor.prototype.onReload,
"navigateTo": TabActor.prototype.onNavigateTo,
"reconfigure": TabActor.prototype.onReconfigure
"reconfigure": TabActor.prototype.onReconfigure,
"switchToFrame": TabActor.prototype.onSwitchToFrame,
"listFrames": TabActor.prototype.onListFrames
};
exports.TabActor = TabActor;
@ -1258,7 +1510,7 @@ Object.defineProperty(BrowserTabActor.prototype, "docShell", {
return null;
},
enumerable: true,
configurable: false
configurable: true
});
Object.defineProperty(BrowserTabActor.prototype, "title", {
@ -1637,14 +1889,17 @@ DebuggerProgressListener.prototype = {
// Dispatch the _windowReady event on the tabActor for pre-existing windows
for (let win of this._getWindowsInDocShell(docShell)) {
this._tabActor._windowReady(win);
this._knownWindowIDs.set(this._getWindowID(win), win);
this._knownWindowIDs.set(getWindowID(win), win);
}
},
unwatch: function(docShell) {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.removeProgressListener(this);
// During process shutdown, the docshell may already be cleaned up and throw
try {
webProgress.removeProgressListener(this);
} catch(e) {}
// TODO: fix docShell.chromeEventHandler in child processes!
let handler = docShell.chromeEventHandler ||
@ -1656,7 +1911,7 @@ DebuggerProgressListener.prototype = {
handler.removeEventListener("pagehide", this._onWindowHidden, true);
for (let win of this._getWindowsInDocShell(docShell)) {
this._knownWindowIDs.delete(this._getWindowID(win));
this._knownWindowIDs.delete(getWindowID(win));
}
},
@ -1675,12 +1930,6 @@ DebuggerProgressListener.prototype = {
return windows;
},
_getWindowID: function(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
},
onWindowCreated: DevToolsUtils.makeInfallible(function(evt) {
if (!this._tabActor.attached) {
return;
@ -1697,7 +1946,7 @@ DebuggerProgressListener.prototype = {
this._tabActor._windowReady(window);
if (evt.type !== "pageshow") {
this._knownWindowIDs.set(this._getWindowID(window), window);
this._knownWindowIDs.set(getWindowID(window), window);
}
}, "DebuggerProgressListener.prototype.onWindowCreated"),
@ -1730,7 +1979,7 @@ DebuggerProgressListener.prototype = {
let window = this._knownWindowIDs.get(innerID);
if (window) {
this._knownWindowIDs.delete(innerID);
this._tabActor._windowDestroyed(window);
this._tabActor._windowDestroyed(window, innerID);
}
}, "DebuggerProgressListener.prototype.observe"),
@ -1745,12 +1994,23 @@ DebuggerProgressListener.prototype = {
let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
// Catch any iframe location change
if (isDocument && isStop) {
// Watch document stop to ensure having the new iframe url.
aProgress.QueryInterface(Ci.nsIDocShell);
this._tabActor._notifyDocShellsUpdate([aProgress]);
}
let window = aProgress.DOMWindow;
if (isDocument && isStart) {
// One of the earliest events that tells us a new URI
// is being loaded in this window.
let newURI = aRequest instanceof Ci.nsIChannel ? aRequest.URI.spec : null;
this._tabActor._willNavigate(window, newURI, aRequest);
}
if (isWindow && isStop) {
// Somewhat equivalent of load event.
// (window.document.readyState == complete)
this._tabActor._navigate(window);
}
}, "DebuggerProgressListener.prototype.onStateChange")