| // 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/service_provider', |
| '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, |
| '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, |
| }; |
| }); |