|
|
@ -1,5 +1,4 @@ |
|
|
|
// @flow
|
|
|
|
import createSemaphore from 'semaphore' |
|
|
|
import type Transport from '@ledgerhq/hw-transport' |
|
|
|
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' |
|
|
|
import { DEBUG_DEVICE } from 'config/constants' |
|
|
@ -10,67 +9,34 @@ import { createCustomErrorClass } from './errors' |
|
|
|
// and guarantee we do one device access at a time. It also will handle the .close()
|
|
|
|
// NOTE optim: in the future we can debounce the close & reuse the same transport instance.
|
|
|
|
|
|
|
|
type WithDevice = (devicePath: string) => <T>(job: (Transport<*>) => Promise<T>) => Promise<T> |
|
|
|
|
|
|
|
const semaphorePerDevice = {} |
|
|
|
type WithDevice = (devicePath: string) => <T>(job: (Transport<*>) => Promise<*>) => Promise<T> |
|
|
|
|
|
|
|
const DisconnectedDevice = createCustomErrorClass('DisconnectedDevice') |
|
|
|
|
|
|
|
const remapError = <T>(p: Promise<T>): Promise<T> => |
|
|
|
p.catch(e => { |
|
|
|
if (e && e.message && e.message.indexOf('HID') >= 0) { |
|
|
|
throw new DisconnectedDevice(e.message) |
|
|
|
} |
|
|
|
throw e |
|
|
|
}) |
|
|
|
const mapError = e => { |
|
|
|
if (e && e.message && e.message.indexOf('HID') >= 0) { |
|
|
|
throw new DisconnectedDevice(e.message) |
|
|
|
} |
|
|
|
throw e |
|
|
|
} |
|
|
|
|
|
|
|
export const withDevice: WithDevice = devicePath => { |
|
|
|
const sem = |
|
|
|
semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1)) |
|
|
|
let queue = Promise.resolve() |
|
|
|
|
|
|
|
return job => |
|
|
|
takeSemaphorePromise(sem, devicePath, async () => { |
|
|
|
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 }) |
|
|
|
export const withDevice: WithDevice = devicePath => job => { |
|
|
|
const p = queue.then(async () => { |
|
|
|
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 }) |
|
|
|
if (DEBUG_DEVICE) { |
|
|
|
t.setDebugMode(true) |
|
|
|
} |
|
|
|
try { |
|
|
|
const res = await job(t).catch(mapError) |
|
|
|
return res |
|
|
|
} finally { |
|
|
|
await t.close() |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
if (DEBUG_DEVICE) t.setDebugMode(true) |
|
|
|
try { |
|
|
|
const res = await remapError(job(t)) |
|
|
|
// $FlowFixMe
|
|
|
|
return res |
|
|
|
} finally { |
|
|
|
await t.close() |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
queue = p.catch(() => null) |
|
|
|
|
|
|
|
function takeSemaphorePromise<T>(sem, devicePath, f: () => Promise<T>): Promise<T> { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
sem.take(() => { |
|
|
|
process.send({ |
|
|
|
type: 'setDeviceBusy', |
|
|
|
busy: true, |
|
|
|
devicePath, |
|
|
|
}) |
|
|
|
f().then( |
|
|
|
r => { |
|
|
|
sem.leave() |
|
|
|
resolve(r) |
|
|
|
process.send({ |
|
|
|
type: 'setDeviceBusy', |
|
|
|
busy: false, |
|
|
|
devicePath, |
|
|
|
}) |
|
|
|
}, |
|
|
|
e => { |
|
|
|
sem.leave() |
|
|
|
reject(e) |
|
|
|
process.send({ |
|
|
|
type: 'setDeviceBusy', |
|
|
|
busy: false, |
|
|
|
devicePath, |
|
|
|
}) |
|
|
|
}, |
|
|
|
) |
|
|
|
}) |
|
|
|
}) |
|
|
|
return p |
|
|
|
} |
|
|
|