mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-05 08:35:26 +00:00
3421774080
Message sequencing allows Marionette to provide an asynchronous, parallel pipelining user-facing interface, limit chances of payload race conditions, and remove stylistic inconsistencies in how commands and responses are dispatched internally. Clients that deliver a blocking WebDriver interface are still be expected to not send further command requests before the response from the last command has come back, but if they still happen to do so because of programming error or otherwise, no harm will be done. This will guard against bugs such as bug 1207125. This patch formalises the command and response concepts, and applies these concepts to emulator callbacks. Through the new message format, Marionette is able to provide two-way parallel communication. In other words, the server will be able to instruct the client to perform a command in a non ad-hoc way. runEmulatorCmd and runEmulatorShell are both turned into command instructions originating from the server. This resolves a lot of technical debt in the server code because they are no longer special-cased to circumvent the dispatching technique used for all other commands; commands may originate from either the client or the server providing parallel pipelining enforced through message sequencing: client server | | msgid=1 |----------->| | command | | | msgid=2 |<-----------| | command | | | msgid=2 |----------->| | response | | | msgid=1 |<-----------| | response | | | The protocol now consists of a "Command" message and the corresponding "Response" message. A "Response" message must always be sent in reply to a "Command" message. This bumps the Marionette protocol level to 3. r=dburns r=jgriffin --HG-- extra : commitid : 1kz4Oa2q3Un
231 lines
10 KiB
JavaScript
231 lines
10 KiB
JavaScript
/* 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/. */
|
|
|
|
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
this.EXPORTED_SYMBOLS = ["FrameManager"];
|
|
|
|
var FRAME_SCRIPT = "chrome://marionette/content/listener.js";
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
|
.getService(Ci.mozIJSSubScriptLoader);
|
|
|
|
//list of OOP frames that has the frame script loaded
|
|
var remoteFrames = [];
|
|
|
|
/**
|
|
* An object representing a frame that Marionette has loaded a
|
|
* frame script in.
|
|
*/
|
|
function MarionetteRemoteFrame(windowId, frameId) {
|
|
this.windowId = windowId; //outerWindowId relative to main process
|
|
this.frameId = frameId; //actual frame relative to windowId's frames list
|
|
this.targetFrameId = this.frameId; //assigned FrameId, used for messaging
|
|
};
|
|
|
|
/**
|
|
* The FrameManager will maintain the list of Out Of Process (OOP) frames and will handle
|
|
* frame switching between them.
|
|
* It handles explicit frame switching (switchToFrame), and implicit frame switching, which
|
|
* occurs when a modal dialog is triggered in B2G.
|
|
*
|
|
*/
|
|
this.FrameManager = function FrameManager(server) {
|
|
//messageManager maintains the messageManager for the current process' chrome frame or the global message manager
|
|
this.currentRemoteFrame = null; //holds a member of remoteFrames (for an OOP frame) or null (for the main process)
|
|
this.previousRemoteFrame = null; //frame we'll need to restore once interrupt is gone
|
|
this.handledModal = false; //set to true when we have been interrupted by a modal
|
|
this.server = server; // a reference to the marionette server
|
|
};
|
|
|
|
FrameManager.prototype = {
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener,
|
|
Ci.nsISupportsWeakReference]),
|
|
|
|
/**
|
|
* Receives all messages from content messageManager
|
|
*/
|
|
receiveMessage: function FM_receiveMessage(message) {
|
|
switch (message.name) {
|
|
case "MarionetteFrame:getInterruptedState":
|
|
// This will return true if the calling frame was interrupted by a modal dialog
|
|
if (this.previousRemoteFrame) {
|
|
let interruptedFrame = Services.wm.getOuterWindowWithId(this.previousRemoteFrame.windowId);//get the frame window of the interrupted frame
|
|
if (this.previousRemoteFrame.frameId != null) {
|
|
interruptedFrame = interruptedFrame.document.getElementsByTagName("iframe")[this.previousRemoteFrame.frameId]; //find the OOP frame
|
|
}
|
|
//check if the interrupted frame is the same as the calling frame
|
|
if (interruptedFrame.src == message.target.src) {
|
|
return {value: this.handledModal};
|
|
}
|
|
}
|
|
else if (this.currentRemoteFrame == null) {
|
|
// we get here if previousRemoteFrame and currentRemoteFrame are null, ie: if we're in a non-OOP process, or we haven't switched into an OOP frame, in which case, handledModal can't be set to true.
|
|
return {value: this.handledModal};
|
|
}
|
|
return {value: false};
|
|
case "MarionetteFrame:handleModal":
|
|
/*
|
|
* handleModal is called when we need to switch frames to the main process due to a modal dialog interrupt.
|
|
*/
|
|
// If previousRemoteFrame was set, that means we switched into a remote frame.
|
|
// If this is the case, then we want to switch back into the system frame.
|
|
// If it isn't the case, then we're in a non-OOP environment, so we don't need to handle remote frames
|
|
let isLocal = true;
|
|
if (this.currentRemoteFrame != null) {
|
|
isLocal = false;
|
|
this.removeMessageManagerListeners(this.currentRemoteFrame.messageManager.get());
|
|
//store the previous frame so we can switch back to it when the modal is dismissed
|
|
this.previousRemoteFrame = this.currentRemoteFrame;
|
|
//by setting currentRemoteFrame to null, it signifies we're in the main process
|
|
this.currentRemoteFrame = null;
|
|
this.server.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
|
|
.getService(Ci.nsIMessageBroadcaster);
|
|
}
|
|
this.handledModal = true;
|
|
this.server.sendOk(this.server.command_id);
|
|
return {value: isLocal};
|
|
case "MarionetteFrame:getCurrentFrameId":
|
|
if (this.currentRemoteFrame != null) {
|
|
return this.currentRemoteFrame.frameId;
|
|
}
|
|
}
|
|
},
|
|
|
|
getOopFrame: function FM_getOopFrame(winId, frameId) {
|
|
// get original frame window
|
|
let outerWin = Services.wm.getOuterWindowWithId(winId);
|
|
// find the OOP frame
|
|
let f = outerWin.document.getElementsByTagName("iframe")[frameId];
|
|
return f;
|
|
},
|
|
|
|
getFrameMM: function FM_getFrameMM(winId, frameId) {
|
|
let oopFrame = this.getOopFrame(winId, frameId);
|
|
let mm = oopFrame.QueryInterface(Ci.nsIFrameLoaderOwner)
|
|
.frameLoader.messageManager;
|
|
return mm;
|
|
},
|
|
|
|
/**
|
|
* Switch to OOP frame. We're handling this here
|
|
* so we can maintain a list of remote frames.
|
|
*/
|
|
switchToFrame: function FM_switchToFrame(winId, frameId) {
|
|
let oopFrame = this.getOopFrame(winId, frameId);
|
|
let mm = this.getFrameMM(winId, frameId);
|
|
|
|
// See if this frame already has our frame script loaded in it;
|
|
// if so, just wake it up.
|
|
for (let i = 0; i < remoteFrames.length; i++) {
|
|
let frame = remoteFrames[i];
|
|
let frameMessageManager = frame.messageManager.get();
|
|
try {
|
|
frameMessageManager.sendAsyncMessage("aliveCheck", {});
|
|
} catch (e) {
|
|
if (e.result == Components.results.NS_ERROR_NOT_INITIALIZED) {
|
|
remoteFrames.splice(i--, 1);
|
|
continue;
|
|
}
|
|
}
|
|
if (frameMessageManager == mm) {
|
|
this.currentRemoteFrame = frame;
|
|
this.addMessageManagerListeners(mm);
|
|
|
|
mm.sendAsyncMessage("Marionette:restart");
|
|
return oopFrame.id;
|
|
}
|
|
}
|
|
|
|
// If we get here, then we need to load the frame script in this frame,
|
|
// and set the frame's ChromeMessageSender as the active message manager
|
|
// the server will listen to.
|
|
this.addMessageManagerListeners(mm);
|
|
let aFrame = new MarionetteRemoteFrame(winId, frameId);
|
|
aFrame.messageManager = Cu.getWeakReference(mm);
|
|
remoteFrames.push(aFrame);
|
|
this.currentRemoteFrame = aFrame;
|
|
|
|
mm.loadFrameScript(FRAME_SCRIPT, true, true);
|
|
|
|
return oopFrame.id;
|
|
},
|
|
|
|
/*
|
|
* This function handles switching back to the frame that was interrupted by the modal dialog.
|
|
* This function gets called by the interrupted frame once the dialog is dismissed and the frame resumes its process
|
|
*/
|
|
switchToModalOrigin: function FM_switchToModalOrigin() {
|
|
//only handle this if we indeed switched out of the modal's originating frame
|
|
if (this.previousRemoteFrame != null) {
|
|
this.currentRemoteFrame = this.previousRemoteFrame;
|
|
this.addMessageManagerListeners(this.currentRemoteFrame.messageManager.get());
|
|
}
|
|
this.handledModal = false;
|
|
},
|
|
|
|
/**
|
|
* Adds message listeners to the server,
|
|
* listening for messages from content frame scripts.
|
|
* It also adds a MarionetteFrame:getInterruptedState
|
|
* message listener to the FrameManager,
|
|
* so the frame manager's state can be checked by the frame.
|
|
*
|
|
* @param {nsIMessageListenerManager} mm
|
|
* The message manager object, typically
|
|
* ChromeMessageBroadcaster or ChromeMessageSender.
|
|
*/
|
|
addMessageManagerListeners: function FM_addMessageManagerListeners(mm) {
|
|
mm.addWeakMessageListener("Marionette:ok", this.server);
|
|
mm.addWeakMessageListener("Marionette:done", this.server);
|
|
mm.addWeakMessageListener("Marionette:error", this.server);
|
|
mm.addWeakMessageListener("Marionette:emitTouchEvent", this.server);
|
|
mm.addWeakMessageListener("Marionette:log", this.server);
|
|
mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
|
|
mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
|
|
mm.addWeakMessageListener("Marionette:shareData", this.server);
|
|
mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.server);
|
|
mm.addWeakMessageListener("Marionette:switchedToFrame", this.server);
|
|
mm.addWeakMessageListener("Marionette:getVisibleCookies", this.server);
|
|
mm.addWeakMessageListener("Marionette:register", this.server);
|
|
mm.addWeakMessageListener("Marionette:listenersAttached", this.server);
|
|
mm.addWeakMessageListener("Marionette:getFiles", this.server);
|
|
mm.addWeakMessageListener("MarionetteFrame:handleModal", this);
|
|
mm.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
|
|
mm.addWeakMessageListener("MarionetteFrame:getInterruptedState", this);
|
|
},
|
|
|
|
/**
|
|
* Removes listeners for messages from content frame scripts.
|
|
* We do not remove the MarionetteFrame:getInterruptedState
|
|
* or the Marionette:switchToModalOrigin message listener,
|
|
* because we want to allow all known frames to contact the frame manager
|
|
* so that it can check if it was interrupted, and if so,
|
|
* it will call switchToModalOrigin when its process gets resumed.
|
|
*
|
|
* @param {nsIMessageListenerManager} mm
|
|
* The message manager object, typically
|
|
* ChromeMessageBroadcaster or ChromeMessageSender.
|
|
*/
|
|
removeMessageManagerListeners: function FM_removeMessageManagerListeners(mm) {
|
|
mm.removeWeakMessageListener("Marionette:ok", this.server);
|
|
mm.removeWeakMessageListener("Marionette:done", this.server);
|
|
mm.removeWeakMessageListener("Marionette:error", this.server);
|
|
mm.removeWeakMessageListener("Marionette:log", this.server);
|
|
mm.removeWeakMessageListener("Marionette:shareData", this.server);
|
|
mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
|
|
mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
|
|
mm.removeWeakMessageListener("Marionette:switchedToFrame", this.server);
|
|
mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.server);
|
|
mm.removeWeakMessageListener("Marionette:listenersAttached", this.server);
|
|
mm.removeWeakMessageListener("Marionette:register", this.server);
|
|
mm.removeWeakMessageListener("Marionette:getFiles", this.server);
|
|
mm.removeWeakMessageListener("MarionetteFrame:handleModal", this);
|
|
mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
|
|
}
|
|
};
|