123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- /* 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;
- };
|