Roll src/third_party/WebKit 9301d6f:4619053 (svn 201058:201059)
[chromium-blink-merge.git] / extensions / renderer / resources / serial_service.js
blob015a7424dd17d2a8a57461b2e303edb8da92ed4f
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 define('serial_service', [
6     'content/public/renderer/service_provider',
7     'data_receiver',
8     'data_sender',
9     'device/serial/serial.mojom',
10     'device/serial/serial_serialization.mojom',
11     'mojo/public/js/core',
12     'mojo/public/js/router',
13     'stash_client',
14 ], function(serviceProvider,
15             dataReceiver,
16             dataSender,
17             serialMojom,
18             serialization,
19             core,
20             routerModule,
21             stashClient) {
22   /**
23    * A Javascript client for the serial service and connection Mojo services.
24    *
25    * This provides a thick client around the Mojo services, exposing a JS-style
26    * interface to serial connections and information about serial devices. This
27    * converts parameters and result between the Apps serial API types and the
28    * Mojo types.
29    */
31   var service = new serialMojom.SerialService.proxyClass(
32       new routerModule.Router(
33           serviceProvider.connectToService(serialMojom.SerialService.name)));
35   function getDevices() {
36     return service.getDevices().then(function(response) {
37       return $Array.map(response.devices, function(device) {
38         var result = {path: device.path};
39         if (device.has_vendor_id)
40           result.vendorId = device.vendor_id;
41         if (device.has_product_id)
42           result.productId = device.product_id;
43         if (device.display_name)
44           result.displayName = device.display_name;
45         return result;
46       });
47     });
48   }
50   var DATA_BITS_TO_MOJO = {
51     undefined: serialMojom.DataBits.NONE,
52     'seven': serialMojom.DataBits.SEVEN,
53     'eight': serialMojom.DataBits.EIGHT,
54   };
55   var STOP_BITS_TO_MOJO = {
56     undefined: serialMojom.StopBits.NONE,
57     'one': serialMojom.StopBits.ONE,
58     'two': serialMojom.StopBits.TWO,
59   };
60   var PARITY_BIT_TO_MOJO = {
61     undefined: serialMojom.ParityBit.NONE,
62     'no': serialMojom.ParityBit.NO,
63     'odd': serialMojom.ParityBit.ODD,
64     'even': serialMojom.ParityBit.EVEN,
65   };
66   var SEND_ERROR_TO_MOJO = {
67     undefined: serialMojom.SendError.NONE,
68     'disconnected': serialMojom.SendError.DISCONNECTED,
69     'pending': serialMojom.SendError.PENDING,
70     'timeout': serialMojom.SendError.TIMEOUT,
71     'system_error': serialMojom.SendError.SYSTEM_ERROR,
72   };
73   var RECEIVE_ERROR_TO_MOJO = {
74     undefined: serialMojom.ReceiveError.NONE,
75     'disconnected': serialMojom.ReceiveError.DISCONNECTED,
76     'device_lost': serialMojom.ReceiveError.DEVICE_LOST,
77     'timeout': serialMojom.ReceiveError.TIMEOUT,
78     'break': serialMojom.ReceiveError.BREAK,
79     'frame_error': serialMojom.ReceiveError.FRAME_ERROR,
80     'overrun': serialMojom.ReceiveError.OVERRUN,
81     'buffer_overflow': serialMojom.ReceiveError.BUFFER_OVERFLOW,
82     'parity_error': serialMojom.ReceiveError.PARITY_ERROR,
83     'system_error': serialMojom.ReceiveError.SYSTEM_ERROR,
84   };
86   function invertMap(input) {
87     var output = {};
88     for (var key in input) {
89       if (key == 'undefined')
90         output[input[key]] = undefined;
91       else
92         output[input[key]] = key;
93     }
94     return output;
95   }
96   var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO);
97   var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO);
98   var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO);
99   var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO);
100   var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO);
102   function getServiceOptions(options) {
103     var out = {};
104     if (options.dataBits)
105       out.data_bits = DATA_BITS_TO_MOJO[options.dataBits];
106     if (options.stopBits)
107       out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits];
108     if (options.parityBit)
109       out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit];
110     if ('ctsFlowControl' in options) {
111       out.has_cts_flow_control = true;
112       out.cts_flow_control = options.ctsFlowControl;
113     }
114     if ('bitrate' in options)
115       out.bitrate = options.bitrate;
116     return out;
117   }
119   function convertServiceInfo(result) {
120     if (!result.info)
121       throw new Error('Failed to get ConnectionInfo.');
122     return {
123       ctsFlowControl: !!result.info.cts_flow_control,
124       bitrate: result.info.bitrate || undefined,
125       dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits],
126       stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits],
127       parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit],
128     };
129   }
131   // Update client-side options |clientOptions| from the user-provided
132   // |options|.
133   function updateClientOptions(clientOptions, options) {
134     if ('name' in options)
135       clientOptions.name = options.name;
136     if ('receiveTimeout' in options)
137       clientOptions.receiveTimeout = options.receiveTimeout;
138     if ('sendTimeout' in options)
139       clientOptions.sendTimeout = options.sendTimeout;
140     if ('bufferSize' in options)
141       clientOptions.bufferSize = options.bufferSize;
142     if ('persistent' in options)
143       clientOptions.persistent = options.persistent;
144   };
146   function Connection(connection, router, receivePipe, receiveClientPipe,
147                       sendPipe, id, options) {
148     var state = new serialization.ConnectionState();
149     state.connectionId = id;
150     updateClientOptions(state, options);
151     var receiver = new dataReceiver.DataReceiver(
152         receivePipe, receiveClientPipe, state.bufferSize,
153         serialMojom.ReceiveError.DISCONNECTED);
154     var sender = new dataSender.DataSender(sendPipe, state.bufferSize,
155                                            serialMojom.SendError.DISCONNECTED);
156     this.init_(state,
157                connection,
158                router,
159                receiver,
160                sender,
161                null,
162                serialMojom.ReceiveError.NONE);
163     connections_.set(id, this);
164     this.startReceive_();
165   }
167   // Initializes this Connection from the provided args.
168   Connection.prototype.init_ = function(state,
169                                         connection,
170                                         router,
171                                         receiver,
172                                         sender,
173                                         queuedReceiveData,
174                                         queuedReceiveError) {
175     this.state_ = state;
177     // queuedReceiveData_ or queuedReceiveError_ will store the receive result
178     // or error, respectively, if a receive completes or fails while this
179     // connection is paused. At most one of the the two may be non-null: a
180     // receive completed while paused will only set one of them, no further
181     // receives will be performed while paused and a queued result is dispatched
182     // before any further receives are initiated when unpausing.
183     if (queuedReceiveError != serialMojom.ReceiveError.NONE)
184       this.queuedReceiveError_ = {error: queuedReceiveError};
185     if (queuedReceiveData) {
186       this.queuedReceiveData_ = new ArrayBuffer(queuedReceiveData.length);
187       new Int8Array(this.queuedReceiveData_).set(queuedReceiveData);
188     }
189     this.router_ = router;
190     this.remoteConnection_ = connection;
191     this.receivePipe_ = receiver;
192     this.sendPipe_ = sender;
193     this.sendInProgress_ = false;
194   };
196   Connection.create = function(path, options) {
197     options = options || {};
198     var serviceOptions = getServiceOptions(options);
199     var pipe = core.createMessagePipe();
200     var sendPipe = core.createMessagePipe();
201     var receivePipe = core.createMessagePipe();
202     var receivePipeClient = core.createMessagePipe();
203     service.connect(path,
204                     serviceOptions,
205                     pipe.handle0,
206                     sendPipe.handle0,
207                     receivePipe.handle0,
208                     receivePipeClient.handle0);
209     var router = new routerModule.Router(pipe.handle1);
210     var connection = new serialMojom.Connection.proxyClass(router);
211     return connection.getInfo().then(convertServiceInfo).then(function(info) {
212       return Promise.all([info, allocateConnectionId()]);
213     }).catch(function(e) {
214       router.close();
215       core.close(sendPipe.handle1);
216       core.close(receivePipe.handle1);
217       core.close(receivePipeClient.handle1);
218       throw e;
219     }).then(function(results) {
220       var info = results[0];
221       var id = results[1];
222       var serialConnectionClient = new Connection(connection,
223                                                   router,
224                                                   receivePipe.handle1,
225                                                   receivePipeClient.handle1,
226                                                   sendPipe.handle1,
227                                                   id,
228                                                   options);
229       var clientInfo = serialConnectionClient.getClientInfo_();
230       for (var key in clientInfo) {
231         info[key] = clientInfo[key];
232       }
233       return {
234         connection: serialConnectionClient,
235         info: info,
236       };
237     });
238   };
240   Connection.prototype.close = function() {
241     this.router_.close();
242     this.receivePipe_.close();
243     this.sendPipe_.close();
244     clearTimeout(this.receiveTimeoutId_);
245     clearTimeout(this.sendTimeoutId_);
246     connections_.delete(this.state_.connectionId);
247     return true;
248   };
250   Connection.prototype.getClientInfo_ = function() {
251     return {
252       connectionId: this.state_.connectionId,
253       paused: this.state_.paused,
254       persistent: this.state_.persistent,
255       name: this.state_.name,
256       receiveTimeout: this.state_.receiveTimeout,
257       sendTimeout: this.state_.sendTimeout,
258       bufferSize: this.state_.bufferSize,
259     };
260   };
262   Connection.prototype.getInfo = function() {
263     var info = this.getClientInfo_();
264     return this.remoteConnection_.getInfo().then(convertServiceInfo).then(
265         function(result) {
266       for (var key in result) {
267         info[key] = result[key];
268       }
269       return info;
270     }).catch(function() {
271       return info;
272     });
273   };
275   Connection.prototype.setOptions = function(options) {
276     updateClientOptions(this.state_, options);
277     var serviceOptions = getServiceOptions(options);
278     if ($Object.keys(serviceOptions).length == 0)
279       return true;
280     return this.remoteConnection_.setOptions(serviceOptions).then(
281         function(result) {
282       return !!result.success;
283     }).catch(function() {
284       return false;
285     });
286   };
288   Connection.prototype.getControlSignals = function() {
289     return this.remoteConnection_.getControlSignals().then(function(result) {
290       if (!result.signals)
291         throw new Error('Failed to get control signals.');
292       var signals = result.signals;
293       return {
294         dcd: !!signals.dcd,
295         cts: !!signals.cts,
296         ri: !!signals.ri,
297         dsr: !!signals.dsr,
298       };
299     });
300   };
302   Connection.prototype.setControlSignals = function(signals) {
303     var controlSignals = {};
304     if ('dtr' in signals) {
305       controlSignals.has_dtr = true;
306       controlSignals.dtr = signals.dtr;
307     }
308     if ('rts' in signals) {
309       controlSignals.has_rts = true;
310       controlSignals.rts = signals.rts;
311     }
312     return this.remoteConnection_.setControlSignals(controlSignals).then(
313         function(result) {
314       return !!result.success;
315     });
316   };
318   Connection.prototype.flush = function() {
319     return this.remoteConnection_.flush().then(function(result) {
320       return !!result.success;
321     });
322   };
324   Connection.prototype.setPaused = function(paused) {
325     this.state_.paused = paused;
326     if (paused) {
327       clearTimeout(this.receiveTimeoutId_);
328       this.receiveTimeoutId_ = null;
329     } else if (!this.receiveInProgress_) {
330       this.startReceive_();
331     }
332   };
334   Connection.prototype.send = function(data) {
335     if (this.sendInProgress_)
336       return Promise.resolve({bytesSent: 0, error: 'pending'});
338     if (this.state_.sendTimeout) {
339       this.sendTimeoutId_ = setTimeout(function() {
340         this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT);
341       }.bind(this), this.state_.sendTimeout);
342     }
343     this.sendInProgress_ = true;
344     return this.sendPipe_.send(data).then(function(bytesSent) {
345       return {bytesSent: bytesSent};
346     }).catch(function(e) {
347       return {
348         bytesSent: e.bytesSent,
349         error: SEND_ERROR_FROM_MOJO[e.error],
350       };
351     }).then(function(result) {
352       if (this.sendTimeoutId_)
353         clearTimeout(this.sendTimeoutId_);
354       this.sendTimeoutId_ = null;
355       this.sendInProgress_ = false;
356       return result;
357     }.bind(this));
358   };
360   Connection.prototype.startReceive_ = function() {
361     this.receiveInProgress_ = true;
362     var receivePromise = null;
363     // If we have a queued receive result, dispatch it immediately instead of
364     // starting a new receive.
365     if (this.queuedReceiveData_) {
366       receivePromise = Promise.resolve(this.queuedReceiveData_);
367       this.queuedReceiveData_ = null;
368     } else if (this.queuedReceiveError_) {
369       receivePromise = Promise.reject(this.queuedReceiveError_);
370       this.queuedReceiveError_ = null;
371     } else {
372       receivePromise = this.receivePipe_.receive();
373     }
374     receivePromise.then(this.onDataReceived_.bind(this)).catch(
375         this.onReceiveError_.bind(this));
376     this.startReceiveTimeoutTimer_();
377   };
379   Connection.prototype.onDataReceived_ = function(data) {
380     this.startReceiveTimeoutTimer_();
381     this.receiveInProgress_ = false;
382     if (this.state_.paused) {
383       this.queuedReceiveData_ = data;
384       return;
385     }
386     if (this.onData) {
387       this.onData(data);
388     }
389     if (!this.state_.paused) {
390       this.startReceive_();
391     }
392   };
394   Connection.prototype.onReceiveError_ = function(e) {
395     clearTimeout(this.receiveTimeoutId_);
396     this.receiveInProgress_ = false;
397     if (this.state_.paused) {
398       this.queuedReceiveError_ = e;
399       return;
400     }
401     var error = e.error;
402     this.state_.paused = true;
403     if (this.onError)
404       this.onError(RECEIVE_ERROR_FROM_MOJO[error]);
405   };
407   Connection.prototype.startReceiveTimeoutTimer_ = function() {
408     clearTimeout(this.receiveTimeoutId_);
409     if (this.state_.receiveTimeout && !this.state_.paused) {
410       this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this),
411                                           this.state_.receiveTimeout);
412     }
413   };
415   Connection.prototype.onReceiveTimeout_ = function() {
416     if (this.onError)
417       this.onError('timeout');
418     this.startReceiveTimeoutTimer_();
419   };
421   Connection.prototype.serialize = function() {
422     connections_.delete(this.state_.connectionId);
423     this.onData = null;
424     this.onError = null;
425     var handle = this.router_.connector_.handle_;
426     this.router_.connector_.handle_ = null;
427     this.router_.close();
428     clearTimeout(this.receiveTimeoutId_);
429     clearTimeout(this.sendTimeoutId_);
431     // Serializing receivePipe_ will cancel an in-progress receive, which would
432     // pause the connection, so save it ahead of time.
433     var paused = this.state_.paused;
434     return Promise.all([
435         this.receivePipe_.serialize(),
436         this.sendPipe_.serialize(),
437     ]).then(function(serializedComponents) {
438       var queuedReceiveError = serialMojom.ReceiveError.NONE;
439       if (this.queuedReceiveError_)
440         queuedReceiveError = this.queuedReceiveError_.error;
441       this.state_.paused = paused;
442       var serialized = new serialization.SerializedConnection();
443       serialized.state = this.state_;
444       serialized.queuedReceiveError = queuedReceiveError;
445       serialized.queuedReceiveData =
446           this.queuedReceiveData_ ? new Int8Array(this.queuedReceiveData_) :
447                                     null;
448       serialized.connection = handle;
449       serialized.receiver = serializedComponents[0];
450       serialized.sender = serializedComponents[1];
451       return serialized;
452     }.bind(this));
453   };
455   Connection.deserialize = function(serialized) {
456     var serialConnection = $Object.create(Connection.prototype);
457     var router = new routerModule.Router(serialized.connection);
458     var connection = new serialMojom.Connection.proxyClass(router);
459     var receiver = dataReceiver.DataReceiver.deserialize(serialized.receiver);
460     var sender = dataSender.DataSender.deserialize(serialized.sender);
462     // Ensure that paused and persistent are booleans.
463     serialized.state.paused = !!serialized.state.paused;
464     serialized.state.persistent = !!serialized.state.persistent;
465     serialConnection.init_(serialized.state,
466                            connection,
467                            router,
468                            receiver,
469                            sender,
470                            serialized.queuedReceiveData,
471                            serialized.queuedReceiveError);
472     serialConnection.awaitingResume_ = true;
473     var connectionId = serialized.state.connectionId;
474     connections_.set(connectionId, serialConnection);
475     if (connectionId >= nextConnectionId_)
476       nextConnectionId_ = connectionId + 1;
477     return serialConnection;
478   };
480   // Resume receives on a deserialized connection.
481   Connection.prototype.resumeReceives = function() {
482     if (!this.awaitingResume_)
483       return;
484     this.awaitingResume_ = false;
485     if (!this.state_.paused)
486       this.startReceive_();
487   };
489   // All accesses to connections_ and nextConnectionId_ other than those
490   // involved in deserialization should ensure that
491   // connectionDeserializationComplete_ has resolved first.
492   var connectionDeserializationComplete_ = stashClient.retrieve(
493       'serial', serialization.SerializedConnection).then(function(decoded) {
494     if (!decoded)
495       return;
496     return Promise.all($Array.map(decoded, Connection.deserialize));
497   });
499   // The map of connection ID to connection object.
500   var connections_ = new Map();
502   // The next connection ID to be allocated.
503   var nextConnectionId_ = 0;
505   function getConnections() {
506     return connectionDeserializationComplete_.then(function() {
507       return new Map(connections_);
508     });
509   }
511   function getConnection(id) {
512     return getConnections().then(function(connections) {
513       if (!connections.has(id))
514         throw new Error('Serial connection not found.');
515       return connections.get(id);
516     });
517   }
519   function allocateConnectionId() {
520     return connectionDeserializationComplete_.then(function() {
521       return nextConnectionId_++;
522     });
523   }
525   stashClient.registerClient(
526       'serial', serialization.SerializedConnection, function() {
527     return connectionDeserializationComplete_.then(function() {
528       var clientPromises = [];
529       for (var connection of connections_.values()) {
530         if (connection.state_.persistent)
531           clientPromises.push(connection.serialize());
532         else
533           connection.close();
534       }
535       return Promise.all($Array.map(clientPromises, function(promise) {
536         return promise.then(function(serialization) {
537           return {
538             serialization: serialization,
539             monitorHandles: !serialization.paused,
540           };
541         });
542       }));
543     });
544   });
546   return {
547     getDevices: getDevices,
548     createConnection: Connection.create,
549     getConnection: getConnection,
550     getConnections: getConnections,
551     // For testing.
552     Connection: Connection,
553   };