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