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