diff --git a/packages/hw-transport-node-hid/package.json b/packages/hw-transport-node-hid/package.json index 33cc0d2a0..d6a9ff3d2 100644 --- a/packages/hw-transport-node-hid/package.json +++ b/packages/hw-transport-node-hid/package.json @@ -26,7 +26,9 @@ "license": "Apache-2.0", "dependencies": { "@ledgerhq/hw-transport": "^4.21.0", - "node-hid": "^0.7.2" + "lodash": "^4.17.10", + "node-hid": "^0.7.2", + "usb": "^1.3.2" }, "devDependencies": { "flow-bin": "^0.68.0", diff --git a/packages/hw-transport-node-hid/src/TransportNodeHid.js b/packages/hw-transport-node-hid/src/TransportNodeHid.js index 1e86aff9d..22efaec70 100644 --- a/packages/hw-transport-node-hid/src/TransportNodeHid.js +++ b/packages/hw-transport-node-hid/src/TransportNodeHid.js @@ -26,8 +26,9 @@ function defer(): Defer { return { promise, resolve, reject }; } -let listenDevicesPollingInterval = 500; +let listenDevicesDebounce = 500; let listenDevicesPollingSkip = () => false; +let listenDevicesDebug = () => {}; /** * node-hid Transport implementation @@ -60,14 +61,23 @@ export default class TransportNodeHid extends Transport { static list = (): Promise => Promise.resolve(getDevices().map(d => d.path)); - static setListenDevicesPollingInterval = (delay: number) => { - listenDevicesPollingInterval = delay; + static setListenDevicesDebounce = (delay: number) => { + listenDevicesDebounce = delay; }; static setListenDevicesPollingSkip = (conditionToSkip: () => boolean) => { listenDevicesPollingSkip = conditionToSkip; }; + static setListenDevicesDebug = (debug: boolean | ((log: string) => void)) => { + listenDevicesDebug = + typeof debug === "function" + ? debug + : debug + ? (...log) => console.log("[listenDevices]", ...log) + : () => {}; + }; + /** */ static listen = ( @@ -84,8 +94,9 @@ export default class TransportNodeHid extends Transport { } }); const { events, stop } = listenDevices( - listenDevicesPollingInterval, - listenDevicesPollingSkip + listenDevicesDebounce, + listenDevicesPollingSkip, + listenDevicesDebug ); const onAdd = device => { diff --git a/packages/hw-transport-node-hid/src/getDevices.js b/packages/hw-transport-node-hid/src/getDevices.js index c128cec67..0b9a97208 100644 --- a/packages/hw-transport-node-hid/src/getDevices.js +++ b/packages/hw-transport-node-hid/src/getDevices.js @@ -1,6 +1,11 @@ // @flow import HID from "node-hid"; -import isLedgerDevice from "./isLedgerDevice"; + +const filterInterface = device => + ["win32", "darwin"].includes(process.platform) + ? device.usagePage === 0xffa0 + : device.interface === 0; + export default function getDevices(): Array<*> { - return HID.devices().filter(isLedgerDevice); + return HID.devices(0x2c97, 0x0).filter(filterInterface); } diff --git a/packages/hw-transport-node-hid/src/isLedgerDevice.js b/packages/hw-transport-node-hid/src/isLedgerDevice.js deleted file mode 100644 index d8b056fe2..000000000 --- a/packages/hw-transport-node-hid/src/isLedgerDevice.js +++ /dev/null @@ -1,6 +0,0 @@ -// We check the usagePage on Win/OSX, and interface on Linux for get only HID device -export default d => - (["win32", "darwin"].includes(process.platform) - ? d.usagePage === 0xffa0 - : d.interface === 0) && - ((d.vendorId === 0x2581 && d.productId === 0x3b7c) || d.vendorId === 0x2c97); diff --git a/packages/hw-transport-node-hid/src/listenDevices.js b/packages/hw-transport-node-hid/src/listenDevices.js index 582fe64d9..f08e2bcf6 100644 --- a/packages/hw-transport-node-hid/src/listenDevices.js +++ b/packages/hw-transport-node-hid/src/listenDevices.js @@ -1,11 +1,14 @@ // @flow import EventEmitter from "events"; +import usb from "usb"; +import debounce from "lodash/debounce"; import getDevices from "./getDevices"; export default ( delay: number, - listenDevicesPollingSkip: () => boolean + listenDevicesPollingSkip: () => boolean, + debug: (...any) => void ): { events: EventEmitter, stop: () => void @@ -13,7 +16,6 @@ export default ( const events = new EventEmitter(); events.setMaxListeners(0); - let timeoutDetection; let listDevices = getDevices(); const flatDevice = d => d.path; @@ -27,37 +29,77 @@ export default ( let lastDevices = getFlatDevices(); - const checkDevices = () => { + const poll = () => { if (!listenDevicesPollingSkip()) { - const currentDevices = getFlatDevices(); + debug("Polling for added or removed devices"); + let changeFound = false; + const currentDevices = getFlatDevices(); const newDevices = currentDevices.filter(d => !lastDevices.includes(d)); - const removeDevices = lastDevices.filter( - d => !currentDevices.includes(d) - ); if (newDevices.length > 0) { + debug("New device found:", newDevices); + listDevices = getDevices(); events.emit("add", getDeviceByPaths(newDevices)); + + changeFound = true; + } else { + debug("No new device found"); } + const removeDevices = lastDevices.filter( + d => !currentDevices.includes(d) + ); + if (removeDevices.length > 0) { + debug("Removed device found:", removeDevices); + events.emit("remove", getDeviceByPaths(removeDevices)); listDevices = listDevices.filter( d => !removeDevices.includes(flatDevice(d)) ); + + changeFound = true; + } else { + debug("No removed device found"); } - lastDevices = currentDevices; + if (changeFound) { + lastDevices = currentDevices; + } + } else { + debug("Polling skipped, re-debouncing"); + debouncedPoll(); } - setTimeout(checkDevices, delay); }; - timeoutDetection = setTimeout(checkDevices, delay); + const debouncedPoll = debounce(poll, delay); + + const attachDetected = device => { + debug("Device add detected:", device); + + debouncedPoll(); + }; + usb.on("attach", attachDetected); + debug("attach listener added"); + + const detachDetected = device => { + debug("Device removal detected:", device); + + debouncedPoll(); + }; + usb.on("detach", detachDetected); + debug("detach listener added"); return { stop: () => { - clearTimeout(timeoutDetection); + debug( + "Stop received, removing listeners and cancelling pending debounced polls" + ); + debouncedPoll.cancel(); + usb.removeListener("attach", attachDetected); + usb.removeListener("detach", detachDetected); }, events }; diff --git a/yarn.lock b/yarn.lock index e62e20fe3..b0f4638a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6761,7 +6761,7 @@ lodash.uniq@^4.5.0: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" -lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0: +lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.2.0: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -7289,7 +7289,7 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.0.5, nan@^2.0.7, nan@^2.10.0, nan@^2.2.1, nan@^2.3.0, nan@^2.9.2: +nan@^2.0.5, nan@^2.0.7, nan@^2.10.0, nan@^2.2.1, nan@^2.3.0, nan@^2.8.0, nan@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" @@ -7322,6 +7322,14 @@ needle@^2.2.0: iconv-lite "^0.4.4" sax "^1.2.4" +needle@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -7418,6 +7426,21 @@ node-notifier@^5.0.2: shellwords "^0.1.1" which "^1.3.0" +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + node-pre-gyp@^0.6.39: version "0.6.39" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" @@ -8752,7 +8775,7 @@ rc@^1.0.1, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -rc@^1.1.6: +rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" dependencies: @@ -10929,6 +10952,13 @@ url@^0.11.0, url@~0.11.0: punycode "1.3.2" querystring "0.2.0" +usb@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/usb/-/usb-1.3.2.tgz#4563a32323856e26c97dae374b34c66c3d83b5f4" + dependencies: + nan "^2.8.0" + node-pre-gyp "^0.10.0" + use@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"