Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / hidgnubbydevice.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.hid.
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.hid.HidConnectInfo} dev The connection to the device.
15  * @param {number} id The device's id.
16  * @constructor
17  * @implements {GnubbyDevice}
18  */
19 function HidGnubbyDevice(gnubbies, dev, id) {
20   /** @private {Gnubbies} */
21   this.gnubbies_ = gnubbies;
22   this.dev = dev;
23   this.id = id;
24   this.txqueue = [];
25   this.clients = [];
26   this.lockCID = 0;     // channel ID of client holding a lock, if != 0.
27   this.lockMillis = 0;  // current lock period.
28   this.lockTID = null;  // timer id of lock timeout.
29   this.closing = false;  // device to be closed by receive loop.
30   this.updating = false;  // device firmware is in final stage of updating.
31 }
32
33 /**
34  * Namespace for the HidGnubbyDevice implementation.
35  * @const
36  */
37 HidGnubbyDevice.NAMESPACE = 'hid';
38
39 /** Destroys this low-level device instance. */
40 HidGnubbyDevice.prototype.destroy = function() {
41   if (!this.dev) return;  // Already dead.
42
43   this.gnubbies_.removeOpenDevice(
44       {namespace: HidGnubbyDevice.NAMESPACE, device: this.id});
45   this.closing = true;
46
47   console.log(UTIL_fmt('HidGnubbyDevice.destroy()'));
48
49   // Synthesize a close error frame to alert all clients,
50   // some of which might be in read state.
51   //
52   // Use magic CID 0 to address all.
53   this.publishFrame_(new Uint8Array([
54         0, 0, 0, 0,  // broadcast CID
55         GnubbyDevice.CMD_ERROR,
56         0, 1,  // length
57         GnubbyDevice.GONE]).buffer);
58
59   // Set all clients to closed status and remove them.
60   while (this.clients.length != 0) {
61     var client = this.clients.shift();
62     if (client) client.closed = true;
63   }
64
65   if (this.lockTID) {
66     window.clearTimeout(this.lockTID);
67     this.lockTID = null;
68   }
69
70   var dev = this.dev;
71   this.dev = null;
72
73   chrome.hid.disconnect(dev.connectionId, function() {
74     console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed'));
75   });
76 };
77
78 /**
79  * Push frame to all clients.
80  * @param {ArrayBuffer} f Data to push
81  * @private
82  */
83 HidGnubbyDevice.prototype.publishFrame_ = function(f) {
84   var old = this.clients;
85
86   var remaining = [];
87   var changes = false;
88   for (var i = 0; i < old.length; ++i) {
89     var client = old[i];
90     if (client.receivedFrame(f)) {
91       // Client still alive; keep on list.
92       remaining.push(client);
93     } else {
94       changes = true;
95       console.log(UTIL_fmt(
96           '[' + client.cid.toString(16) + '] left?'));
97     }
98   }
99   if (changes) this.clients = remaining;
100 };
101
102 /**
103  * Register a client for this gnubby.
104  * @param {*} who The client.
105  */
106 HidGnubbyDevice.prototype.registerClient = function(who) {
107   for (var i = 0; i < this.clients.length; ++i) {
108     if (this.clients[i] === who) return;  // Already registered.
109   }
110   this.clients.push(who);
111   if (this.clients.length == 1) {
112     // First client? Kick off read loop.
113     this.readLoop_();
114   }
115 };
116
117 /**
118  * De-register a client.
119  * @param {*} who The client.
120  * @return {number} The number of remaining listeners for this device, or -1
121  * Returns number of remaining listeners for this device.
122  *     if this had no clients to start with.
123  */
124 HidGnubbyDevice.prototype.deregisterClient = function(who) {
125   var current = this.clients;
126   if (current.length == 0) return -1;
127   this.clients = [];
128   for (var i = 0; i < current.length; ++i) {
129     var client = current[i];
130     if (client !== who) this.clients.push(client);
131   }
132   return this.clients.length;
133 };
134
135 /**
136  * @param {*} who The client.
137  * @return {boolean} Whether this device has who as a client.
138  */
139 HidGnubbyDevice.prototype.hasClient = function(who) {
140   if (this.clients.length == 0) return false;
141   for (var i = 0; i < this.clients.length; ++i) {
142     if (who === this.clients[i])
143       return true;
144   }
145   return false;
146 };
147
148 /**
149  * Reads all incoming frames and notifies clients of their receipt.
150  * @private
151  */
152 HidGnubbyDevice.prototype.readLoop_ = function() {
153   //console.log(UTIL_fmt('entering readLoop'));
154   if (!this.dev) return;
155
156   if (this.closing) {
157     this.destroy();
158     return;
159   }
160
161   // No interested listeners, yet we hit readLoop().
162   // Must be clean-up. We do this here to make sure no transfer is pending.
163   if (!this.clients.length) {
164     this.closing = true;
165     this.destroy();
166     return;
167   }
168
169   // firmwareUpdate() sets this.updating when writing the last block before
170   // the signature. We process that reply with the already pending
171   // read transfer but we do not want to start another read transfer for the
172   // signature block, since that request will have no reply.
173   // Instead we will see the device drop and re-appear on the bus.
174   // Current libusb on some platforms gets unhappy when transfer are pending
175   // when that happens.
176   // TODO: revisit once Chrome stabilizes its behavior.
177   if (this.updating) {
178     console.log(UTIL_fmt('device updating. Ending readLoop()'));
179     return;
180   }
181
182   var self = this;
183   chrome.hid.receive(
184     this.dev.connectionId,
185     function(report_id, data) {
186       if (chrome.runtime.lastError || !data) {
187         console.log(UTIL_fmt('got lastError'));
188         console.log(chrome.runtime.lastError);
189         window.setTimeout(function() { self.destroy(); }, 0);
190         return;
191       }
192       var u8 = new Uint8Array(data);
193       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
194
195       self.publishFrame_(data);
196
197       // Read more.
198       window.setTimeout(function() { self.readLoop_(); }, 0);
199     }
200   );
201 };
202
203 /**
204  * Check whether channel is locked for this request or not.
205  * @param {number} cid Channel id
206  * @param {number} cmd Request command
207  * @return {boolean} true if not locked for this request.
208  * @private
209  */
210 HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
211   if (this.lockCID) {
212     // We have an active lock.
213     if (this.lockCID != cid) {
214       // Some other channel has active lock.
215
216       if (cmd != GnubbyDevice.CMD_SYNC) {
217         // Anything but SYNC gets an immediate busy.
218         var busy = new Uint8Array(
219             [(cid >> 24) & 255,
220              (cid >> 16) & 255,
221              (cid >> 8) & 255,
222              cid & 255,
223              GnubbyDevice.CMD_ERROR,
224              0, 1,  // length
225              GnubbyDevice.BUSY]);
226         // Log the synthetic busy too.
227         console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
228         this.publishFrame_(busy.buffer);
229         return false;
230       }
231
232       // SYNC gets to go to the device to flush OS tx/rx queues.
233       // The usb firmware always responds to SYNC, regardless of lock status.
234     }
235   }
236   return true;
237 };
238
239 /**
240  * Update or grab lock.
241  * @param {number} cid Channel ID
242  * @param {number} cmd Command
243  * @param {number} arg Command argument
244  * @private
245  */
246 HidGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) {
247   if (this.lockCID == 0 || this.lockCID == cid) {
248     // It is this caller's or nobody's lock.
249     if (this.lockTID) {
250       window.clearTimeout(this.lockTID);
251       this.lockTID = null;
252     }
253
254     if (cmd == GnubbyDevice.CMD_LOCK) {
255       var nseconds = arg;
256       if (nseconds != 0) {
257         this.lockCID = cid;
258         // Set tracking time to be .1 seconds longer than usb device does.
259         this.lockMillis = nseconds * 1000 + 100;
260       } else {
261         // Releasing lock voluntarily.
262         this.lockCID = 0;
263       }
264     }
265
266     // (re)set the lock timeout if we still hold it.
267     if (this.lockCID) {
268       var self = this;
269       this.lockTID = window.setTimeout(
270           function() {
271             console.warn(UTIL_fmt(
272                 'lock for CID ' + cid.toString(16) + ' expired!'));
273             self.lockTID = null;
274             self.lockCID = 0;
275           },
276           this.lockMillis);
277     }
278   }
279 };
280
281 /**
282  * Queue command to be sent.
283  * If queue was empty, initiate the write.
284  * @param {number} cid The client's channel ID.
285  * @param {number} cmd The command to send.
286  * @param {ArrayBuffer|Uint8Array} data Command arguments
287  */
288 HidGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) {
289   if (!this.dev) return;
290   if (!this.checkLock_(cid, cmd)) return;
291
292   var u8 = new Uint8Array(data);
293   var f = new Uint8Array(64);
294
295   HidGnubbyDevice.setCid_(f, cid);
296   f[4] = cmd;
297   f[5] = (u8.length >> 8);
298   f[6] = (u8.length & 255);
299
300   var lockArg = (u8.length > 0) ? u8[0] : 0;
301
302   // Fragment over our 64 byte frames.
303   var n = 7;
304   var seq = 0;
305   for (var i = 0; i < u8.length; ++i) {
306     f[n++] = u8[i];
307     if (n == f.length) {
308       this.queueFrame_(f.buffer, cid, cmd, lockArg);
309
310       f = new Uint8Array(64);
311       HidGnubbyDevice.setCid_(f, cid);
312       cmd = f[4] = seq++;
313       n = 5;
314     }
315   }
316   if (n != 5) {
317     this.queueFrame_(f.buffer, cid, cmd, lockArg);
318   }
319 };
320
321 /**
322  * Sets the channel id in the frame.
323  * @param {Uint8Array} frame Data frame
324  * @param {number} cid The client's channel ID.
325  * @private
326  */
327 HidGnubbyDevice.setCid_ = function(frame, cid) {
328   frame[0] = cid >>> 24;
329   frame[1] = cid >>> 16;
330   frame[2] = cid >>> 8;
331   frame[3] = cid;
332 };
333
334 /**
335  * Updates the lock, and queues the frame for sending. Also begins sending if
336  * no other writes are outstanding.
337  * @param {ArrayBuffer} frame Data frame
338  * @param {number} cid The client's channel ID.
339  * @param {number} cmd The command to send.
340  * @param {number} arg Command argument
341  * @private
342  */
343 HidGnubbyDevice.prototype.queueFrame_ = function(frame, cid, cmd, arg) {
344   this.updateLock_(cid, cmd, arg);
345   var wasEmpty = (this.txqueue.length == 0);
346   this.txqueue.push(frame);
347   if (wasEmpty) this.writePump_();
348 };
349
350 /**
351  * Stuff queued frames from txqueue[] to device, one by one.
352  * @private
353  */
354 HidGnubbyDevice.prototype.writePump_ = function() {
355   if (!this.dev) return;  // Ignore.
356
357   if (this.txqueue.length == 0) return;  // Done with current queue.
358
359   var frame = this.txqueue[0];
360
361   var self = this;
362   function transferComplete() {
363     if (chrome.runtime.lastError) {
364       console.log(UTIL_fmt('got lastError'));
365       console.log(chrome.runtime.lastError);
366       window.setTimeout(function() { self.destroy(); }, 0);
367       return;
368     }
369     self.txqueue.shift();  // drop sent frame from queue.
370     if (self.txqueue.length != 0) {
371       window.setTimeout(function() { self.writePump_(); }, 0);
372     }
373   };
374
375   var u8 = new Uint8Array(frame);
376
377   // See whether this requires scrubbing before logging.
378   var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') &&
379                      Gnubby['redactRequestLog'](u8);
380   if (alternateLog) {
381     console.log(UTIL_fmt('>' + alternateLog));
382   } else {
383     console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
384   }
385
386   var u8f = new Uint8Array(64);
387   for (var i = 0; i < u8.length; ++i) {
388     u8f[i] = u8[i];
389   }
390
391   chrome.hid.send(
392       this.dev.connectionId,
393       0,  // report Id. Must be 0 for our use.
394       u8f.buffer,
395       transferComplete
396   );
397 };
398
399 /**
400  * @param {function(Array)} cb Enumeration callback
401  */
402 HidGnubbyDevice.enumerate = function(cb) {
403   var permittedDevs;
404   var numEnumerated = 0;
405   var allDevs = [];
406
407   function enumerated(devs) {
408     allDevs = allDevs.concat(devs);
409     if (++numEnumerated == permittedDevs.length) {
410       cb(allDevs);
411     }
412   }
413
414   GnubbyDevice.getPermittedUsbDevices(function(devs) {
415     permittedDevs = devs;
416     for (var i = 0; i < devs.length; i++) {
417       chrome.hid.getDevices(devs[i], enumerated);
418     }
419   });
420 };
421
422 /**
423  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
424  *     in.
425  * @param {number} which The index of the device to open.
426  * @param {!chrome.hid.HidDeviceInfo} dev The device to open.
427  * @param {function(number, GnubbyDevice=)} cb Called back with the
428  *     result of opening the device.
429  */
430 HidGnubbyDevice.open = function(gnubbies, which, dev, cb) {
431   chrome.hid.connect(dev.deviceId, function(handle) {
432     if (chrome.runtime.lastError) {
433       console.log(chrome.runtime.lastError);
434     }
435     if (!handle) {
436       console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
437       cb(-GnubbyDevice.NODEVICE);
438       return;
439     }
440     var nonNullHandle = /** @type {!chrome.hid.HidConnectInfo} */ (handle);
441     var gnubby = new HidGnubbyDevice(gnubbies, nonNullHandle, which);
442     cb(-GnubbyDevice.OK, gnubby);
443   });
444 };
445
446 /**
447  * @param {*} dev A browser API device object
448  * @return {GnubbyDeviceId} A device identifier for the device.
449  */
450 HidGnubbyDevice.deviceToDeviceId = function(dev) {
451   var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
452   var deviceId = {
453     namespace: HidGnubbyDevice.NAMESPACE,
454     device: hidDev.deviceId
455   };
456   return deviceId;
457 };
458
459 /**
460  * Registers this implementation with gnubbies.
461  * @param {Gnubbies} gnubbies Gnubbies registry
462  */
463 HidGnubbyDevice.register = function(gnubbies) {
464   var HID_GNUBBY_IMPL = {
465     isSharedAccess: true,
466     enumerate: HidGnubbyDevice.enumerate,
467     deviceToDeviceId: HidGnubbyDevice.deviceToDeviceId,
468     open: HidGnubbyDevice.open
469   };
470   gnubbies.registerNamespace(HidGnubbyDevice.NAMESPACE, HID_GNUBBY_IMPL);
471 };