proxy.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  6. Cu.import("chrome://marionette/content/error.js");
  7. Cu.import("chrome://marionette/content/modal.js");
  8. this.EXPORTED_SYMBOLS = ["proxy"];
  9. const uuidgen = Cc["@mozilla.org/uuid-generator;1"]
  10. .getService(Ci.nsIUUIDGenerator);
  11. // Proxy handler that traps requests to get a property. Will prioritise
  12. // properties that exist on the object's own prototype.
  13. var ownPriorityGetterTrap = {
  14. get: (obj, prop) => {
  15. if (obj.hasOwnProperty(prop)) {
  16. return obj[prop];
  17. }
  18. return (...args) => obj.send(prop, args);
  19. }
  20. };
  21. this.proxy = {};
  22. /**
  23. * Creates a transparent interface between the chrome- and content
  24. * contexts.
  25. *
  26. * Calls to this object will be proxied via the message manager to a
  27. * content frame script, and responses are returend as promises.
  28. *
  29. * The argument sequence is serialised and passed as an array, unless it
  30. * consists of a single object type that isn't null, in which case it's
  31. * passed literally. The latter specialisation is temporary to achieve
  32. * backwards compatibility with listener.js.
  33. *
  34. * @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
  35. * Closure function returning the current message manager.
  36. * @param {function(string, Object, number)} sendAsyncFn
  37. * Callback for sending async messages.
  38. */
  39. proxy.toListener = function (mmFn, sendAsyncFn) {
  40. let sender = new proxy.AsyncMessageChannel(mmFn, sendAsyncFn);
  41. return new Proxy(sender, ownPriorityGetterTrap);
  42. };
  43. /**
  44. * Provides a transparent interface between chrome- and content space.
  45. *
  46. * The AsyncMessageChannel is an abstraction of the message manager
  47. * IPC architecture allowing calls to be made to any registered message
  48. * listener in Marionette. The {@code #send(...)} method returns a promise
  49. * that gets resolved when the message handler calls {@code .reply(...)}.
  50. */
  51. proxy.AsyncMessageChannel = class {
  52. constructor(mmFn, sendAsyncFn) {
  53. this.sendAsync = sendAsyncFn;
  54. // TODO(ato): Bug 1242595
  55. this.activeMessageId = null;
  56. this.mmFn_ = mmFn;
  57. this.listeners_ = new Map();
  58. this.dialogueObserver_ = null;
  59. }
  60. get mm() {
  61. return this.mmFn_();
  62. }
  63. /**
  64. * Send a message across the channel. The name of the function to
  65. * call must be registered as a message listener.
  66. *
  67. * Usage:
  68. *
  69. * let channel = new AsyncMessageChannel(
  70. * messageManager, sendAsyncMessage.bind(this));
  71. * let rv = yield channel.send("remoteFunction", ["argument"]);
  72. *
  73. * @param {string} name
  74. * Function to call in the listener, e.g. for the message listener
  75. * "Marionette:foo8", use "foo".
  76. * @param {Array.<?>=} args
  77. * Argument list to pass the function. If args has a single entry
  78. * that is an object, we assume it's an old style dispatch, and
  79. * the object will passed literally.
  80. *
  81. * @return {Promise}
  82. * A promise that resolves to the result of the command.
  83. * @throws {TypeError}
  84. * If an unsupported reply type is received.
  85. * @throws {WebDriverError}
  86. * If an error is returned over the channel.
  87. */
  88. send(name, args = []) {
  89. let uuid = uuidgen.generateUUID().toString();
  90. // TODO(ato): Bug 1242595
  91. this.activeMessageId = uuid;
  92. return new Promise((resolve, reject) => {
  93. let path = proxy.AsyncMessageChannel.makePath(uuid);
  94. let cb = msg => {
  95. this.activeMessageId = null;
  96. switch (msg.json.type) {
  97. case proxy.AsyncMessageChannel.ReplyType.Ok:
  98. case proxy.AsyncMessageChannel.ReplyType.Value:
  99. resolve(msg.json.data);
  100. break;
  101. case proxy.AsyncMessageChannel.ReplyType.Error:
  102. let err = WebDriverError.fromJSON(msg.json.data);
  103. reject(err);
  104. break;
  105. default:
  106. throw new TypeError(
  107. `Unknown async response type: ${msg.json.type}`);
  108. }
  109. };
  110. this.dialogueObserver_ = (subject, topic) => {
  111. this.cancelAll();
  112. resolve();
  113. };
  114. // start content message listener
  115. // and install observers for global- and tab modal dialogues
  116. this.addListener_(path, cb);
  117. modal.addHandler(this.dialogueObserver_);
  118. // sendAsync is GeckoDriver#sendAsync
  119. this.sendAsync(name, marshal(args), uuid);
  120. });
  121. }
  122. /**
  123. * Reply to an asynchronous request.
  124. *
  125. * Passing an WebDriverError prototype will cause the receiving channel
  126. * to throw this error.
  127. *
  128. * Usage:
  129. *
  130. * let channel = proxy.AsyncMessageChannel(
  131. * messageManager, sendAsyncMessage.bind(this));
  132. *
  133. * // throws in requester:
  134. * channel.reply(uuid, new WebDriverError());
  135. *
  136. * // returns with value:
  137. * channel.reply(uuid, "hello world!");
  138. *
  139. * // returns with undefined:
  140. * channel.reply(uuid);
  141. *
  142. * @param {UUID} uuid
  143. * Unique identifier of the request.
  144. * @param {?=} obj
  145. * Message data to reply with.
  146. */
  147. reply(uuid, obj = undefined) {
  148. // TODO(ato): Eventually the uuid will be hidden in the dispatcher
  149. // in listener, and passing it explicitly to this function will be
  150. // unnecessary.
  151. if (typeof obj == "undefined") {
  152. this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Ok);
  153. } else if (error.isError(obj)) {
  154. let err = error.wrap(obj);
  155. this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Error, err);
  156. } else {
  157. this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Value, obj);
  158. }
  159. }
  160. sendReply_(uuid, type, data = undefined) {
  161. const path = proxy.AsyncMessageChannel.makePath(uuid);
  162. let payload;
  163. if (data && typeof data.toJSON == "function") {
  164. payload = data.toJSON();
  165. } else {
  166. payload = data;
  167. }
  168. const msg = {type: type, data: payload};
  169. // here sendAsync is actually the content frame's
  170. // sendAsyncMessage(path, message) global
  171. this.sendAsync(path, msg);
  172. }
  173. /**
  174. * Produces a path, or a name, for the message listener handler that
  175. * listens for a reply.
  176. *
  177. * @param {UUID} uuid
  178. * Unique identifier of the channel request.
  179. *
  180. * @return {string}
  181. * Path to be used for nsIMessageListener.addMessageListener.
  182. */
  183. static makePath(uuid) {
  184. return "Marionette:asyncReply:" + uuid;
  185. }
  186. /**
  187. * Abort listening for responses, remove all modal dialogue handlers,
  188. * and cancel any ongoing requests in the listener.
  189. */
  190. cancelAll() {
  191. this.removeAllListeners_();
  192. modal.removeHandler(this.dialogueObserver_);
  193. // TODO(ato): It's not ideal to have listener specific behaviour here:
  194. this.sendAsync("cancelRequest");
  195. }
  196. addListener_(path, callback) {
  197. let autoRemover = msg => {
  198. this.removeListener_(path);
  199. modal.removeHandler(this.dialogueObserver_);
  200. callback(msg);
  201. };
  202. this.mm.addMessageListener(path, autoRemover);
  203. this.listeners_.set(path, autoRemover);
  204. }
  205. removeListener_(path) {
  206. if (!this.listeners_.has(path)) {
  207. return true;
  208. }
  209. let l = this.listeners_.get(path);
  210. this.mm.removeMessageListener(path, l[1]);
  211. return this.listeners_.delete(path);
  212. }
  213. removeAllListeners_() {
  214. let ok = true;
  215. for (let [p, cb] of this.listeners_) {
  216. ok |= this.removeListener_(p);
  217. }
  218. return ok;
  219. }
  220. };
  221. proxy.AsyncMessageChannel.ReplyType = {
  222. Ok: 0,
  223. Value: 1,
  224. Error: 2,
  225. };
  226. /**
  227. * A transparent content-to-chrome RPC interface where responses are
  228. * presented as promises.
  229. *
  230. * @param {nsIFrameMessageManager} frameMessageManager
  231. * The content frame's message manager, which itself is usually an
  232. * implementor of.
  233. */
  234. proxy.toChromeAsync = function (frameMessageManager) {
  235. let sender = new AsyncChromeSender(frameMessageManager);
  236. return new Proxy(sender, ownPriorityGetterTrap);
  237. };
  238. /**
  239. * Sends asynchronous RPC messages to chrome space using a frame's
  240. * sendAsyncMessage (nsIAsyncMessageSender) function.
  241. *
  242. * Example on how to use from a frame content script:
  243. *
  244. * let sender = new AsyncChromeSender(messageManager);
  245. * let promise = sender.send("runEmulatorCmd", "my command");
  246. * let rv = yield promise;
  247. */
  248. this.AsyncChromeSender = class {
  249. constructor(frameMessageManager) {
  250. this.mm = frameMessageManager;
  251. }
  252. /**
  253. * Call registered function in chrome context.
  254. *
  255. * @param {string} name
  256. * Function to call in the chrome, e.g. for "Marionette:foo", use
  257. * "foo".
  258. * @param {?} args
  259. * Argument list to pass the function. Must be JSON serialisable.
  260. *
  261. * @return {Promise}
  262. * A promise that resolves to the value sent back.
  263. */
  264. send(name, args) {
  265. let uuid = uuidgen.generateUUID().toString();
  266. let proxy = new Promise((resolve, reject) => {
  267. let responseListener = msg => {
  268. if (msg.json.id != uuid) {
  269. return;
  270. }
  271. this.mm.removeMessageListener(
  272. "Marionette:listenerResponse", responseListener);
  273. if ("value" in msg.json) {
  274. resolve(msg.json.value);
  275. } else if ("error" in msg.json) {
  276. reject(msg.json.error);
  277. } else {
  278. throw new TypeError(
  279. `Unexpected response: ${msg.name} ${JSON.stringify(msg.json)}`);
  280. }
  281. };
  282. let msg = {arguments: marshal(args), id: uuid};
  283. this.mm.addMessageListener(
  284. "Marionette:listenerResponse", responseListener);
  285. this.mm.sendAsyncMessage("Marionette:" + name, msg);
  286. });
  287. return proxy;
  288. }
  289. };
  290. /**
  291. * Creates a transparent interface from the content- to the chrome context.
  292. *
  293. * Calls to this object will be proxied via the frame's sendSyncMessage
  294. * (nsISyncMessageSender) function. Since the message is synchronous,
  295. * the return value is presented as a return value.
  296. *
  297. * Example on how to use from a frame content script:
  298. *
  299. * let chrome = proxy.toChrome(sendSyncMessage.bind(this));
  300. * let cookie = chrome.getCookie("foo");
  301. *
  302. * @param {nsISyncMessageSender} sendSyncMessageFn
  303. * The frame message manager's sendSyncMessage function.
  304. */
  305. proxy.toChrome = function (sendSyncMessageFn) {
  306. let sender = new proxy.SyncChromeSender(sendSyncMessageFn);
  307. return new Proxy(sender, ownPriorityGetterTrap);
  308. };
  309. /**
  310. * The SyncChromeSender sends synchronous RPC messages to the chrome
  311. * context, using a frame's sendSyncMessage (nsISyncMessageSender)
  312. * function.
  313. *
  314. * Example on how to use from a frame content script:
  315. *
  316. * let sender = new SyncChromeSender(sendSyncMessage.bind(this));
  317. * let res = sender.send("addCookie", cookie);
  318. */
  319. proxy.SyncChromeSender = class {
  320. constructor(sendSyncMessage) {
  321. this.sendSyncMessage_ = sendSyncMessage;
  322. }
  323. send(func, args) {
  324. let name = "Marionette:" + func.toString();
  325. return this.sendSyncMessage_(name, marshal(args));
  326. }
  327. };
  328. var marshal = function (args) {
  329. if (args.length == 1 && typeof args[0] == "object") {
  330. return args[0];
  331. }
  332. return args;
  333. };