dispatcher.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 {interfaces: Ci, utils: Cu} = Components;
  6. Cu.import("resource://gre/modules/Log.jsm");
  7. Cu.import("resource://gre/modules/Preferences.jsm");
  8. Cu.import("resource://gre/modules/Task.jsm");
  9. Cu.import("chrome://marionette/content/assert.js");
  10. Cu.import("chrome://marionette/content/driver.js");
  11. Cu.import("chrome://marionette/content/error.js");
  12. Cu.import("chrome://marionette/content/message.js");
  13. this.EXPORTED_SYMBOLS = ["Dispatcher"];
  14. const PROTOCOL_VERSION = 3;
  15. const logger = Log.repository.getLogger("Marionette");
  16. /**
  17. * Manages a Marionette connection, and dispatches packets received to
  18. * their correct destinations.
  19. *
  20. * @param {number} connId
  21. * Unique identifier of the connection this dispatcher should handle.
  22. * @param {DebuggerTransport} transport
  23. * Debugger transport connection to the client.
  24. * @param {function(): GeckoDriver} driverFactory
  25. * A factory function that produces a GeckoDriver.
  26. */
  27. this.Dispatcher = function (connId, transport, driverFactory) {
  28. this.connId = connId;
  29. this.conn = transport;
  30. // transport hooks are Dispatcher#onPacket
  31. // and Dispatcher#onClosed
  32. this.conn.hooks = this;
  33. // callback for when connection is closed
  34. this.onclose = null;
  35. // last received/sent message ID
  36. this.lastId = 0;
  37. this.driver = driverFactory();
  38. // lookup of commands sent by server to client by message ID
  39. this.commands_ = new Map();
  40. };
  41. /**
  42. * Debugger transport callback that cleans up
  43. * after a connection is closed.
  44. */
  45. Dispatcher.prototype.onClosed = function (reason) {
  46. this.driver.deleteSession();
  47. if (this.onclose) {
  48. this.onclose(this);
  49. }
  50. };
  51. /**
  52. * Callback that receives data packets from the client.
  53. *
  54. * If the message is a Response, we look up the command previously issued
  55. * to the client and run its callback, if any. In case of a Command,
  56. * the corresponding is executed.
  57. *
  58. * @param {Array.<number, number, ?, ?>} data
  59. * A four element array where the elements, in sequence, signifies
  60. * message type, message ID, method name or error, and parameters
  61. * or result.
  62. */
  63. Dispatcher.prototype.onPacket = function (data) {
  64. let msg = Message.fromMsg(data);
  65. msg.origin = MessageOrigin.Client;
  66. this.log_(msg);
  67. if (msg instanceof Response) {
  68. let cmd = this.commands_.get(msg.id);
  69. this.commands_.delete(msg.id);
  70. cmd.onresponse(msg);
  71. } else if (msg instanceof Command) {
  72. this.lastId = msg.id;
  73. this.execute(msg);
  74. }
  75. };
  76. /**
  77. * Executes a WebDriver command and sends back a response when it has
  78. * finished executing.
  79. *
  80. * Commands implemented in GeckoDriver and registered in its
  81. * {@code GeckoDriver.commands} attribute. The return values from
  82. * commands are expected to be Promises. If the resolved value of said
  83. * promise is not an object, the response body will be wrapped in an object
  84. * under a "value" field.
  85. *
  86. * If the command implementation sends the response itself by calling
  87. * {@code resp.send()}, the response is guaranteed to not be sent twice.
  88. *
  89. * Errors thrown in commands are marshaled and sent back, and if they
  90. * are not WebDriverError instances, they are additionally propagated and
  91. * reported to {@code Components.utils.reportError}.
  92. *
  93. * @param {Command} cmd
  94. * The requested command to execute.
  95. */
  96. Dispatcher.prototype.execute = function (cmd) {
  97. let resp = new Response(cmd.id, this.send.bind(this));
  98. let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
  99. let sendError = resp.sendError.bind(resp);
  100. let req = Task.spawn(function*() {
  101. let fn = this.driver.commands[cmd.name];
  102. if (typeof fn == "undefined") {
  103. throw new UnknownCommandError(cmd.name);
  104. }
  105. if (cmd.name !== "newSession") {
  106. assert.session(this.driver);
  107. }
  108. let rv = yield fn.bind(this.driver)(cmd, resp);
  109. if (typeof rv != "undefined") {
  110. if (typeof rv != "object") {
  111. resp.body = {value: rv};
  112. } else {
  113. resp.body = rv;
  114. }
  115. }
  116. }.bind(this));
  117. req.then(sendResponse, sendError).catch(error.report);
  118. };
  119. Dispatcher.prototype.sendError = function (err, cmdId) {
  120. let resp = new Response(cmdId, this.send.bind(this));
  121. resp.sendError(err);
  122. };
  123. // Convenience methods:
  124. /**
  125. * When a client connects we send across a JSON Object defining the
  126. * protocol level.
  127. *
  128. * This is the only message sent by Marionette that does not follow
  129. * the regular message format.
  130. */
  131. Dispatcher.prototype.sayHello = function() {
  132. let whatHo = {
  133. applicationType: "gecko",
  134. marionetteProtocol: PROTOCOL_VERSION,
  135. };
  136. this.sendRaw(whatHo);
  137. };
  138. /**
  139. * Delegates message to client based on the provided {@code cmdId}.
  140. * The message is sent over the debugger transport socket.
  141. *
  142. * The command ID is a unique identifier assigned to the client's request
  143. * that is used to distinguish the asynchronous responses.
  144. *
  145. * Whilst responses to commands are synchronous and must be sent in the
  146. * correct order.
  147. *
  148. * @param {Command,Response} msg
  149. * The command or response to send.
  150. */
  151. Dispatcher.prototype.send = function (msg) {
  152. msg.origin = MessageOrigin.Server;
  153. if (msg instanceof Command) {
  154. this.commands_.set(msg.id, msg);
  155. this.sendToEmulator(msg);
  156. } else if (msg instanceof Response) {
  157. this.sendToClient(msg);
  158. }
  159. };
  160. // Low-level methods:
  161. /**
  162. * Send given response to the client over the debugger transport socket.
  163. *
  164. * @param {Response} resp
  165. * The response to send back to the client.
  166. */
  167. Dispatcher.prototype.sendToClient = function (resp) {
  168. this.driver.responseCompleted();
  169. this.sendMessage(resp);
  170. };
  171. /**
  172. * Marshal message to the Marionette message format and send it.
  173. *
  174. * @param {Command,Response} msg
  175. * The message to send.
  176. */
  177. Dispatcher.prototype.sendMessage = function (msg) {
  178. this.log_(msg);
  179. let payload = msg.toMsg();
  180. this.sendRaw(payload);
  181. };
  182. /**
  183. * Send the given payload over the debugger transport socket to the
  184. * connected client.
  185. *
  186. * @param {Object} payload
  187. * The payload to ship.
  188. */
  189. Dispatcher.prototype.sendRaw = function (payload) {
  190. this.conn.send(payload);
  191. };
  192. Dispatcher.prototype.log_ = function (msg) {
  193. let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
  194. let s = JSON.stringify(msg.toMsg());
  195. logger.trace(this.connId + a + s);
  196. };