09b3909f91580a26157df46516f7ebcd6cd8868a
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / usbgnubbydevice.js
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.
4
5 /**
6  * @fileoverview Implements a low-level gnubby driver based on chrome.usb.
7  */
8 'use strict';
9
10 /**
11  * Low level gnubby 'driver'. One per physical USB device.
12  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
13  *     in.
14  * @param {!chrome.usb.ConnectionHandle} dev The device.
15  * @param {number} id The device's id.
16  * @param {number} inEndpoint The device's in endpoint.
17  * @param {number} outEndpoint The device's out endpoint.
18  * @constructor
19  * @implements {GnubbyDevice}
20  */
21 function UsbGnubbyDevice(gnubbies, dev, id, inEndpoint, outEndpoint) {
22   /** @private {Gnubbies} */
23   this.gnubbies_ = gnubbies;
24   this.dev = dev;
25   this.id = id;
26   this.inEndpoint = inEndpoint;
27   this.outEndpoint = outEndpoint;
28   this.txqueue = [];
29   this.clients = [];
30   this.lockCID = 0;     // channel ID of client holding a lock, if != 0.
31   this.lockMillis = 0;  // current lock period.
32   this.lockTID = null;  // timer id of lock timeout.
33   this.closing = false;  // device to be closed by receive loop.
34   this.updating = false;  // device firmware is in final stage of updating.
35   this.inTransferPending = false;
36   this.outTransferPending = false;
37 }
38
39 /**
40  * Namespace for the UsbGnubbyDevice implementation.
41  * @const
42  */
43 UsbGnubbyDevice.NAMESPACE = 'usb';
44
45 /** Destroys this low-level device instance. */
46 UsbGnubbyDevice.prototype.destroy = function() {
47   if (!this.dev) return;  // Already dead.
48
49   this.gnubbies_.removeOpenDevice(
50       {namespace: UsbGnubbyDevice.NAMESPACE, device: this.id});
51   this.closing = true;
52
53   console.log(UTIL_fmt('UsbGnubbyDevice.destroy()'));
54
55   // Synthesize a close error frame to alert all clients,
56   // some of which might be in read state.
57   //
58   // Use magic CID 0 to address all.
59   this.publishFrame_(new Uint8Array([
60         0, 0, 0, 0,  // broadcast CID
61         GnubbyDevice.CMD_ERROR,
62         0, 1,  // length
63         GnubbyDevice.GONE]).buffer);
64
65   // Set all clients to closed status and remove them.
66   while (this.clients.length != 0) {
67     var client = this.clients.shift();
68     if (client) client.closed = true;
69   }
70
71   if (this.lockTID) {
72     window.clearTimeout(this.lockTID);
73     this.lockTID = null;
74   }
75
76   var dev = this.dev;
77   this.dev = null;
78
79   chrome.usb.releaseInterface(dev, 0, function() {
80     console.log(UTIL_fmt('Device ' + dev.handle + ' released'));
81     chrome.usb.closeDevice(dev, function() {
82       console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
83     });
84   });
85 };
86
87 /**
88  * Push frame to all clients.
89  * @param {ArrayBuffer} f Data frame
90  * @private
91  */
92 UsbGnubbyDevice.prototype.publishFrame_ = function(f) {
93   var old = this.clients;
94
95   var remaining = [];
96   var changes = false;
97   for (var i = 0; i < old.length; ++i) {
98     var client = old[i];
99     if (client.receivedFrame(f)) {
100       // Client still alive; keep on list.
101       remaining.push(client);
102     } else {
103       changes = true;
104       console.log(UTIL_fmt(
105           '[' + client.cid.toString(16) + '] left?'));
106     }
107   }
108   if (changes) this.clients = remaining;
109 };
110
111 /**
112  * @return {boolean} whether this device is open and ready to use.
113  * @private
114  */
115 UsbGnubbyDevice.prototype.readyToUse_ = function() {
116   if (this.closing) return false;
117   if (!this.dev) return false;
118
119   return true;
120 };
121
122 /**
123  * Reads one reply from the low-level device.
124  * @private
125  */
126 UsbGnubbyDevice.prototype.readOneReply_ = function() {
127   if (!this.readyToUse_()) return;  // No point in continuing.
128   if (this.updating) return;  // Do not bother waiting for final update reply.
129
130   var self = this;
131
132   function inTransferComplete(x) {
133     self.inTransferPending = false;
134
135     if (!self.readyToUse_()) return;  // No point in continuing.
136
137     if (chrome.runtime.lastError) {
138       console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
139       console.log(chrome.runtime.lastError);
140       window.setTimeout(function() { self.destroy(); }, 0);
141       return;
142     }
143
144     if (x.data) {
145       var u8 = new Uint8Array(x.data);
146       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
147
148       self.publishFrame_(x.data);
149
150       // Write another pending request, if any.
151       window.setTimeout(
152           function() {
153             self.txqueue.shift();  // Drop sent frame from queue.
154             self.writeOneRequest_();
155           },
156           0);
157     } else {
158       console.log(UTIL_fmt('no x.data!'));
159       console.log(x);
160       window.setTimeout(function() { self.destroy(); }, 0);
161     }
162   }
163
164   if (this.inTransferPending == false) {
165     this.inTransferPending = true;
166     chrome.usb.bulkTransfer(
167       /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
168       { direction: 'in', endpoint: this.inEndpoint, length: 2048 },
169       inTransferComplete);
170   } else {
171     throw 'inTransferPending!';
172   }
173 };
174
175 /**
176  * Register a client for this gnubby.
177  * @param {*} who The client.
178  */
179 UsbGnubbyDevice.prototype.registerClient = function(who) {
180   for (var i = 0; i < this.clients.length; ++i) {
181     if (this.clients[i] === who) return;  // Already registered.
182   }
183   this.clients.push(who);
184 };
185
186 /**
187  * De-register a client.
188  * @param {*} who The client.
189  * @return {number} The number of remaining listeners for this device, or -1
190  * Returns number of remaining listeners for this device.
191  *     if this had no clients to start with.
192  */
193 UsbGnubbyDevice.prototype.deregisterClient = function(who) {
194   var current = this.clients;
195   if (current.length == 0) return -1;
196   this.clients = [];
197   for (var i = 0; i < current.length; ++i) {
198     var client = current[i];
199     if (client !== who) this.clients.push(client);
200   }
201   return this.clients.length;
202 };
203
204 /**
205  * @param {*} who The client.
206  * @return {boolean} Whether this device has who as a client.
207  */
208 UsbGnubbyDevice.prototype.hasClient = function(who) {
209   if (this.clients.length == 0) return false;
210   for (var i = 0; i < this.clients.length; ++i) {
211     if (who === this.clients[i])
212       return true;
213   }
214   return false;
215 };
216
217 /**
218  * Stuff queued frames from txqueue[] to device, one by one.
219  * @private
220  */
221 UsbGnubbyDevice.prototype.writeOneRequest_ = function() {
222   if (!this.readyToUse_()) return;  // No point in continuing.
223
224   if (this.txqueue.length == 0) return;  // Nothing to send.
225
226   var frame = this.txqueue[0];
227
228   var self = this;
229   function OutTransferComplete(x) {
230     self.outTransferPending = false;
231
232     if (!self.readyToUse_()) return;  // No point in continuing.
233
234     if (chrome.runtime.lastError) {
235       console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
236       console.log(chrome.runtime.lastError);
237       window.setTimeout(function() { self.destroy(); }, 0);
238       return;
239     }
240
241     window.setTimeout(function() { self.readOneReply_(); }, 0);
242   };
243
244   var u8 = new Uint8Array(frame);
245
246   // See whether this requires scrubbing before logging.
247   var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') &&
248                      Gnubby['redactRequestLog'](u8);
249   if (alternateLog) {
250     console.log(UTIL_fmt('>' + alternateLog));
251   } else {
252     console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
253   }
254
255   if (this.outTransferPending == false) {
256     this.outTransferPending = true;
257     chrome.usb.bulkTransfer(
258         /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
259         { direction: 'out', endpoint: this.outEndpoint, data: frame },
260         OutTransferComplete);
261   } else {
262     throw 'outTransferPending!';
263   }
264 };
265
266 /**
267  * Check whether channel is locked for this request or not.
268  * @param {number} cid Channel id
269  * @param {number} cmd Command to be sent
270  * @return {boolean} true if not locked for this request.
271  * @private
272  */
273 UsbGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
274   if (this.lockCID) {
275     // We have an active lock.
276     if (this.lockCID != cid) {
277       // Some other channel has active lock.
278
279       if (cmd != GnubbyDevice.CMD_SYNC) {
280         // Anything but SYNC gets an immediate busy.
281         var busy = new Uint8Array(
282             [(cid >> 24) & 255,
283              (cid >> 16) & 255,
284              (cid >> 8) & 255,
285              cid & 255,
286              GnubbyDevice.CMD_ERROR,
287              0, 1,  // length
288              GnubbyDevice.BUSY]);
289         // Log the synthetic busy too.
290         console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
291         this.publishFrame_(busy.buffer);
292         return false;
293       }
294
295       // SYNC gets to go to the device to flush OS tx/rx queues.
296       // The usb firmware always responds to SYNC, regardless of lock status.
297     }
298   }
299   return true;
300 };
301
302 /**
303  * Update or grab lock.
304  * @param {number} cid Channel id
305  * @param {number} cmd Command
306  * @param {number} arg Command argument
307  * @private
308  */
309 UsbGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) {
310   if (this.lockCID == 0 || this.lockCID == cid) {
311     // It is this caller's or nobody's lock.
312     if (this.lockTID) {
313       window.clearTimeout(this.lockTID);
314       this.lockTID = null;
315     }
316
317     if (cmd == GnubbyDevice.CMD_LOCK) {
318       var nseconds = arg;
319       if (nseconds != 0) {
320         this.lockCID = cid;
321         // Set tracking time to be .1 seconds longer than usb device does.
322         this.lockMillis = nseconds * 1000 + 100;
323       } else {
324         // Releasing lock voluntarily.
325         this.lockCID = 0;
326       }
327     }
328
329     // (re)set the lock timeout if we still hold it.
330     if (this.lockCID) {
331       var self = this;
332       this.lockTID = window.setTimeout(
333           function() {
334             console.warn(UTIL_fmt(
335                 'lock for CID ' + cid.toString(16) + ' expired!'));
336             self.lockTID = null;
337             self.lockCID = 0;
338           },
339           this.lockMillis);
340     }
341   }
342 };
343
344 /**
345  * Queue command to be sent.
346  * If queue was empty, initiate the write.
347  * @param {number} cid The client's channel ID.
348  * @param {number} cmd The command to send.
349  * @param {ArrayBuffer|Uint8Array} data Command argument data
350  */
351 UsbGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) {
352   if (!this.dev) return;
353   if (!this.checkLock_(cid, cmd)) return;
354
355   var u8 = new Uint8Array(data);
356   var frame = new Uint8Array(u8.length + 7);
357
358   frame[0] = cid >>> 24;
359   frame[1] = cid >>> 16;
360   frame[2] = cid >>> 8;
361   frame[3] = cid;
362   frame[4] = cmd;
363   frame[5] = (u8.length >> 8);
364   frame[6] = (u8.length & 255);
365
366   frame.set(u8, 7);
367
368   var lockArg = (u8.length > 0) ? u8[0] : 0;
369   this.updateLock_(cid, cmd, lockArg);
370
371   var wasEmpty = (this.txqueue.length == 0);
372   this.txqueue.push(frame.buffer);
373   if (wasEmpty) this.writeOneRequest_();
374 };
375
376 /**
377  * @param {function(Array)} cb Enumerate callback
378  */
379 UsbGnubbyDevice.enumerate = function(cb) {
380   var permittedDevs;
381   var numEnumerated = 0;
382   var allDevs = [];
383
384   function enumerated(devs) {
385     allDevs = allDevs.concat(devs);
386     if (++numEnumerated == permittedDevs.length) {
387       cb(allDevs);
388     }
389   }
390
391   GnubbyDevice.getPermittedUsbDevices(function(devs) {
392     permittedDevs = devs;
393     for (var i = 0; i < devs.length; i++) {
394       chrome.usb.getDevices(devs[i], enumerated);
395     }
396   });
397 };
398
399 /**
400  * @typedef {?{
401  *   address: number,
402  *   type: string,
403  *   direction: string,
404  *   maximumPacketSize: number,
405  *   synchronization: (string|undefined),
406  *   usage: (string|undefined),
407  *   pollingInterval: (number|undefined)
408  * }}
409  * @see http://developer.chrome.com/apps/usb.html#method-listInterfaces
410  */
411 var InterfaceEndpoint;
412
413
414 /**
415  * @typedef {?{
416  *   interfaceNumber: number,
417  *   alternateSetting: number,
418  *   interfaceClass: number,
419  *   interfaceSubclass: number,
420  *   interfaceProtocol: number,
421  *   description: (string|undefined),
422  *   endpoints: !Array.<!InterfaceEndpoint>
423  * }}
424  * @see http://developer.chrome.com/apps/usb.html#method-listInterfaces
425  */
426 var InterfaceDescriptor;
427
428 /**
429  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
430  *     in.
431  * @param {number} which The index of the device to open.
432  * @param {!chrome.usb.Device} dev The device to open.
433  * @param {function(number, GnubbyDevice=)} cb Called back with the
434  *     result of opening the device.
435  */
436 UsbGnubbyDevice.open = function(gnubbies, which, dev, cb) {
437   /** @param {chrome.usb.ConnectionHandle=} handle Connection handle */
438   function deviceOpened(handle) {
439     if (!handle) {
440       console.warn(UTIL_fmt('failed to open device. permissions issue?'));
441       cb(-GnubbyDevice.NODEVICE);
442       return;
443     }
444     var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle);
445     chrome.usb.listInterfaces(nonNullHandle, function(descriptors) {
446       var inEndpoint, outEndpoint;
447       for (var i = 0; i < descriptors.length; i++) {
448         var descriptor = /** @type {InterfaceDescriptor} */ (descriptors[i]);
449         for (var j = 0; j < descriptor.endpoints.length; j++) {
450           var endpoint = descriptor.endpoints[j];
451           if (inEndpoint == undefined && endpoint.type == 'bulk' &&
452               endpoint.direction == 'in') {
453             inEndpoint = endpoint.address;
454           }
455           if (outEndpoint == undefined && endpoint.type == 'bulk' &&
456               endpoint.direction == 'out') {
457             outEndpoint = endpoint.address;
458           }
459         }
460       }
461       if (inEndpoint == undefined || outEndpoint == undefined) {
462         console.warn(UTIL_fmt('device lacking an endpoint (broken?)'));
463         chrome.usb.closeDevice(nonNullHandle);
464         cb(-GnubbyDevice.NODEVICE);
465         return;
466       }
467       // Try getting it claimed now.
468       chrome.usb.claimInterface(nonNullHandle, 0, function() {
469         if (chrome.runtime.lastError) {
470           console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
471           console.log(chrome.runtime.lastError);
472         }
473         var claimed = !chrome.runtime.lastError;
474         if (!claimed) {
475           console.warn(UTIL_fmt('failed to claim interface. busy?'));
476           // Claim failed? Let the callers know and bail out.
477           chrome.usb.closeDevice(nonNullHandle);
478           cb(-GnubbyDevice.BUSY);
479           return;
480         }
481         var gnubby = new UsbGnubbyDevice(gnubbies, nonNullHandle, which,
482             inEndpoint, outEndpoint);
483         cb(-GnubbyDevice.OK, gnubby);
484       });
485     });
486   }
487
488   if (UsbGnubbyDevice.runningOnCrOS === undefined) {
489     UsbGnubbyDevice.runningOnCrOS =
490         (window.navigator.appVersion.indexOf('; CrOS ') != -1);
491   }
492   if (UsbGnubbyDevice.runningOnCrOS) {
493     chrome.usb.requestAccess(dev, 0, function(success) {
494       // Even though the argument to requestAccess is a chrome.usb.Device, the
495       // access request is for access to all devices with the same vid/pid.
496       // Curiously, if the first chrome.usb.requestAccess succeeds, a second
497       // call with a separate device with the same vid/pid fails. Since
498       // chrome.usb.openDevice will fail if a previous access request really
499       // failed, just ignore the outcome of the access request and move along.
500       chrome.usb.openDevice(dev, deviceOpened);
501     });
502   } else {
503     chrome.usb.openDevice(dev, deviceOpened);
504   }
505 };
506
507 /**
508  * @param {*} dev Chrome usb device
509  * @return {GnubbyDeviceId} A device identifier for the device.
510  */
511 UsbGnubbyDevice.deviceToDeviceId = function(dev) {
512   var usbDev = /** @type {!chrome.usb.Device} */ (dev);
513   var deviceId = {
514     namespace: UsbGnubbyDevice.NAMESPACE,
515     device: usbDev.device
516   };
517   return deviceId;
518 };
519
520 /**
521  * Registers this implementation with gnubbies.
522  * @param {Gnubbies} gnubbies Gnubbies singleton instance
523  */
524 UsbGnubbyDevice.register = function(gnubbies) {
525   var USB_GNUBBY_IMPL = {
526     isSharedAccess: false,
527     enumerate: UsbGnubbyDevice.enumerate,
528     deviceToDeviceId: UsbGnubbyDevice.deviceToDeviceId,
529     open: UsbGnubbyDevice.open
530   };
531   gnubbies.registerNamespace(UsbGnubbyDevice.NAMESPACE, USB_GNUBBY_IMPL);
532 };