summaryrefslogtreecommitdiffstats
path: root/testing/marionette/dispatcher.js
blob: 1f09ef8bf8960f28e29cfe660ab74937710a443c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/* 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 {interfaces: Ci, utils: Cu} = Components;

Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");

Cu.import("chrome://marionette/content/assert.js");
Cu.import("chrome://marionette/content/driver.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/message.js");

this.EXPORTED_SYMBOLS = ["Dispatcher"];

const PROTOCOL_VERSION = 3;

const logger = Log.repository.getLogger("Marionette");

/**
 * Manages a Marionette connection, and dispatches packets received to
 * their correct destinations.
 *
 * @param {number} connId
 *     Unique identifier of the connection this dispatcher should handle.
 * @param {DebuggerTransport} transport
 *     Debugger transport connection to the client.
 * @param {function(): GeckoDriver} driverFactory
 *     A factory function that produces a GeckoDriver.
 */
this.Dispatcher = function (connId, transport, driverFactory) {
  this.connId = connId;
  this.conn = transport;

  // transport hooks are Dispatcher#onPacket
  // and Dispatcher#onClosed
  this.conn.hooks = this;

  // callback for when connection is closed
  this.onclose = null;

  // last received/sent message ID
  this.lastId = 0;

  this.driver = driverFactory();

  // lookup of commands sent by server to client by message ID
  this.commands_ = new Map();
};

/**
 * Debugger transport callback that cleans up
 * after a connection is closed.
 */
Dispatcher.prototype.onClosed = function (reason) {
  this.driver.deleteSession();
  if (this.onclose) {
    this.onclose(this);
  }
};

/**
 * Callback that receives data packets from the client.
 *
 * If the message is a Response, we look up the command previously issued
 * to the client and run its callback, if any.  In case of a Command,
 * the corresponding is executed.
 *
 * @param {Array.<number, number, ?, ?>} data
 *     A four element array where the elements, in sequence, signifies
 *     message type, message ID, method name or error, and parameters
 *     or result.
 */
Dispatcher.prototype.onPacket = function (data) {
  let msg = Message.fromMsg(data);
  msg.origin = MessageOrigin.Client;
  this.log_(msg);

  if (msg instanceof Response) {
    let cmd = this.commands_.get(msg.id);
    this.commands_.delete(msg.id);
    cmd.onresponse(msg);
  } else if (msg instanceof Command) {
    this.lastId = msg.id;
    this.execute(msg);
  }
};

/**
 * Executes a WebDriver command and sends back a response when it has
 * finished executing.
 *
 * Commands implemented in GeckoDriver and registered in its
 * {@code GeckoDriver.commands} attribute.  The return values from
 * commands are expected to be Promises.  If the resolved value of said
 * promise is not an object, the response body will be wrapped in an object
 * under a "value" field.
 *
 * If the command implementation sends the response itself by calling
 * {@code resp.send()}, the response is guaranteed to not be sent twice.
 *
 * Errors thrown in commands are marshaled and sent back, and if they
 * are not WebDriverError instances, they are additionally propagated and
 * reported to {@code Components.utils.reportError}.
 *
 * @param {Command} cmd
 *     The requested command to execute.
 */
Dispatcher.prototype.execute = function (cmd) {
  let resp = new Response(cmd.id, this.send.bind(this));
  let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
  let sendError = resp.sendError.bind(resp);

  let req = Task.spawn(function*() {
    let fn = this.driver.commands[cmd.name];
    if (typeof fn == "undefined") {
      throw new UnknownCommandError(cmd.name);
    }

    if (cmd.name !== "newSession") {
      assert.session(this.driver);
    }

    let rv = yield fn.bind(this.driver)(cmd, resp);

    if (typeof rv != "undefined") {
      if (typeof rv != "object") {
        resp.body = {value: rv};
      } else {
        resp.body = rv;
      }
    }
  }.bind(this));

  req.then(sendResponse, sendError).catch(error.report);
};

Dispatcher.prototype.sendError = function (err, cmdId) {
  let resp = new Response(cmdId, this.send.bind(this));
  resp.sendError(err);
};

// Convenience methods:

/**
 * When a client connects we send across a JSON Object defining the
 * protocol level.
 *
 * This is the only message sent by Marionette that does not follow
 * the regular message format.
 */
Dispatcher.prototype.sayHello = function() {
  let whatHo = {
    applicationType: "gecko",
    marionetteProtocol: PROTOCOL_VERSION,
  };
  this.sendRaw(whatHo);
};


/**
 * Delegates message to client based on the provided  {@code cmdId}.
 * The message is sent over the debugger transport socket.
 *
 * The command ID is a unique identifier assigned to the client's request
 * that is used to distinguish the asynchronous responses.
 *
 * Whilst responses to commands are synchronous and must be sent in the
 * correct order.
 *
 * @param {Command,Response} msg
 *     The command or response to send.
 */
Dispatcher.prototype.send = function (msg) {
  msg.origin = MessageOrigin.Server;
  if (msg instanceof Command) {
    this.commands_.set(msg.id, msg);
    this.sendToEmulator(msg);
  } else if (msg instanceof Response) {
    this.sendToClient(msg);
  }
};

// Low-level methods:

/**
 * Send given response to the client over the debugger transport socket.
 *
 * @param {Response} resp
 *     The response to send back to the client.
 */
Dispatcher.prototype.sendToClient = function (resp) {
  this.driver.responseCompleted();
  this.sendMessage(resp);
};

/**
 * Marshal message to the Marionette message format and send it.
 *
 * @param {Command,Response} msg
 *     The message to send.
 */
Dispatcher.prototype.sendMessage = function (msg) {
  this.log_(msg);
  let payload = msg.toMsg();
  this.sendRaw(payload);
};

/**
 * Send the given payload over the debugger transport socket to the
 * connected client.
 *
 * @param {Object} payload
 *     The payload to ship.
 */
Dispatcher.prototype.sendRaw = function (payload) {
  this.conn.send(payload);
};

Dispatcher.prototype.log_ = function (msg) {
  let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
  let s = JSON.stringify(msg.toMsg());
  logger.trace(this.connId + a + s);
};