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