diff options
Diffstat (limited to 'testing/marionette/proxy.js')
-rw-r--r-- | testing/marionette/proxy.js | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/testing/marionette/proxy.js b/testing/marionette/proxy.js new file mode 100644 index 000000000..9d338d44a --- /dev/null +++ b/testing/marionette/proxy.js @@ -0,0 +1,376 @@ +/* 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/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("chrome://marionette/content/error.js"); +Cu.import("chrome://marionette/content/modal.js"); + +this.EXPORTED_SYMBOLS = ["proxy"]; + +const uuidgen = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + +// Proxy handler that traps requests to get a property. Will prioritise +// properties that exist on the object's own prototype. +var ownPriorityGetterTrap = { + get: (obj, prop) => { + if (obj.hasOwnProperty(prop)) { + return obj[prop]; + } + return (...args) => obj.send(prop, args); + } +}; + +this.proxy = {}; + +/** + * Creates a transparent interface between the chrome- and content + * contexts. + * + * Calls to this object will be proxied via the message manager to a + * content frame script, and responses are returend as promises. + * + * The argument sequence is serialised and passed as an array, unless it + * consists of a single object type that isn't null, in which case it's + * passed literally. The latter specialisation is temporary to achieve + * backwards compatibility with listener.js. + * + * @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn + * Closure function returning the current message manager. + * @param {function(string, Object, number)} sendAsyncFn + * Callback for sending async messages. + */ +proxy.toListener = function (mmFn, sendAsyncFn) { + let sender = new proxy.AsyncMessageChannel(mmFn, sendAsyncFn); + return new Proxy(sender, ownPriorityGetterTrap); +}; + +/** + * Provides a transparent interface between chrome- and content space. + * + * The AsyncMessageChannel is an abstraction of the message manager + * IPC architecture allowing calls to be made to any registered message + * listener in Marionette. The {@code #send(...)} method returns a promise + * that gets resolved when the message handler calls {@code .reply(...)}. + */ +proxy.AsyncMessageChannel = class { + constructor(mmFn, sendAsyncFn) { + this.sendAsync = sendAsyncFn; + // TODO(ato): Bug 1242595 + this.activeMessageId = null; + + this.mmFn_ = mmFn; + this.listeners_ = new Map(); + this.dialogueObserver_ = null; + } + + get mm() { + return this.mmFn_(); + } + + /** + * Send a message across the channel. The name of the function to + * call must be registered as a message listener. + * + * Usage: + * + * let channel = new AsyncMessageChannel( + * messageManager, sendAsyncMessage.bind(this)); + * let rv = yield channel.send("remoteFunction", ["argument"]); + * + * @param {string} name + * Function to call in the listener, e.g. for the message listener + * "Marionette:foo8", use "foo". + * @param {Array.<?>=} args + * Argument list to pass the function. If args has a single entry + * that is an object, we assume it's an old style dispatch, and + * the object will passed literally. + * + * @return {Promise} + * A promise that resolves to the result of the command. + * @throws {TypeError} + * If an unsupported reply type is received. + * @throws {WebDriverError} + * If an error is returned over the channel. + */ + send(name, args = []) { + let uuid = uuidgen.generateUUID().toString(); + // TODO(ato): Bug 1242595 + this.activeMessageId = uuid; + + return new Promise((resolve, reject) => { + let path = proxy.AsyncMessageChannel.makePath(uuid); + let cb = msg => { + this.activeMessageId = null; + + switch (msg.json.type) { + case proxy.AsyncMessageChannel.ReplyType.Ok: + case proxy.AsyncMessageChannel.ReplyType.Value: + resolve(msg.json.data); + break; + + case proxy.AsyncMessageChannel.ReplyType.Error: + let err = WebDriverError.fromJSON(msg.json.data); + reject(err); + break; + + default: + throw new TypeError( + `Unknown async response type: ${msg.json.type}`); + } + }; + + this.dialogueObserver_ = (subject, topic) => { + this.cancelAll(); + resolve(); + }; + + // start content message listener + // and install observers for global- and tab modal dialogues + this.addListener_(path, cb); + modal.addHandler(this.dialogueObserver_); + + // sendAsync is GeckoDriver#sendAsync + this.sendAsync(name, marshal(args), uuid); + }); + } + + /** + * Reply to an asynchronous request. + * + * Passing an WebDriverError prototype will cause the receiving channel + * to throw this error. + * + * Usage: + * + * let channel = proxy.AsyncMessageChannel( + * messageManager, sendAsyncMessage.bind(this)); + * + * // throws in requester: + * channel.reply(uuid, new WebDriverError()); + * + * // returns with value: + * channel.reply(uuid, "hello world!"); + * + * // returns with undefined: + * channel.reply(uuid); + * + * @param {UUID} uuid + * Unique identifier of the request. + * @param {?=} obj + * Message data to reply with. + */ + reply(uuid, obj = undefined) { + // TODO(ato): Eventually the uuid will be hidden in the dispatcher + // in listener, and passing it explicitly to this function will be + // unnecessary. + if (typeof obj == "undefined") { + this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Ok); + } else if (error.isError(obj)) { + let err = error.wrap(obj); + this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Error, err); + } else { + this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Value, obj); + } + } + + sendReply_(uuid, type, data = undefined) { + const path = proxy.AsyncMessageChannel.makePath(uuid); + + let payload; + if (data && typeof data.toJSON == "function") { + payload = data.toJSON(); + } else { + payload = data; + } + + const msg = {type: type, data: payload}; + + // here sendAsync is actually the content frame's + // sendAsyncMessage(path, message) global + this.sendAsync(path, msg); + } + + /** + * Produces a path, or a name, for the message listener handler that + * listens for a reply. + * + * @param {UUID} uuid + * Unique identifier of the channel request. + * + * @return {string} + * Path to be used for nsIMessageListener.addMessageListener. + */ + static makePath(uuid) { + return "Marionette:asyncReply:" + uuid; + } + + /** + * Abort listening for responses, remove all modal dialogue handlers, + * and cancel any ongoing requests in the listener. + */ + cancelAll() { + this.removeAllListeners_(); + modal.removeHandler(this.dialogueObserver_); + // TODO(ato): It's not ideal to have listener specific behaviour here: + this.sendAsync("cancelRequest"); + } + + addListener_(path, callback) { + let autoRemover = msg => { + this.removeListener_(path); + modal.removeHandler(this.dialogueObserver_); + callback(msg); + }; + + this.mm.addMessageListener(path, autoRemover); + this.listeners_.set(path, autoRemover); + } + + removeListener_(path) { + if (!this.listeners_.has(path)) { + return true; + } + + let l = this.listeners_.get(path); + this.mm.removeMessageListener(path, l[1]); + return this.listeners_.delete(path); + } + + removeAllListeners_() { + let ok = true; + for (let [p, cb] of this.listeners_) { + ok |= this.removeListener_(p); + } + return ok; + } +}; +proxy.AsyncMessageChannel.ReplyType = { + Ok: 0, + Value: 1, + Error: 2, +}; + +/** + * A transparent content-to-chrome RPC interface where responses are + * presented as promises. + * + * @param {nsIFrameMessageManager} frameMessageManager + * The content frame's message manager, which itself is usually an + * implementor of. + */ +proxy.toChromeAsync = function (frameMessageManager) { + let sender = new AsyncChromeSender(frameMessageManager); + return new Proxy(sender, ownPriorityGetterTrap); +}; + +/** + * Sends asynchronous RPC messages to chrome space using a frame's + * sendAsyncMessage (nsIAsyncMessageSender) function. + * + * Example on how to use from a frame content script: + * + * let sender = new AsyncChromeSender(messageManager); + * let promise = sender.send("runEmulatorCmd", "my command"); + * let rv = yield promise; + */ +this.AsyncChromeSender = class { + constructor(frameMessageManager) { + this.mm = frameMessageManager; + } + + /** + * Call registered function in chrome context. + * + * @param {string} name + * Function to call in the chrome, e.g. for "Marionette:foo", use + * "foo". + * @param {?} args + * Argument list to pass the function. Must be JSON serialisable. + * + * @return {Promise} + * A promise that resolves to the value sent back. + */ + send(name, args) { + let uuid = uuidgen.generateUUID().toString(); + + let proxy = new Promise((resolve, reject) => { + let responseListener = msg => { + if (msg.json.id != uuid) { + return; + } + + this.mm.removeMessageListener( + "Marionette:listenerResponse", responseListener); + + if ("value" in msg.json) { + resolve(msg.json.value); + } else if ("error" in msg.json) { + reject(msg.json.error); + } else { + throw new TypeError( + `Unexpected response: ${msg.name} ${JSON.stringify(msg.json)}`); + } + }; + + let msg = {arguments: marshal(args), id: uuid}; + this.mm.addMessageListener( + "Marionette:listenerResponse", responseListener); + this.mm.sendAsyncMessage("Marionette:" + name, msg); + }); + + return proxy; + } +}; + +/** + * Creates a transparent interface from the content- to the chrome context. + * + * Calls to this object will be proxied via the frame's sendSyncMessage + * (nsISyncMessageSender) function. Since the message is synchronous, + * the return value is presented as a return value. + * + * Example on how to use from a frame content script: + * + * let chrome = proxy.toChrome(sendSyncMessage.bind(this)); + * let cookie = chrome.getCookie("foo"); + * + * @param {nsISyncMessageSender} sendSyncMessageFn + * The frame message manager's sendSyncMessage function. + */ +proxy.toChrome = function (sendSyncMessageFn) { + let sender = new proxy.SyncChromeSender(sendSyncMessageFn); + return new Proxy(sender, ownPriorityGetterTrap); +}; + +/** + * The SyncChromeSender sends synchronous RPC messages to the chrome + * context, using a frame's sendSyncMessage (nsISyncMessageSender) + * function. + * + * Example on how to use from a frame content script: + * + * let sender = new SyncChromeSender(sendSyncMessage.bind(this)); + * let res = sender.send("addCookie", cookie); + */ +proxy.SyncChromeSender = class { + constructor(sendSyncMessage) { + this.sendSyncMessage_ = sendSyncMessage; + } + + send(func, args) { + let name = "Marionette:" + func.toString(); + return this.sendSyncMessage_(name, marshal(args)); + } +}; + +var marshal = function (args) { + if (args.length == 1 && typeof args[0] == "object") { + return args[0]; + } + return args; +}; |