2015-04-28 16:25:37 +03:00
|
|
|
|
/* 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";
|
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2016-01-29 15:57:46 +03:00
|
|
|
|
Cu.import("chrome://marionette/content/error.js");
|
2015-04-28 16:25:37 +03:00
|
|
|
|
Cu.import("chrome://marionette/content/modal.js");
|
|
|
|
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = ["proxy"];
|
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
const uuidgen = Cc["@mozilla.org/uuid-generator;1"]
|
|
|
|
|
.getService(Ci.nsIUUIDGenerator);
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2015-04-28 16:25:37 +03:00
|
|
|
|
this.proxy = {};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a transparent interface between the chrome- and content
|
2015-11-13 16:35:22 +03:00
|
|
|
|
* contexts.
|
2015-04-28 16:25:37 +03:00
|
|
|
|
*
|
2016-01-21 22:27:23 +03:00
|
|
|
|
* Calls to this object will be proxied via the message manager to a
|
|
|
|
|
* content frame script, and responses are returend as promises.
|
2015-04-28 16:25:37 +03:00
|
|
|
|
*
|
|
|
|
|
* 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
|
2016-01-21 22:27:23 +03:00
|
|
|
|
* Closure function returning the current message manager.
|
2015-04-28 16:25:37 +03:00
|
|
|
|
* @param {function(string, Object, number)} sendAsyncFn
|
2016-01-21 22:27:23 +03:00
|
|
|
|
* Callback for sending async messages.
|
2015-04-28 16:25:37 +03:00
|
|
|
|
*/
|
|
|
|
|
proxy.toListener = function(mmFn, sendAsyncFn) {
|
2016-01-29 15:57:46 +03:00
|
|
|
|
let sender = new proxy.AsyncMessageChannel(mmFn, sendAsyncFn);
|
2015-11-13 16:35:22 +03:00
|
|
|
|
return new Proxy(sender, ownPriorityGetterTrap);
|
2015-04-28 16:25:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
2016-01-29 15:57:46 +03:00
|
|
|
|
* Provides a transparent interface between chrome- and content space.
|
2015-04-28 16:25:37 +03:00
|
|
|
|
*
|
2016-01-29 15:57:46 +03:00
|
|
|
|
* 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(...)}.
|
2015-04-28 16:25:37 +03:00
|
|
|
|
*/
|
2016-01-29 15:57:46 +03:00
|
|
|
|
proxy.AsyncMessageChannel = class {
|
2015-11-13 16:35:22 +03:00
|
|
|
|
constructor(mmFn, sendAsyncFn) {
|
|
|
|
|
this.sendAsync = sendAsyncFn;
|
2016-01-21 22:27:23 +03:00
|
|
|
|
// TODO(ato): Bug 1242595
|
|
|
|
|
this.activeMessageId = null;
|
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
this.mmFn_ = mmFn;
|
2016-01-21 22:27:23 +03:00
|
|
|
|
this.listeners_ = new Map();
|
|
|
|
|
this.dialogueObserver_ = null;
|
2015-11-13 16:35:22 +03:00
|
|
|
|
}
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
get mm() {
|
|
|
|
|
return this.mmFn_();
|
|
|
|
|
}
|
2015-09-25 17:33:11 +03:00
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
/**
|
2016-01-29 15:57:46 +03:00
|
|
|
|
* 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"]);
|
2015-11-13 16:35:22 +03:00
|
|
|
|
*
|
|
|
|
|
* @param {string} name
|
2016-01-21 22:27:23 +03:00
|
|
|
|
* Function to call in the listener, e.g. for the message listener
|
|
|
|
|
* "Marionette:foo8", use "foo".
|
|
|
|
|
* @param {Array.<?>=} args
|
2015-11-13 16:35:22 +03:00
|
|
|
|
* 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.
|
2016-01-29 15:57:46 +03:00
|
|
|
|
* @throws {TypeError}
|
|
|
|
|
* If an unsupported reply type is received.
|
|
|
|
|
* @throws {WebDriverError}
|
|
|
|
|
* If an error is returned over the channel.
|
2015-11-13 16:35:22 +03:00
|
|
|
|
*/
|
2016-01-21 22:27:23 +03:00
|
|
|
|
send(name, args = []) {
|
|
|
|
|
let uuid = uuidgen.generateUUID().toString();
|
|
|
|
|
// TODO(ato): Bug 1242595
|
|
|
|
|
this.activeMessageId = uuid;
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2016-01-29 15:57:46 +03:00
|
|
|
|
let path = proxy.AsyncMessageChannel.makePath(uuid);
|
2016-01-21 22:27:23 +03:00
|
|
|
|
let cb = msg => {
|
|
|
|
|
this.activeMessageId = null;
|
2016-01-29 15:57:46 +03:00
|
|
|
|
|
|
|
|
|
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 = error.fromJson(msg.json.data);
|
|
|
|
|
reject(err);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw new TypeError(
|
|
|
|
|
`Unknown async response type: ${msg.json.type}`);
|
2016-01-21 22:27:23 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
2016-01-29 15:57:46 +03:00
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
this.dialogueObserver_ = (subject, topic) => {
|
|
|
|
|
this.cancelAll();
|
|
|
|
|
resolve();
|
|
|
|
|
};
|
2015-11-13 16:35:22 +03:00
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
// start content message listener
|
|
|
|
|
// and install observers for global- and tab modal dialogues
|
|
|
|
|
this.addListener_(path, cb);
|
|
|
|
|
modal.addHandler(this.dialogueObserver_);
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2016-01-29 15:57:46 +03:00
|
|
|
|
// sendAsync is GeckoDriver#sendAsync
|
2016-01-21 22:27:23 +03:00
|
|
|
|
this.sendAsync(name, marshal(args), uuid);
|
|
|
|
|
});
|
|
|
|
|
}
|
2015-11-13 16:35:22 +03:00
|
|
|
|
|
2016-01-29 15:57:46 +03:00
|
|
|
|
/**
|
|
|
|
|
* 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)) {
|
2016-02-03 21:44:55 +03:00
|
|
|
|
let err = error.wrap(obj);
|
|
|
|
|
let serr = error.toJson(err);
|
2016-01-29 15:57:46 +03:00
|
|
|
|
this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Error, serr);
|
|
|
|
|
} else {
|
|
|
|
|
this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Value, obj);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendReply_(uuid, type, data = undefined) {
|
|
|
|
|
let path = proxy.AsyncMessageChannel.makePath(uuid);
|
|
|
|
|
let msg = {type: type, data: data};
|
|
|
|
|
// 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.
|
|
|
|
|
*/
|
2016-01-21 22:27:23 +03:00
|
|
|
|
cancelAll() {
|
|
|
|
|
this.removeAllListeners_();
|
|
|
|
|
modal.removeHandler(this.dialogueObserver_);
|
2016-01-29 15:57:46 +03:00
|
|
|
|
// TODO(ato): It's not ideal to have listener specific behaviour here:
|
2016-01-21 22:27:23 +03:00
|
|
|
|
this.sendAsync("cancelRequest");
|
|
|
|
|
}
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
addListener_(path, callback) {
|
|
|
|
|
let autoRemover = msg => {
|
|
|
|
|
this.removeListener_(path);
|
|
|
|
|
modal.removeHandler(this.dialogueObserver_);
|
|
|
|
|
callback(msg);
|
|
|
|
|
};
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
this.mm.addMessageListener(path, autoRemover);
|
|
|
|
|
this.listeners_.set(path, autoRemover);
|
|
|
|
|
}
|
2015-11-13 16:35:22 +03:00
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
removeListener_(path) {
|
|
|
|
|
let l = this.listeners_.get(path);
|
|
|
|
|
this.mm.removeMessageListener(path, l[1]);
|
|
|
|
|
return this.listeners_.delete(path);
|
|
|
|
|
}
|
2015-11-13 16:35:22 +03:00
|
|
|
|
|
2016-01-21 22:27:23 +03:00
|
|
|
|
removeAllListeners_() {
|
|
|
|
|
let ok = true;
|
|
|
|
|
for (let [p, cb] of this.listeners_) {
|
|
|
|
|
ok |= this.removeListener_(p);
|
|
|
|
|
}
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
2016-01-29 15:57:46 +03:00
|
|
|
|
};
|
|
|
|
|
proxy.AsyncMessageChannel.ReplyType = {
|
|
|
|
|
Ok: 0,
|
|
|
|
|
Value: 1,
|
|
|
|
|
Error: 2,
|
2015-11-13 16:35:22 +03:00
|
|
|
|
};
|
2015-04-28 16:25:37 +03:00
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
2016-01-21 22:27:23 +03:00
|
|
|
|
let sender = new proxy.SyncChromeSender(sendSyncMessageFn);
|
2015-11-13 16:35:22 +03:00
|
|
|
|
return new Proxy(sender, ownPriorityGetterTrap);
|
2015-04-28 16:25:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
2015-11-13 16:35:22 +03:00
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
*/
|
2016-01-21 22:27:23 +03:00
|
|
|
|
proxy.SyncChromeSender = class {
|
2015-11-13 16:35:22 +03:00
|
|
|
|
constructor(sendSyncMessage) {
|
|
|
|
|
this.sendSyncMessage_ = sendSyncMessage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
send(func, args) {
|
|
|
|
|
let name = "Marionette:" + func;
|
|
|
|
|
return this.sendSyncMessage_(name, marshal(args));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var marshal = function(args) {
|
|
|
|
|
if (args.length == 1 && typeof args[0] == "object") {
|
|
|
|
|
return args[0];
|
|
|
|
|
}
|
|
|
|
|
return args;
|
|
|
|
|
};
|