Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / extensions / renderer / resources / serial_service.js
blobe5d909d175a83c30bb9c7e87c1eb041ca434ee0d
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     'system_error': serialMojom.ReceiveError.SYSTEM_ERROR,
79   };
81   function invertMap(input) {
82     var output = {};
83     for (var key in input) {
84       if (key == 'undefined')
85         output[input[key]] = undefined;
86       else
87         output[input[key]] = key;
88     }
89     return output;
90   }
91   var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO);
92   var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO);
93   var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO);
94   var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO);
95   var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO);
97   function getServiceOptions(options) {
98     var out = {};
99     if (options.dataBits)
100       out.data_bits = DATA_BITS_TO_MOJO[options.dataBits];
101     if (options.stopBits)
102       out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits];
103     if (options.parityBit)
104       out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit];
105     if ('ctsFlowControl' in options) {
106       out.has_cts_flow_control = true;
107       out.cts_flow_control = options.ctsFlowControl;
108     }
109     if ('bitrate' in options)
110       out.bitrate = options.bitrate;
111     return out;
112   }
114   function convertServiceInfo(result) {
115     if (!result.info)
116       throw new Error('Failed to get ConnectionInfo.');
117     return {
118       ctsFlowControl: !!result.info.cts_flow_control,
119       bitrate: result.info.bitrate || undefined,
120       dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits],
121       stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits],
122       parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit],
123     };
124   }
126   // Update client-side options |clientOptions| from the user-provided
127   // |options|.
128   function updateClientOptions(clientOptions, options) {
129     if ('name' in options)
130       clientOptions.name = options.name;
131     if ('receiveTimeout' in options)
132       clientOptions.receiveTimeout = options.receiveTimeout;
133     if ('sendTimeout' in options)
134       clientOptions.sendTimeout = options.sendTimeout;
135     if ('bufferSize' in options)
136       clientOptions.bufferSize = options.bufferSize;
137     if ('persistent' in options)
138       clientOptions.persistent = options.persistent;
139   };
141   function Connection(connection, router, receivePipe, receiveClientPipe,
142                       sendPipe, id, options) {
143     var state = new serialization.ConnectionState();
144     state.connectionId = id;
145     updateClientOptions(state, options);
146     var receiver = new dataReceiver.DataReceiver(
147         receivePipe, receiveClientPipe, state.bufferSize,
148         serialMojom.ReceiveError.DISCONNECTED);
149     var sender = new dataSender.DataSender(sendPipe, state.bufferSize,
150                                            serialMojom.SendError.DISCONNECTED);
151     this.init_(state,
152                connection,
153                router,
154                receiver,
155                sender,
156                null,
157                serialMojom.ReceiveError.NONE);
158     connections_.set(id, this);
159     this.startReceive_();
160   }
162   // Initializes this Connection from the provided args.
163   Connection.prototype.init_ = function(state,
164                                         connection,
165                                         router,
166                                         receiver,
167                                         sender,
168                                         queuedReceiveData,
169                                         queuedReceiveError) {
170     this.state_ = state;
172     // queuedReceiveData_ or queuedReceiveError_ will store the receive result
173     // or error, respectively, if a receive completes or fails while this
174     // connection is paused. At most one of the the two may be non-null: a
175     // receive completed while paused will only set one of them, no further
176     // receives will be performed while paused and a queued result is dispatched
177     // before any further receives are initiated when unpausing.
178     if (queuedReceiveError != serialMojom.ReceiveError.NONE)
179       this.queuedReceiveError_ = {error: queuedReceiveError};
180     if (queuedReceiveData) {
181       this.queuedReceiveData_ = new ArrayBuffer(queuedReceiveData.length);
182       new Int8Array(this.queuedReceiveData_).set(queuedReceiveData);
183     }
184     this.router_ = router;
185     this.remoteConnection_ = connection;
186     this.receivePipe_ = receiver;
187     this.sendPipe_ = sender;
188     this.sendInProgress_ = false;
189   };
191   Connection.create = function(path, options) {
192     options = options || {};
193     var serviceOptions = getServiceOptions(options);
194     var pipe = core.createMessagePipe();
195     var sendPipe = core.createMessagePipe();
196     var receivePipe = core.createMessagePipe();
197     var receivePipeClient = core.createMessagePipe();
198     service.connect(path,
199                     serviceOptions,
200                     pipe.handle0,
201                     sendPipe.handle0,
202                     receivePipe.handle0,
203                     receivePipeClient.handle0);
204     var router = new routerModule.Router(pipe.handle1);
205     var connection = new serialMojom.Connection.proxyClass(router);
206     return connection.getInfo().then(convertServiceInfo).then(function(info) {
207       return Promise.all([info, allocateConnectionId()]);
208     }).catch(function(e) {
209       router.close();
210       core.close(sendPipe.handle1);
211       core.close(receivePipe.handle1);
212       core.close(receivePipeClient.handle1);
213       throw e;
214     }).then(function(results) {
215       var info = results[0];
216       var id = results[1];
217       var serialConnectionClient = new Connection(connection,
218                                                   router,
219                                                   receivePipe.handle1,
220                                                   receivePipeClient.handle1,
221                                                   sendPipe.handle1,
222                                                   id,
223                                                   options);
224       var clientInfo = serialConnectionClient.getClientInfo_();
225       for (var key in clientInfo) {
226         info[key] = clientInfo[key];
227       }
228       return {
229         connection: serialConnectionClient,
230         info: info,
231       };
232     });
233   };
235   Connection.prototype.close = function() {
236     this.router_.close();
237     this.receivePipe_.close();
238     this.sendPipe_.close();
239     clearTimeout(this.receiveTimeoutId_);
240     clearTimeout(this.sendTimeoutId_);
241     connections_.delete(this.state_.connectionId);
242     return true;
243   };
245   Connection.prototype.getClientInfo_ = function() {
246     return {
247       connectionId: this.state_.connectionId,
248       paused: this.state_.paused,
249       persistent: this.state_.persistent,
250       name: this.state_.name,
251       receiveTimeout: this.state_.receiveTimeout,
252       sendTimeout: this.state_.sendTimeout,
253       bufferSize: this.state_.bufferSize,
254     };
255   };
257   Connection.prototype.getInfo = function() {
258     var info = this.getClientInfo_();
259     return this.remoteConnection_.getInfo().then(convertServiceInfo).then(
260         function(result) {
261       for (var key in result) {
262         info[key] = result[key];
263       }
264       return info;
265     }).catch(function() {
266       return info;
267     });
268   };
270   Connection.prototype.setOptions = function(options) {
271     updateClientOptions(this.state_, options);
272     var serviceOptions = getServiceOptions(options);
273     if ($Object.keys(serviceOptions).length == 0)
274       return true;
275     return this.remoteConnection_.setOptions(serviceOptions).then(
276         function(result) {
277       return !!result.success;
278     }).catch(function() {
279       return false;
280     });
281   };
283   Connection.prototype.getControlSignals = function() {
284     return this.remoteConnection_.getControlSignals().then(function(result) {
285       if (!result.signals)
286         throw new Error('Failed to get control signals.');
287       var signals = result.signals;
288       return {
289         dcd: !!signals.dcd,
290         cts: !!signals.cts,
291         ri: !!signals.ri,
292         dsr: !!signals.dsr,
293       };
294     });
295   };
297   Connection.prototype.setControlSignals = function(signals) {
298     var controlSignals = {};
299     if ('dtr' in signals) {
300       controlSignals.has_dtr = true;
301       controlSignals.dtr = signals.dtr;
302     }
303     if ('rts' in signals) {
304       controlSignals.has_rts = true;
305       controlSignals.rts = signals.rts;
306     }
307     return this.remoteConnection_.setControlSignals(controlSignals).then(
308         function(result) {
309       return !!result.success;
310     });
311   };
313   Connection.prototype.flush = function() {
314     return this.remoteConnection_.flush().then(function(result) {
315       return !!result.success;
316     });
317   };
319   Connection.prototype.setPaused = function(paused) {
320     this.state_.paused = paused;
321     if (paused) {
322       clearTimeout(this.receiveTimeoutId_);
323       this.receiveTimeoutId_ = null;
324     } else if (!this.receiveInProgress_) {
325       this.startReceive_();
326     }
327   };
329   Connection.prototype.send = function(data) {
330     if (this.sendInProgress_)
331       return Promise.resolve({bytesSent: 0, error: 'pending'});
333     if (this.state_.sendTimeout) {
334       this.sendTimeoutId_ = setTimeout(function() {
335         this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT);
336       }.bind(this), this.state_.sendTimeout);
337     }
338     this.sendInProgress_ = true;
339     return this.sendPipe_.send(data).then(function(bytesSent) {
340       return {bytesSent: bytesSent};
341     }).catch(function(e) {
342       return {
343         bytesSent: e.bytesSent,
344         error: SEND_ERROR_FROM_MOJO[e.error],
345       };
346     }).then(function(result) {
347       if (this.sendTimeoutId_)
348         clearTimeout(this.sendTimeoutId_);
349       this.sendTimeoutId_ = null;
350       this.sendInProgress_ = false;
351       return result;
352     }.bind(this));
353   };
355   Connection.prototype.startReceive_ = function() {
356     this.receiveInProgress_ = true;
357     var receivePromise = null;
358     // If we have a queued receive result, dispatch it immediately instead of
359     // starting a new receive.
360     if (this.queuedReceiveData_) {
361       receivePromise = Promise.resolve(this.queuedReceiveData_);
362       this.queuedReceiveData_ = null;
363     } else if (this.queuedReceiveError_) {
364       receivePromise = Promise.reject(this.queuedReceiveError_);
365       this.queuedReceiveError_ = null;
366     } else {
367       receivePromise = this.receivePipe_.receive();
368     }
369     receivePromise.then(this.onDataReceived_.bind(this)).catch(
370         this.onReceiveError_.bind(this));
371     this.startReceiveTimeoutTimer_();
372   };
374   Connection.prototype.onDataReceived_ = function(data) {
375     this.startReceiveTimeoutTimer_();
376     this.receiveInProgress_ = false;
377     if (this.state_.paused) {
378       this.queuedReceiveData_ = data;
379       return;
380     }
381     if (this.onData) {
382       this.onData(data);
383     }
384     if (!this.state_.paused) {
385       this.startReceive_();
386     }
387   };
389   Connection.prototype.onReceiveError_ = function(e) {
390     clearTimeout(this.receiveTimeoutId_);
391     this.receiveInProgress_ = false;
392     if (this.state_.paused) {
393       this.queuedReceiveError_ = e;
394       return;
395     }
396     var error = e.error;
397     this.state_.paused = true;
398     if (this.onError)
399       this.onError(RECEIVE_ERROR_FROM_MOJO[error]);
400   };
402   Connection.prototype.startReceiveTimeoutTimer_ = function() {
403     clearTimeout(this.receiveTimeoutId_);
404     if (this.state_.receiveTimeout && !this.state_.paused) {
405       this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this),
406                                           this.state_.receiveTimeout);
407     }
408   };
410   Connection.prototype.onReceiveTimeout_ = function() {
411     if (this.onError)
412       this.onError('timeout');
413     this.startReceiveTimeoutTimer_();
414   };
416   Connection.prototype.serialize = function() {
417     connections_.delete(this.state_.connectionId);
418     this.onData = null;
419     this.onError = null;
420     var handle = this.router_.connector_.handle_;
421     this.router_.connector_.handle_ = null;
422     this.router_.close();
423     clearTimeout(this.receiveTimeoutId_);
424     clearTimeout(this.sendTimeoutId_);
426     // Serializing receivePipe_ will cancel an in-progress receive, which would
427     // pause the connection, so save it ahead of time.
428     var paused = this.state_.paused;
429     return Promise.all([
430         this.receivePipe_.serialize(),
431         this.sendPipe_.serialize(),
432     ]).then(function(serializedComponents) {
433       var queuedReceiveError = serialMojom.ReceiveError.NONE;
434       if (this.queuedReceiveError_)
435         queuedReceiveError = this.queuedReceiveError_.error;
436       this.state_.paused = paused;
437       var serialized = new serialization.SerializedConnection();
438       serialized.state = this.state_;
439       serialized.queuedReceiveError = queuedReceiveError;
440       serialized.queuedReceiveData =
441           this.queuedReceiveData_ ? new Int8Array(this.queuedReceiveData_) :
442                                     null;
443       serialized.connection = handle;
444       serialized.receiver = serializedComponents[0];
445       serialized.sender = serializedComponents[1];
446       return serialized;
447     }.bind(this));
448   };
450   Connection.deserialize = function(serialized) {
451     var serialConnection = $Object.create(Connection.prototype);
452     var router = new routerModule.Router(serialized.connection);
453     var connection = new serialMojom.Connection.proxyClass(router);
454     var receiver = dataReceiver.DataReceiver.deserialize(serialized.receiver);
455     var sender = dataSender.DataSender.deserialize(serialized.sender);
457     // Ensure that paused and persistent are booleans.
458     serialized.state.paused = !!serialized.state.paused;
459     serialized.state.persistent = !!serialized.state.persistent;
460     serialConnection.init_(serialized.state,
461                            connection,
462                            router,
463                            receiver,
464                            sender,
465                            serialized.queuedReceiveData,
466                            serialized.queuedReceiveError);
467     serialConnection.awaitingResume_ = true;
468     var connectionId = serialized.state.connectionId;
469     connections_.set(connectionId, serialConnection);
470     if (connectionId >= nextConnectionId_)
471       nextConnectionId_ = connectionId + 1;
472     return serialConnection;
473   };
475   // Resume receives on a deserialized connection.
476   Connection.prototype.resumeReceives = function() {
477     if (!this.awaitingResume_)
478       return;
479     this.awaitingResume_ = false;
480     if (!this.state_.paused)
481       this.startReceive_();
482   };
484   // All accesses to connections_ and nextConnectionId_ other than those
485   // involved in deserialization should ensure that
486   // connectionDeserializationComplete_ has resolved first.
487   var connectionDeserializationComplete_ = stashClient.retrieve(
488       'serial', serialization.SerializedConnection).then(function(decoded) {
489     if (!decoded)
490       return;
491     return Promise.all($Array.map(decoded, Connection.deserialize));
492   });
494   // The map of connection ID to connection object.
495   var connections_ = new Map();
497   // The next connection ID to be allocated.
498   var nextConnectionId_ = 0;
500   function getConnections() {
501     return connectionDeserializationComplete_.then(function() {
502       return new Map(connections_);
503     });
504   }
506   function getConnection(id) {
507     return getConnections().then(function(connections) {
508       if (!connections.has(id))
509         throw new Error('Serial connection not found.');
510       return connections.get(id);
511     });
512   }
514   function allocateConnectionId() {
515     return connectionDeserializationComplete_.then(function() {
516       return nextConnectionId_++;
517     });
518   }
520   stashClient.registerClient(
521       'serial', serialization.SerializedConnection, function() {
522     return connectionDeserializationComplete_.then(function() {
523       var clientPromises = [];
524       for (var connection of connections_.values()) {
525         if (connection.state_.persistent)
526           clientPromises.push(connection.serialize());
527         else
528           connection.close();
529       }
530       return Promise.all($Array.map(clientPromises, function(promise) {
531         return promise.then(function(serialization) {
532           return {
533             serialization: serialization,
534             monitorHandles: !serialization.paused,
535           };
536         });
537       }));
538     });
539   });
541   return {
542     getDevices: getDevices,
543     createConnection: Connection.create,
544     getConnection: getConnection,
545     getConnections: getConnections,
546     // For testing.
547     Connection: Connection,
548   };