summaryrefslogtreecommitdiff
path: root/chromium/extensions/renderer/resources/serial_service.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/extensions/renderer/resources/serial_service.js')
-rw-r--r--chromium/extensions/renderer/resources/serial_service.js554
1 files changed, 554 insertions, 0 deletions
diff --git a/chromium/extensions/renderer/resources/serial_service.js b/chromium/extensions/renderer/resources/serial_service.js
new file mode 100644
index 00000000000..26b990b227e
--- /dev/null
+++ b/chromium/extensions/renderer/resources/serial_service.js
@@ -0,0 +1,554 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+define('serial_service', [
+ 'content/public/renderer/frame_service_registry',
+ 'data_receiver',
+ 'data_sender',
+ 'device/serial/serial.mojom',
+ 'device/serial/serial_serialization.mojom',
+ 'mojo/public/js/core',
+ 'mojo/public/js/router',
+ 'stash_client',
+], function(serviceProvider,
+ dataReceiver,
+ dataSender,
+ serialMojom,
+ serialization,
+ core,
+ routerModule,
+ stashClient) {
+ /**
+ * A Javascript client for the serial service and connection Mojo services.
+ *
+ * This provides a thick client around the Mojo services, exposing a JS-style
+ * interface to serial connections and information about serial devices. This
+ * converts parameters and result between the Apps serial API types and the
+ * Mojo types.
+ */
+
+ var service = new serialMojom.SerialService.proxyClass(
+ new routerModule.Router(
+ serviceProvider.connectToService(serialMojom.SerialService.name)));
+
+ function getDevices() {
+ return service.getDevices().then(function(response) {
+ return $Array.map(response.devices, function(device) {
+ var result = {path: device.path};
+ if (device.has_vendor_id)
+ result.vendorId = device.vendor_id;
+ if (device.has_product_id)
+ result.productId = device.product_id;
+ if (device.display_name)
+ result.displayName = device.display_name;
+ return result;
+ });
+ });
+ }
+
+ var DATA_BITS_TO_MOJO = {
+ undefined: serialMojom.DataBits.NONE,
+ 'seven': serialMojom.DataBits.SEVEN,
+ 'eight': serialMojom.DataBits.EIGHT,
+ };
+ var STOP_BITS_TO_MOJO = {
+ undefined: serialMojom.StopBits.NONE,
+ 'one': serialMojom.StopBits.ONE,
+ 'two': serialMojom.StopBits.TWO,
+ };
+ var PARITY_BIT_TO_MOJO = {
+ undefined: serialMojom.ParityBit.NONE,
+ 'no': serialMojom.ParityBit.NO,
+ 'odd': serialMojom.ParityBit.ODD,
+ 'even': serialMojom.ParityBit.EVEN,
+ };
+ var SEND_ERROR_TO_MOJO = {
+ undefined: serialMojom.SendError.NONE,
+ 'disconnected': serialMojom.SendError.DISCONNECTED,
+ 'pending': serialMojom.SendError.PENDING,
+ 'timeout': serialMojom.SendError.TIMEOUT,
+ 'system_error': serialMojom.SendError.SYSTEM_ERROR,
+ };
+ var RECEIVE_ERROR_TO_MOJO = {
+ undefined: serialMojom.ReceiveError.NONE,
+ 'disconnected': serialMojom.ReceiveError.DISCONNECTED,
+ 'device_lost': serialMojom.ReceiveError.DEVICE_LOST,
+ 'timeout': serialMojom.ReceiveError.TIMEOUT,
+ 'break': serialMojom.ReceiveError.BREAK,
+ 'frame_error': serialMojom.ReceiveError.FRAME_ERROR,
+ 'overrun': serialMojom.ReceiveError.OVERRUN,
+ 'buffer_overflow': serialMojom.ReceiveError.BUFFER_OVERFLOW,
+ 'parity_error': serialMojom.ReceiveError.PARITY_ERROR,
+ 'system_error': serialMojom.ReceiveError.SYSTEM_ERROR,
+ };
+
+ function invertMap(input) {
+ var output = {};
+ for (var key in input) {
+ if (key == 'undefined')
+ output[input[key]] = undefined;
+ else
+ output[input[key]] = key;
+ }
+ return output;
+ }
+ var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO);
+ var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO);
+ var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO);
+ var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO);
+ var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO);
+
+ function getServiceOptions(options) {
+ var out = {};
+ if (options.dataBits)
+ out.data_bits = DATA_BITS_TO_MOJO[options.dataBits];
+ if (options.stopBits)
+ out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits];
+ if (options.parityBit)
+ out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit];
+ if ('ctsFlowControl' in options) {
+ out.has_cts_flow_control = true;
+ out.cts_flow_control = options.ctsFlowControl;
+ }
+ if ('bitrate' in options)
+ out.bitrate = options.bitrate;
+ return out;
+ }
+
+ function convertServiceInfo(result) {
+ if (!result.info)
+ throw new Error('Failed to get ConnectionInfo.');
+ return {
+ ctsFlowControl: !!result.info.cts_flow_control,
+ bitrate: result.info.bitrate || undefined,
+ dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits],
+ stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits],
+ parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit],
+ };
+ }
+
+ // Update client-side options |clientOptions| from the user-provided
+ // |options|.
+ function updateClientOptions(clientOptions, options) {
+ if ('name' in options)
+ clientOptions.name = options.name;
+ if ('receiveTimeout' in options)
+ clientOptions.receiveTimeout = options.receiveTimeout;
+ if ('sendTimeout' in options)
+ clientOptions.sendTimeout = options.sendTimeout;
+ if ('bufferSize' in options)
+ clientOptions.bufferSize = options.bufferSize;
+ if ('persistent' in options)
+ clientOptions.persistent = options.persistent;
+ };
+
+ function Connection(connection, router, receivePipe, receiveClientPipe,
+ sendPipe, id, options) {
+ var state = new serialization.ConnectionState();
+ state.connectionId = id;
+ updateClientOptions(state, options);
+ var receiver = new dataReceiver.DataReceiver(
+ receivePipe, receiveClientPipe, state.bufferSize,
+ serialMojom.ReceiveError.DISCONNECTED);
+ var sender = new dataSender.DataSender(sendPipe, state.bufferSize,
+ serialMojom.SendError.DISCONNECTED);
+ this.init_(state,
+ connection,
+ router,
+ receiver,
+ sender,
+ null,
+ serialMojom.ReceiveError.NONE);
+ connections_.set(id, this);
+ this.startReceive_();
+ }
+
+ // Initializes this Connection from the provided args.
+ Connection.prototype.init_ = function(state,
+ connection,
+ router,
+ receiver,
+ sender,
+ queuedReceiveData,
+ queuedReceiveError) {
+ this.state_ = state;
+
+ // queuedReceiveData_ or queuedReceiveError_ will store the receive result
+ // or error, respectively, if a receive completes or fails while this
+ // connection is paused. At most one of the the two may be non-null: a
+ // receive completed while paused will only set one of them, no further
+ // receives will be performed while paused and a queued result is dispatched
+ // before any further receives are initiated when unpausing.
+ if (queuedReceiveError != serialMojom.ReceiveError.NONE)
+ this.queuedReceiveError_ = {error: queuedReceiveError};
+ if (queuedReceiveData) {
+ this.queuedReceiveData_ = new ArrayBuffer(queuedReceiveData.length);
+ new Int8Array(this.queuedReceiveData_).set(queuedReceiveData);
+ }
+ this.router_ = router;
+ this.remoteConnection_ = connection;
+ this.receivePipe_ = receiver;
+ this.sendPipe_ = sender;
+ this.sendInProgress_ = false;
+ };
+
+ Connection.create = function(path, options) {
+ options = options || {};
+ var serviceOptions = getServiceOptions(options);
+ var pipe = core.createMessagePipe();
+ var sendPipe = core.createMessagePipe();
+ var receivePipe = core.createMessagePipe();
+ var receivePipeClient = core.createMessagePipe();
+ service.connect(path,
+ serviceOptions,
+ pipe.handle0,
+ sendPipe.handle0,
+ receivePipe.handle0,
+ receivePipeClient.handle0);
+ var router = new routerModule.Router(pipe.handle1);
+ var connection = new serialMojom.Connection.proxyClass(router);
+ return connection.getInfo().then(convertServiceInfo).then(function(info) {
+ return Promise.all([info, allocateConnectionId()]);
+ }).catch(function(e) {
+ router.close();
+ core.close(sendPipe.handle1);
+ core.close(receivePipe.handle1);
+ core.close(receivePipeClient.handle1);
+ throw e;
+ }).then(function(results) {
+ var info = results[0];
+ var id = results[1];
+ var serialConnectionClient = new Connection(connection,
+ router,
+ receivePipe.handle1,
+ receivePipeClient.handle1,
+ sendPipe.handle1,
+ id,
+ options);
+ var clientInfo = serialConnectionClient.getClientInfo_();
+ for (var key in clientInfo) {
+ info[key] = clientInfo[key];
+ }
+ return {
+ connection: serialConnectionClient,
+ info: info,
+ };
+ });
+ };
+
+ Connection.prototype.close = function() {
+ this.router_.close();
+ this.receivePipe_.close();
+ this.sendPipe_.close();
+ clearTimeout(this.receiveTimeoutId_);
+ clearTimeout(this.sendTimeoutId_);
+ connections_.delete(this.state_.connectionId);
+ return true;
+ };
+
+ Connection.prototype.getClientInfo_ = function() {
+ return {
+ connectionId: this.state_.connectionId,
+ paused: this.state_.paused,
+ persistent: this.state_.persistent,
+ name: this.state_.name,
+ receiveTimeout: this.state_.receiveTimeout,
+ sendTimeout: this.state_.sendTimeout,
+ bufferSize: this.state_.bufferSize,
+ };
+ };
+
+ Connection.prototype.getInfo = function() {
+ var info = this.getClientInfo_();
+ return this.remoteConnection_.getInfo().then(convertServiceInfo).then(
+ function(result) {
+ for (var key in result) {
+ info[key] = result[key];
+ }
+ return info;
+ }).catch(function() {
+ return info;
+ });
+ };
+
+ Connection.prototype.setOptions = function(options) {
+ updateClientOptions(this.state_, options);
+ var serviceOptions = getServiceOptions(options);
+ if ($Object.keys(serviceOptions).length == 0)
+ return true;
+ return this.remoteConnection_.setOptions(serviceOptions).then(
+ function(result) {
+ return !!result.success;
+ }).catch(function() {
+ return false;
+ });
+ };
+
+ Connection.prototype.getControlSignals = function() {
+ return this.remoteConnection_.getControlSignals().then(function(result) {
+ if (!result.signals)
+ throw new Error('Failed to get control signals.');
+ var signals = result.signals;
+ return {
+ dcd: !!signals.dcd,
+ cts: !!signals.cts,
+ ri: !!signals.ri,
+ dsr: !!signals.dsr,
+ };
+ });
+ };
+
+ Connection.prototype.setControlSignals = function(signals) {
+ var controlSignals = {};
+ if ('dtr' in signals) {
+ controlSignals.has_dtr = true;
+ controlSignals.dtr = signals.dtr;
+ }
+ if ('rts' in signals) {
+ controlSignals.has_rts = true;
+ controlSignals.rts = signals.rts;
+ }
+ return this.remoteConnection_.setControlSignals(controlSignals).then(
+ function(result) {
+ return !!result.success;
+ });
+ };
+
+ Connection.prototype.flush = function() {
+ return this.remoteConnection_.flush().then(function(result) {
+ return !!result.success;
+ });
+ };
+
+ Connection.prototype.setPaused = function(paused) {
+ this.state_.paused = paused;
+ if (paused) {
+ clearTimeout(this.receiveTimeoutId_);
+ this.receiveTimeoutId_ = null;
+ } else if (!this.receiveInProgress_) {
+ this.startReceive_();
+ }
+ };
+
+ Connection.prototype.send = function(data) {
+ if (this.sendInProgress_)
+ return Promise.resolve({bytesSent: 0, error: 'pending'});
+
+ if (this.state_.sendTimeout) {
+ this.sendTimeoutId_ = setTimeout(function() {
+ this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT);
+ }.bind(this), this.state_.sendTimeout);
+ }
+ this.sendInProgress_ = true;
+ return this.sendPipe_.send(data).then(function(bytesSent) {
+ return {bytesSent: bytesSent};
+ }).catch(function(e) {
+ return {
+ bytesSent: e.bytesSent,
+ error: SEND_ERROR_FROM_MOJO[e.error],
+ };
+ }).then(function(result) {
+ if (this.sendTimeoutId_)
+ clearTimeout(this.sendTimeoutId_);
+ this.sendTimeoutId_ = null;
+ this.sendInProgress_ = false;
+ return result;
+ }.bind(this));
+ };
+
+ Connection.prototype.startReceive_ = function() {
+ this.receiveInProgress_ = true;
+ var receivePromise = null;
+ // If we have a queued receive result, dispatch it immediately instead of
+ // starting a new receive.
+ if (this.queuedReceiveData_) {
+ receivePromise = Promise.resolve(this.queuedReceiveData_);
+ this.queuedReceiveData_ = null;
+ } else if (this.queuedReceiveError_) {
+ receivePromise = Promise.reject(this.queuedReceiveError_);
+ this.queuedReceiveError_ = null;
+ } else {
+ receivePromise = this.receivePipe_.receive();
+ }
+ receivePromise.then(this.onDataReceived_.bind(this)).catch(
+ this.onReceiveError_.bind(this));
+ this.startReceiveTimeoutTimer_();
+ };
+
+ Connection.prototype.onDataReceived_ = function(data) {
+ this.startReceiveTimeoutTimer_();
+ this.receiveInProgress_ = false;
+ if (this.state_.paused) {
+ this.queuedReceiveData_ = data;
+ return;
+ }
+ if (this.onData) {
+ this.onData(data);
+ }
+ if (!this.state_.paused) {
+ this.startReceive_();
+ }
+ };
+
+ Connection.prototype.onReceiveError_ = function(e) {
+ clearTimeout(this.receiveTimeoutId_);
+ this.receiveInProgress_ = false;
+ if (this.state_.paused) {
+ this.queuedReceiveError_ = e;
+ return;
+ }
+ var error = e.error;
+ this.state_.paused = true;
+ if (this.onError)
+ this.onError(RECEIVE_ERROR_FROM_MOJO[error]);
+ };
+
+ Connection.prototype.startReceiveTimeoutTimer_ = function() {
+ clearTimeout(this.receiveTimeoutId_);
+ if (this.state_.receiveTimeout && !this.state_.paused) {
+ this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this),
+ this.state_.receiveTimeout);
+ }
+ };
+
+ Connection.prototype.onReceiveTimeout_ = function() {
+ if (this.onError)
+ this.onError('timeout');
+ this.startReceiveTimeoutTimer_();
+ };
+
+ Connection.prototype.serialize = function() {
+ connections_.delete(this.state_.connectionId);
+ this.onData = null;
+ this.onError = null;
+ var handle = this.router_.connector_.handle_;
+ this.router_.connector_.handle_ = null;
+ this.router_.close();
+ clearTimeout(this.receiveTimeoutId_);
+ clearTimeout(this.sendTimeoutId_);
+
+ // Serializing receivePipe_ will cancel an in-progress receive, which would
+ // pause the connection, so save it ahead of time.
+ var paused = this.state_.paused;
+ return Promise.all([
+ this.receivePipe_.serialize(),
+ this.sendPipe_.serialize(),
+ ]).then(function(serializedComponents) {
+ var queuedReceiveError = serialMojom.ReceiveError.NONE;
+ if (this.queuedReceiveError_)
+ queuedReceiveError = this.queuedReceiveError_.error;
+ this.state_.paused = paused;
+ var serialized = new serialization.SerializedConnection();
+ serialized.state = this.state_;
+ serialized.queuedReceiveError = queuedReceiveError;
+ serialized.queuedReceiveData =
+ this.queuedReceiveData_ ? new Int8Array(this.queuedReceiveData_) :
+ null;
+ serialized.connection = handle;
+ serialized.receiver = serializedComponents[0];
+ serialized.sender = serializedComponents[1];
+ return serialized;
+ }.bind(this));
+ };
+
+ Connection.deserialize = function(serialized) {
+ var serialConnection = $Object.create(Connection.prototype);
+ var router = new routerModule.Router(serialized.connection);
+ var connection = new serialMojom.Connection.proxyClass(router);
+ var receiver = dataReceiver.DataReceiver.deserialize(serialized.receiver);
+ var sender = dataSender.DataSender.deserialize(serialized.sender);
+
+ // Ensure that paused and persistent are booleans.
+ serialized.state.paused = !!serialized.state.paused;
+ serialized.state.persistent = !!serialized.state.persistent;
+ serialConnection.init_(serialized.state,
+ connection,
+ router,
+ receiver,
+ sender,
+ serialized.queuedReceiveData,
+ serialized.queuedReceiveError);
+ serialConnection.awaitingResume_ = true;
+ var connectionId = serialized.state.connectionId;
+ connections_.set(connectionId, serialConnection);
+ if (connectionId >= nextConnectionId_)
+ nextConnectionId_ = connectionId + 1;
+ return serialConnection;
+ };
+
+ // Resume receives on a deserialized connection.
+ Connection.prototype.resumeReceives = function() {
+ if (!this.awaitingResume_)
+ return;
+ this.awaitingResume_ = false;
+ if (!this.state_.paused)
+ this.startReceive_();
+ };
+
+ // All accesses to connections_ and nextConnectionId_ other than those
+ // involved in deserialization should ensure that
+ // connectionDeserializationComplete_ has resolved first.
+ var connectionDeserializationComplete_ = stashClient.retrieve(
+ 'serial', serialization.SerializedConnection).then(function(decoded) {
+ if (!decoded)
+ return;
+ return Promise.all($Array.map(decoded, Connection.deserialize));
+ });
+
+ // The map of connection ID to connection object.
+ var connections_ = new Map();
+
+ // The next connection ID to be allocated.
+ var nextConnectionId_ = 0;
+
+ function getConnections() {
+ return connectionDeserializationComplete_.then(function() {
+ return new Map(connections_);
+ });
+ }
+
+ function getConnection(id) {
+ return getConnections().then(function(connections) {
+ if (!connections.has(id))
+ throw new Error('Serial connection not found.');
+ return connections.get(id);
+ });
+ }
+
+ function allocateConnectionId() {
+ return connectionDeserializationComplete_.then(function() {
+ return nextConnectionId_++;
+ });
+ }
+
+ stashClient.registerClient(
+ 'serial', serialization.SerializedConnection, function() {
+ return connectionDeserializationComplete_.then(function() {
+ var clientPromises = [];
+ for (var connection of connections_.values()) {
+ if (connection.state_.persistent)
+ clientPromises.push(connection.serialize());
+ else
+ connection.close();
+ }
+ return Promise.all($Array.map(clientPromises, function(promise) {
+ return promise.then(function(serialization) {
+ return {
+ serialization: serialization,
+ monitorHandles: !serialization.paused,
+ };
+ });
+ }));
+ });
+ });
+
+ return {
+ getDevices: getDevices,
+ createConnection: Connection.create,
+ getConnection: getConnection,
+ getConnections: getConnections,
+ // For testing.
+ Connection: Connection,
+ };
+});