Bug 1332122 - Re-register the browser for frame script reloads. r=automatedtester

With multi-processes for content reloads of the frame script
can happen at any time, and not only for remoteness changes.
As such the current browser has to be re-registered with the
new id of the listener.

MozReview-Commit-ID: 48MOZfuPTR9

--HG--
extra : rebase_source : 60e91ae7e1cdc942d0ac9a9dd3aac3baeccc5ddf
This commit is contained in:
Henrik Skupin 2017-07-06 18:02:19 +02:00
parent d373a550b4
commit 618082fc0c
3 changed files with 86 additions and 102 deletions

View File

@ -78,8 +78,8 @@ browser.getTabBrowser = function(win) {
browser.Context = class {
/**
* @param {<xul:browser>} win
* Frame that is expected to contain the view of the web document.
* @param {ChromeWindow} win
* ChromeWindow that contains the top-level browsing context.
* @param {GeckoDriver} driver
* Reference to driver instance.
*/
@ -106,7 +106,14 @@ browser.Context = class {
// browser window, this.tab will still point to tab A, despite tab B
// being the currently selected tab.
this.tab = null;
// Commands which trigger a page load can cause the frame script to be
// reloaded. To not loose the currently active command, or any other
// already pushed following command, store them as long as they haven't
// been fully processed. The commands get flushed after a new browser
// has been registered.
this.pendingCommands = [];
this._needsFlushPendingCommands = false;
// We should have one frame.Manager per browser.Context so that we
// can handle modals in each <xul:browser>.
@ -117,8 +124,6 @@ browser.Context = class {
this.frameManager.addMessageManagerListeners(driver.mm);
this.getIdForBrowser = driver.getIdForBrowser.bind(driver);
this.updateIdForBrowser = driver.updateIdForBrowser.bind(driver);
this._browserWasRemote = null;
this._hasRemotenessChange = false;
}
/**
@ -286,8 +291,7 @@ browser.Context = class {
}
/**
* Set the current tab and update remoteness tracking if a tabbrowser
* is available.
* Set the current tab.
*
* @param {number=} index
* Tab index to switch to. If the parameter is undefined,
@ -330,11 +334,6 @@ browser.Context = class {
}
}
}
if (this.driver.appName == "Firefox") {
this._browserWasRemote = this.contentBrowser.isRemoteBrowser;
this._hasRemotenessChange = false;
}
}
/**
@ -344,65 +343,38 @@ browser.Context = class {
*
* @param {string} uid
* Frame uid for use by Marionette.
* @param the XUL <browser> that was the target of the originating message.
* @param {xul:browser} target
* The <xul:browser> that was the target of the originating message.
*/
register(uid, target) {
let remotenessChange = this.hasRemotenessChange();
if (this.curFrameId === null || remotenessChange) {
if (this.tabBrowser) {
// If we're setting up a new session on Firefox, we only process the
// registration for this frame if it belongs to the current tab.
if (!this.tab) {
this.switchToTab();
}
if (this.tabBrowser) {
// If we're setting up a new session on Firefox, we only process the
// registration for this frame if it belongs to the current tab.
if (!this.tab) {
this.switchToTab();
}
if (target === this.contentBrowser) {
this.updateIdForBrowser(this.contentBrowser, uid);
}
if (target === this.contentBrowser) {
this.updateIdForBrowser(this.contentBrowser, uid);
this._needsFlushPendingCommands = true;
}
}
// used to delete sessions
this.knownFrames.push(uid);
return remotenessChange;
}
/**
* When navigating between pages results in changing a browser's
* process, we need to take measures not to lose contact with a listener
* script. This function does the necessary bookkeeping.
*/
hasRemotenessChange() {
// None of these checks are relevant if we don't have a tab yet,
// and may not apply on Fennec.
if (this.driver.appName != "Firefox" ||
this.tab === null ||
this.contentBrowser === null) {
return false;
}
if (this._hasRemotenessChange) {
return true;
}
let currentIsRemote = this.contentBrowser.isRemoteBrowser;
this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote;
this._browserWasRemote = currentIsRemote;
return this._hasRemotenessChange;
}
/**
* Flushes any pending commands queued when a remoteness change is being
* processed and mark this remotenessUpdate as complete.
* Flushes any queued pending commands after a reload of the frame script.
*/
flushPendingCommands() {
if (!this._hasRemotenessChange) {
if (!this._needsFlushPendingCommands) {
return;
}
this._hasRemotenessChange = false;
this.pendingCommands.forEach(cb => cb());
this.pendingCommands = [];
this._needsFlushPendingCommands = false;
}
/**
@ -412,11 +384,11 @@ browser.Context = class {
* No commands interacting with content are safe to process until
* the new listener script is loaded and registers itself.
* This occurs when a command whose effect is asynchronous (such
* as goBack) results in a remoteness change and new commands
* as goBack) results in a reload of the frame script and new commands
* are subsequently posted to the server.
*/
executeWhenReady(cb) {
if (this.hasRemotenessChange()) {
if (this._needsFlushPendingCommands) {
this.pendingCommands.push(cb);
} else {
cb();

View File

@ -571,8 +571,6 @@ GeckoDriver.prototype.registerBrowser = function(id, be) {
this.curBrowser.frameManager.currentRemoteFrame.targetFrameId = id;
}
let reg = {};
// We want to ignore frames that are XUL browsers that aren't in the "main"
// tabbrowser, but accept things on Fennec (which doesn't have a
// xul:tabbrowser), and accept HTML iframes (because tests depend on it),
@ -581,11 +579,10 @@ GeckoDriver.prototype.registerBrowser = function(id, be) {
if (this.appName != "Firefox" || be.namespaceURI != XUL_NS ||
be.nodeName != "browser" || be.getTabBrowser()) {
// curBrowser holds all the registered frames in knownFrames
reg.id = id;
reg.remotenessChange = this.curBrowser.register(id, be);
this.curBrowser.register(id, be);
}
this.wins.set(reg.id, listenerWindow);
this.wins.set(id, listenerWindow);
if (nullPrevious && (this.curBrowser.curFrameId !== null)) {
this.sendAsync(
"newSession",
@ -596,7 +593,7 @@ GeckoDriver.prototype.registerBrowser = function(id, be) {
}
}
return [reg, this.capabilities.toJSON()];
return [id, this.capabilities.toJSON()];
};
GeckoDriver.prototype.registerPromise = function() {
@ -626,10 +623,13 @@ GeckoDriver.prototype.registerPromise = function() {
GeckoDriver.prototype.listeningPromise = function() {
const li = "Marionette:listenersAttached";
return new Promise(resolve => {
let cb = () => {
this.mm.removeMessageListener(li, cb);
resolve();
let cb = msg => {
if (msg.json.listenerId === this.curBrowser.curFrameId) {
this.mm.removeMessageListener(li, cb);
resolve();
}
};
this.mm.addMessageListener(li, cb);
});
@ -978,9 +978,9 @@ GeckoDriver.prototype.get = function* (cmd, resp) {
let get = this.listener.get({url, pageTimeout: this.timeouts.pageLoad});
// If a remoteness update interrupts our page load, this will never return
// We need to re-issue this request to correctly poll for readyState and
// send errors.
// If a reload of the frame script interrupts our page load, this will
// never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
@ -1097,9 +1097,9 @@ GeckoDriver.prototype.goBack = function* (cmd, resp) {
let lastURL = this.currentURL;
let goBack = this.listener.goBack({pageTimeout: this.timeouts.pageLoad});
// If a remoteness update interrupts our page load, this will never return
// We need to re-issue this request to correctly poll for readyState and
// send errors.
// If a reload of the frame script interrupts our page load, this will
// never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
@ -1141,9 +1141,9 @@ GeckoDriver.prototype.goForward = function* (cmd, resp) {
let goForward = this.listener.goForward(
{pageTimeout: this.timeouts.pageLoad});
// If a remoteness update interrupts our page load, this will never return
// We need to re-issue this request to correctly poll for readyState and
// send errors.
// If a reload of the frame script interrupts our page load, this will
// never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
@ -1176,7 +1176,25 @@ GeckoDriver.prototype.refresh = function* (cmd, resp) {
assert.window(this.getCurrentWindow());
assert.noUserPrompt(this.dialog);
yield this.listener.refresh({pageTimeout: this.timeouts.pageLoad});
let refresh = this.listener.refresh(
{pageTimeout: this.timeouts.pageLoad})
// If a reload of the frame script interrupts our page load, this will
// never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
command_id: this.listener.activeMessageId,
pageTimeout: this.timeouts.pageLoad,
startTime: new Date().getTime(),
};
this.mm.broadcastAsyncMessage(
"Marionette:waitForPageLoaded" + this.curBrowser.curFrameId,
parameters);
});
yield refresh;
};
/**
@ -2069,9 +2087,9 @@ GeckoDriver.prototype.clickElement = function* (cmd, resp) {
let click = this.listener.clickElement(
{id, pageTimeout: this.timeouts.pageLoad});
// If a remoteness update interrupts our page load, this will
// never return We need to re-issue this request to correctly poll
// for readyState and send errors.
// If a reload of the frame script interrupts our page load, this will
// never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
@ -3178,9 +3196,9 @@ GeckoDriver.prototype.receiveMessage = function(message) {
case "Marionette:listenersAttached":
if (message.json.listenerId === this.curBrowser.curFrameId) {
// If remoteness gets updated we need to call newSession. In the case
// of desktop this just sets up a small amount of state that doesn't
// change over the course of a session.
// If the frame script gets reloaded we need to call newSession.
// In the case of desktop this just sets up a small amount of state
// that doesn't change over the course of a session.
this.sendAsync("newSession", this.capabilities.toJSON());
this.curBrowser.flushPendingCommands();
}

View File

@ -54,7 +54,6 @@ var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
var listenerId = null; // unique ID of this listener
var curContainer = {frame: content, shadowRoot: null};
var isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
var previousContainer = null;
var seenEls = new element.Store();
@ -118,10 +117,10 @@ var sandboxes = new Sandboxes(() => curContainer.frame);
var sandboxName = "default";
/**
* The load listener singleton helps to keep track of active page
* load activities, and can be used by any command which might cause a
* navigation to happen. In the specific case of remoteness changes it
* allows to continue observing the current page load.
* The load listener singleton helps to keep track of active page load
* activities, and can be used by any command which might cause a navigation
* to happen. In the specific case of a reload of the frame script it allows
* to continue observing the current page load.
*/
var loadListener = {
command_id: null,
@ -157,7 +156,7 @@ var loadListener = {
.createInstance(Ci.nsITimer);
this.timerPageUnload = null;
// In case of a remoteness change, only wait the remaining time
// In case the frame script has been reloaded, wait the remaining time
timeout = startTime + timeout - new Date().getTime();
if (timeout <= 0) {
@ -212,8 +211,8 @@ var loadListener = {
}
}
// In the case when the observer was added before a remoteness change,
// it will no longer be available. Exceptions can be silently ignored.
// In case the observer was added before the frame script has been
// reloaded, it will no longer be available. Exceptions can be ignored.
try {
Services.obs.removeObserver(this, "outer-window-destroyed");
} catch (e) {}
@ -344,8 +343,8 @@ var loadListener = {
},
/**
* Continue to listen for page load events after a remoteness change
* happened.
* Continue to listen for page load events after the frame script has been
* reloaded.
*
* @param {number} command_id
* ID of the currently handled message between the driver and
@ -356,7 +355,7 @@ var loadListener = {
* @param {number} startTime
* Unix timestap when the navitation request got triggered.
*/
waitForLoadAfterRemotenessChange(command_id, timeout, startTime) {
waitForLoadAfterFramescriptReload(command_id, timeout, startTime) {
this.start(command_id, timeout, startTime, false);
},
@ -430,18 +429,13 @@ function registerSelf() {
// register will have the ID and a boolean describing if this is the
// main process or not
let register = sendSyncMessage("Marionette:register", msg);
if (register[0]) {
let {id, remotenessChange} = register[0][0];
listenerId = register[0][0];
capabilities = session.Capabilities.fromJSON(register[0][1]);
listenerId = id;
if (typeof id != "undefined") {
if (typeof listenerId != "undefined") {
startListeners();
let rv = {};
if (remotenessChange) {
rv.listenerId = id;
}
sendAsyncMessage("Marionette:listenersAttached", rv);
sendAsyncMessage("Marionette:listenersAttached",
{"listenerId": listenerId});
}
}
}
@ -1105,8 +1099,8 @@ function cancelRequest() {
}
/**
* This implements the latter part of a get request (for the case we
* need to resume one when a remoteness update happens in the middle of a
* This implements the latter part of a get request (for the case we need
* to resume one when the frame script has been reloaded in the middle of a
* navigate request). This is most of of the work of a navigate request,
* but doesn't assume DOMContentLoaded is yet to fire.
*
@ -1122,7 +1116,7 @@ function cancelRequest() {
function waitForPageLoaded(msg) {
let {command_id, pageTimeout, startTime} = msg.json;
loadListener.waitForLoadAfterRemotenessChange(
loadListener.waitForLoadAfterFramescriptReload(
command_id, pageTimeout, startTime);
}