@onekeyfe/hd-core 1.2.2-alpha.119 → 1.2.2-alpha.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +298 -8
- package/__tests__/core-error-output.test.ts +0 -77
- package/__tests__/device-lifecycle-events.test.ts +8 -8
- package/__tests__/search-devices.test.ts +8 -196
- package/dist/api/SearchDevices.d.ts +15 -2
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +3 -0
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts +0 -2
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.js +88 -173
- package/package.json +4 -4
- package/src/api/SearchDevices.ts +27 -121
- package/src/api/allnetwork/AllNetworkGetAddress.ts +47 -20
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -7
- package/src/core/index.ts +19 -58
- package/src/data-manager/TransportManager.ts +3 -14
- package/src/device/Device.ts +0 -7
package/src/api/SearchDevices.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import { BaseMethod } from './BaseMethod';
|
|
4
4
|
import TransportManager from '../data-manager/TransportManager';
|
|
@@ -6,80 +6,10 @@ import { DataManager } from '../data-manager';
|
|
|
6
6
|
import { LoggerNames, getDeviceTypeByBleName, getLogger } from '../utils';
|
|
7
7
|
import { DevicePool } from '../device/DevicePool';
|
|
8
8
|
|
|
9
|
-
import type { SearchDevice } from '../types/device';
|
|
10
9
|
import type DeviceConnector from '../device/DeviceConnector';
|
|
11
|
-
import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
|
|
12
|
-
import type { Device } from '../device/Device';
|
|
13
10
|
|
|
14
11
|
const Log = getLogger(LoggerNames.DevicePool);
|
|
15
12
|
|
|
16
|
-
type RequestQueueLookup = {
|
|
17
|
-
getRequestTasksIdByConnectId: (connectId: string) => number[];
|
|
18
|
-
getTask: (requestId: number) => { method: { device?: Device } } | undefined;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const getDescriptorKeys = (descriptor: DeviceDescriptor) =>
|
|
22
|
-
[descriptor.path, descriptor.id].filter(
|
|
23
|
-
(key): key is string => typeof key === 'string' && key.length > 0
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const isOwnedByActiveWebUsbRequest = (
|
|
27
|
-
descriptor: DeviceDescriptor,
|
|
28
|
-
requestQueue?: RequestQueueLookup
|
|
29
|
-
) => {
|
|
30
|
-
if (!requestQueue) return false;
|
|
31
|
-
return getDescriptorKeys(descriptor).some(
|
|
32
|
-
key => requestQueue.getRequestTasksIdByConnectId(key).length > 0
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const getUsbHandle = (descriptor?: DeviceDescriptor) =>
|
|
37
|
-
(descriptor as { device?: unknown } | undefined)?.device;
|
|
38
|
-
|
|
39
|
-
const getOwningRequestDevice = (
|
|
40
|
-
descriptor: DeviceDescriptor,
|
|
41
|
-
requestQueue?: RequestQueueLookup
|
|
42
|
-
) => {
|
|
43
|
-
if (!requestQueue) return undefined;
|
|
44
|
-
const usbHandle = getUsbHandle(descriptor);
|
|
45
|
-
// A path alone can be shared (the all-zero bootloader path, a synthesized serial-less
|
|
46
|
-
// path), so the owner's identity is only reported for the USB handle it actually bound.
|
|
47
|
-
if (usbHandle === undefined) return undefined;
|
|
48
|
-
for (const key of getDescriptorKeys(descriptor)) {
|
|
49
|
-
for (const requestId of requestQueue.getRequestTasksIdByConnectId(key)) {
|
|
50
|
-
const device = requestQueue.getTask(requestId)?.method.device;
|
|
51
|
-
if (
|
|
52
|
-
device?.features &&
|
|
53
|
-
(device.mainId === key || device.originalDescriptor?.path === key) &&
|
|
54
|
-
getUsbHandle(device.originalDescriptor) === usbHandle
|
|
55
|
-
) {
|
|
56
|
-
return device;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return undefined;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// A USB path is a routing key (the USB serial, a synthesized usb-vid-pid-name, or the
|
|
64
|
-
// bootloader placeholder), not a hardware identity, so it never fills uuid/serialNo.
|
|
65
|
-
const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
|
|
66
|
-
const connectId = descriptor.path || descriptor.id || null;
|
|
67
|
-
return {
|
|
68
|
-
connectId,
|
|
69
|
-
uuid: '',
|
|
70
|
-
serialNo: null,
|
|
71
|
-
deviceId: null,
|
|
72
|
-
deviceType: EDeviceType.Unknown,
|
|
73
|
-
name: descriptor.name || connectId || '',
|
|
74
|
-
commType: descriptor.commType,
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const toSearchDevice = (device: Device): SearchDevice | null => {
|
|
79
|
-
const message = device.toMessageObject();
|
|
80
|
-
return message ? (message as SearchDevice) : null;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
13
|
export default class SearchDevices extends BaseMethod {
|
|
84
14
|
connector?: DeviceConnector;
|
|
85
15
|
|
|
@@ -89,31 +19,19 @@ export default class SearchDevices extends BaseMethod {
|
|
|
89
19
|
this.skipForceUpdateCheck = true;
|
|
90
20
|
}
|
|
91
21
|
|
|
92
|
-
async run()
|
|
93
|
-
|
|
94
|
-
const isWebUsb = env === 'webusb' || env === 'desktop-webusb';
|
|
95
|
-
const requestQueue = this.context?.requestQueue;
|
|
96
|
-
const hasActiveWebUsbRequest = isWebUsb && (requestQueue?.getRequestTasksId().length ?? 0) > 0;
|
|
97
|
-
// Bring up WebUSB even when schema configuration is deferred while a
|
|
98
|
-
// business request owns the discovery lock. Failures here used to be
|
|
99
|
-
// swallowed by configure(); keep searchDevices resolving empty.
|
|
100
|
-
if (isWebUsb) {
|
|
101
|
-
try {
|
|
102
|
-
await TransportManager.ensureInitialized();
|
|
103
|
-
} catch (error) {
|
|
104
|
-
Log.debug('WebUSB bring-up unavailable', error);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (!hasActiveWebUsbRequest) await TransportManager.configure();
|
|
22
|
+
async run() {
|
|
23
|
+
await TransportManager.configure();
|
|
108
24
|
const deviceDiff = await this.connector?.enumerate();
|
|
109
25
|
const devicesDescriptor = deviceDiff?.descriptors ?? [];
|
|
110
26
|
|
|
27
|
+
const env = DataManager.getSettings('env');
|
|
28
|
+
|
|
111
29
|
/**
|
|
112
30
|
* No need to call features during Bluetooth scaning
|
|
113
31
|
* to avoid device pairing
|
|
114
32
|
*/
|
|
115
33
|
if (DataManager.isBleConnect(env)) {
|
|
116
|
-
const devices
|
|
34
|
+
const devices = [];
|
|
117
35
|
const seenIds = new Set<string>();
|
|
118
36
|
|
|
119
37
|
for (const device of devicesDescriptor) {
|
|
@@ -138,42 +56,30 @@ export default class SearchDevices extends BaseMethod {
|
|
|
138
56
|
return devices;
|
|
139
57
|
}
|
|
140
58
|
|
|
141
|
-
const deviceList
|
|
59
|
+
const deviceList = [];
|
|
142
60
|
for (const descriptor of devicesDescriptor) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (message) deviceList.push(message);
|
|
163
|
-
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
const errorCode =
|
|
166
|
-
error && typeof error === 'object' && 'errorCode' in error
|
|
167
|
-
? (error as { errorCode?: unknown }).errorCode
|
|
168
|
-
: undefined;
|
|
169
|
-
Log.debug('Skip unavailable device during search', {
|
|
170
|
-
path: descriptor.path,
|
|
171
|
-
...(errorCode !== undefined ? { errorCode } : {}),
|
|
172
|
-
});
|
|
173
|
-
}
|
|
61
|
+
try {
|
|
62
|
+
// Discovery is best effort. Browsers may retain WebUSB grants for devices that
|
|
63
|
+
// are offline, busy, or not ready, so one descriptor must not abort the scan.
|
|
64
|
+
const result = await DevicePool.getDevices([descriptor], descriptor.path, {
|
|
65
|
+
// Discovery must actively identify the protocol instead of trusting a caller hint.
|
|
66
|
+
connectProtocol: undefined,
|
|
67
|
+
forceProtocolDetection: true,
|
|
68
|
+
refreshRuntimeState: true,
|
|
69
|
+
});
|
|
70
|
+
deviceList.push(...result.deviceList);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const errorCode =
|
|
73
|
+
error && typeof error === 'object' && 'errorCode' in error
|
|
74
|
+
? (error as { errorCode?: unknown }).errorCode
|
|
75
|
+
: undefined;
|
|
76
|
+
Log.debug('Skip unavailable device during search', {
|
|
77
|
+
path: descriptor.path,
|
|
78
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
79
|
+
});
|
|
174
80
|
}
|
|
175
81
|
}
|
|
176
82
|
|
|
177
|
-
return deviceList;
|
|
83
|
+
return deviceList.map(device => device.toMessageObject());
|
|
178
84
|
}
|
|
179
85
|
}
|
|
@@ -31,22 +31,20 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
|
31
31
|
originalIndex: index,
|
|
32
32
|
})
|
|
33
33
|
);
|
|
34
|
-
|
|
34
|
+
// Protocol V2 DeviceSessionGet is the Initialize(session_id) equivalent: the
|
|
35
|
+
// SE wallet stays selected until the next Ask/Get or lock. Nested chain
|
|
36
|
+
// methods still resume once in callMethod; same-method addresses can share
|
|
37
|
+
// that session the way Protocol V1 bundles do.
|
|
38
|
+
const methodGroups = methodParams.reduce((groups, param) => {
|
|
35
39
|
const group = groups.get(param.methodName) ?? [];
|
|
36
40
|
group.push(param);
|
|
37
41
|
groups.set(param.methodName, group);
|
|
38
42
|
return groups;
|
|
39
43
|
}, new Map<keyof CoreApi, MethodParams[]>());
|
|
40
|
-
const requiresProtocolV2WalletHandoff =
|
|
41
|
-
this.device.isProtocolV2() &&
|
|
42
|
-
(this.payload.useEmptyPassphrase === true || !!this.payload.passphraseState);
|
|
43
|
-
const methodGroups: [keyof CoreApi, MethodParams[]][] = requiresProtocolV2WalletHandoff
|
|
44
|
-
? methodParams.map(param => [param.methodName, [param]])
|
|
45
|
-
: Array.from(groupedMethodParams.entries());
|
|
46
44
|
|
|
47
|
-
let
|
|
48
|
-
for (const [methodName, params] of methodGroups) {
|
|
49
|
-
const
|
|
45
|
+
let processed = 0;
|
|
46
|
+
for (const [methodName, params] of methodGroups.entries()) {
|
|
47
|
+
const methodCallParams = {
|
|
50
48
|
bundle: params.map(param => ({
|
|
51
49
|
...param.params,
|
|
52
50
|
})),
|
|
@@ -55,27 +53,56 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
|
55
53
|
if (this.abortController?.signal.aborted) {
|
|
56
54
|
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
57
55
|
}
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const isProtocolV2 = this.device.isProtocolV2();
|
|
57
|
+
// Displayed addresses must not be replayed if a later item fails.
|
|
58
|
+
const runIndividually =
|
|
59
|
+
isProtocolV2 &&
|
|
60
|
+
params.length > 1 &&
|
|
61
|
+
params.some(param => param._originRequestParams.showOnOneKey !== false);
|
|
62
|
+
let response: AllNetworkAddress[] = [];
|
|
63
|
+
if (!runIndividually) {
|
|
64
|
+
response = await this.callMethod(methodName, methodCallParams, rootFingerprint);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// callMethod returns failures only for skippable errors; link, cancellation,
|
|
68
|
+
// and wallet errors throw. Retry silent reads separately to isolate a bad
|
|
69
|
+
// path or unsupported coin while reusing the already selected wallet.
|
|
70
|
+
if (
|
|
71
|
+
isProtocolV2 &&
|
|
72
|
+
params.length > 1 &&
|
|
73
|
+
(runIndividually || response.every(item => !item.success))
|
|
74
|
+
) {
|
|
75
|
+
response = [];
|
|
76
|
+
for (const param of params) {
|
|
77
|
+
if (this.abortController?.signal.aborted) {
|
|
78
|
+
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
79
|
+
}
|
|
80
|
+
const itemResponse = await this.callMethod(
|
|
81
|
+
methodName,
|
|
82
|
+
{ bundle: [{ ...param.params }] },
|
|
83
|
+
rootFingerprint
|
|
84
|
+
);
|
|
85
|
+
response.push(...itemResponse);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
60
88
|
|
|
61
89
|
if (this.abortController?.signal.aborted) {
|
|
62
90
|
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
for (let
|
|
66
|
-
const { _originRequestParams, _originalIndex } = params[
|
|
67
|
-
|
|
68
|
-
resultMap[responseKey] = {
|
|
93
|
+
for (let index = 0; index < params.length; index++) {
|
|
94
|
+
const { _originRequestParams, _originalIndex } = params[index];
|
|
95
|
+
resultMap[`${_originalIndex}`] = {
|
|
69
96
|
..._originRequestParams,
|
|
70
|
-
...response[
|
|
97
|
+
...response[index],
|
|
71
98
|
};
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
processed += params.length;
|
|
102
|
+
if (bundle.length > 1) {
|
|
103
|
+
const progress = Math.round((processed / bundle.length) * 100);
|
|
76
104
|
this.postMessage(createUiMessage(UI_REQUEST.DEVICE_PROGRESS, { progress }));
|
|
77
105
|
}
|
|
78
|
-
i++;
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
for (let i = 0; i < bundle.length; i++) {
|
|
@@ -273,6 +273,12 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
273
273
|
|
|
274
274
|
private loadingCommands?: DeviceCommands;
|
|
275
275
|
|
|
276
|
+
// DeviceSessionGet selects the SE wallet like Initialize(session_id). Nested
|
|
277
|
+
// all-network methods skip callAPI, so the first chain call still resumes;
|
|
278
|
+
// later same-domain calls reuse that session. Cardano may Ask [Standard,
|
|
279
|
+
// Cardano] once, which also covers later non-Cardano commands.
|
|
280
|
+
private protocolV2ResumedSeedDomains = new Set<'standard' | 'cardano'>();
|
|
281
|
+
|
|
276
282
|
init() {
|
|
277
283
|
this.checkDeviceId = true;
|
|
278
284
|
this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
|
|
@@ -320,6 +326,23 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
320
326
|
};
|
|
321
327
|
}
|
|
322
328
|
|
|
329
|
+
private hasProtocolV2WalletResume(deriveCardano?: boolean) {
|
|
330
|
+
if (deriveCardano) {
|
|
331
|
+
return this.protocolV2ResumedSeedDomains.has('cardano');
|
|
332
|
+
}
|
|
333
|
+
return (
|
|
334
|
+
this.protocolV2ResumedSeedDomains.has('standard') ||
|
|
335
|
+
this.protocolV2ResumedSeedDomains.has('cardano')
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private markProtocolV2WalletResumed(deriveCardano?: boolean) {
|
|
340
|
+
this.protocolV2ResumedSeedDomains.add('standard');
|
|
341
|
+
if (deriveCardano) {
|
|
342
|
+
this.protocolV2ResumedSeedDomains.add('cardano');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
323
346
|
async callMethod(
|
|
324
347
|
methodName: keyof CoreApi,
|
|
325
348
|
params: any & {
|
|
@@ -395,16 +418,17 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
395
418
|
}
|
|
396
419
|
}
|
|
397
420
|
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
// requested standard or hidden wallet before sending its device command.
|
|
421
|
+
// Nested chain methods skip callAPI's session gate. Resume the requested
|
|
422
|
+
// wallet once per seed domain; DeviceSessionGet is sticky like V1
|
|
423
|
+
// Initialize, so later addresses and chains reuse it.
|
|
402
424
|
const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
|
|
403
|
-
// Nested Cardano methods opt in to [Standard, Cardano] if Ask rebuilds.
|
|
404
|
-
// Other chains stay Standard-only.
|
|
405
425
|
const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
|
|
406
426
|
const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
|
|
407
|
-
if (
|
|
427
|
+
if (
|
|
428
|
+
this.device.isProtocolV2() &&
|
|
429
|
+
shouldResumeWalletSession &&
|
|
430
|
+
!this.hasProtocolV2WalletResume(deriveCardano)
|
|
431
|
+
) {
|
|
408
432
|
const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
|
|
409
433
|
this.payload.passphraseState,
|
|
410
434
|
useEmptyPassphrase,
|
|
@@ -415,6 +439,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
415
439
|
if (!passphraseStateSafety) {
|
|
416
440
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
417
441
|
}
|
|
442
|
+
this.markProtocolV2WalletResumed(deriveCardano);
|
|
418
443
|
}
|
|
419
444
|
},
|
|
420
445
|
});
|
package/src/core/index.ts
CHANGED
|
@@ -84,7 +84,7 @@ import type { BaseMethod } from '../api/BaseMethod';
|
|
|
84
84
|
|
|
85
85
|
const Log = getLogger(LoggerNames.Core);
|
|
86
86
|
const PRE_INITIALIZE_TTL_MS = 60 * 1000;
|
|
87
|
-
const PRE_PENDING_CALL_TIMEOUT_MS =
|
|
87
|
+
const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 1000;
|
|
88
88
|
const PRO2_USB_SIGNING_COOLDOWN_MS = 1000;
|
|
89
89
|
|
|
90
90
|
// Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
|
|
@@ -183,7 +183,6 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
|
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
method.setContext?.(context);
|
|
186
|
-
method.context = context;
|
|
187
186
|
|
|
188
187
|
method.requestContext = createRequestContext(method.responseID, method.name, {
|
|
189
188
|
sdkInstanceId: context.sdkInstanceId,
|
|
@@ -204,12 +203,7 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
|
|
|
204
203
|
if (!method.useDevice) {
|
|
205
204
|
updateMethodRequestContext(method, { status: 'running' });
|
|
206
205
|
try {
|
|
207
|
-
const
|
|
208
|
-
const response =
|
|
209
|
-
method.name === 'searchDevices' &&
|
|
210
|
-
(DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env))
|
|
211
|
-
? await context.methodSynchronize(() => method.run(), 'webusb-discovery')
|
|
212
|
-
: await method.run();
|
|
206
|
+
const response = await method.run();
|
|
213
207
|
completeMethodRequestContext(method);
|
|
214
208
|
return createResponseMessage(method.responseID, true, response);
|
|
215
209
|
} catch (error) {
|
|
@@ -321,27 +315,25 @@ const waitForPendingPromise = async (
|
|
|
321
315
|
if (pendingPromise) {
|
|
322
316
|
Log.debug('pre pending call promise before call method, wait for it');
|
|
323
317
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
324
|
-
let timedOut = false;
|
|
325
|
-
let completed = false;
|
|
326
318
|
try {
|
|
327
319
|
await Promise.race([
|
|
328
320
|
pendingPromise,
|
|
329
|
-
new Promise<void>(
|
|
321
|
+
new Promise<void>((_, reject) => {
|
|
330
322
|
timer = setTimeout(() => {
|
|
331
|
-
|
|
332
|
-
|
|
323
|
+
reject(
|
|
324
|
+
ERRORS.TypedError(
|
|
325
|
+
HardwareErrorCode.DeviceBusy,
|
|
326
|
+
'Previous device cancellation is still draining'
|
|
327
|
+
)
|
|
328
|
+
);
|
|
333
329
|
}, PRE_PENDING_CALL_TIMEOUT_MS);
|
|
334
330
|
}),
|
|
335
331
|
]);
|
|
336
|
-
|
|
332
|
+
// A deadline is not evidence that old I/O is safe to reuse. Keep the
|
|
333
|
+
// barrier on failure; a later call may proceed only after cleanup settles.
|
|
334
|
+
removePrePendingCallPromise?.(connectId, pendingPromise);
|
|
337
335
|
} finally {
|
|
338
336
|
if (timer) clearTimeout(timer);
|
|
339
|
-
// Keep a rejected cleanup barrier for the next caller's safety check;
|
|
340
|
-
// only a completed cleanup or a timeout may clear it.
|
|
341
|
-
if (timedOut || completed) removePrePendingCallPromise?.(connectId, pendingPromise);
|
|
342
|
-
}
|
|
343
|
-
if (timedOut) {
|
|
344
|
-
Log.warn('pre pending call promise timed out before call method', { connectId });
|
|
345
337
|
}
|
|
346
338
|
Log.debug('pre pending call promise before call method done');
|
|
347
339
|
}
|
|
@@ -419,26 +411,7 @@ const onCallDevice = async (
|
|
|
419
411
|
* Polling to ensure successful connection
|
|
420
412
|
*/
|
|
421
413
|
const pollingId = pollingManager.start(connectId);
|
|
422
|
-
|
|
423
|
-
const connect = () =>
|
|
424
|
-
ensureConnected(context, method, connectId, pollingId, method.abortSignal);
|
|
425
|
-
// Discovery may acquire USB endpoints. Finish it before initializing a public
|
|
426
|
-
// request; once registered, that request makes discovery use cached state only.
|
|
427
|
-
if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
|
|
428
|
-
// Synchronization can keep the connect action queued after the caller is
|
|
429
|
-
// cancelled. Observe that promise so a late device-not-found error does
|
|
430
|
-
// not become an unhandled rejection.
|
|
431
|
-
const connectPromise = context.methodSynchronize(async () => {
|
|
432
|
-
if (method.abortSignal?.aborted) {
|
|
433
|
-
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
434
|
-
}
|
|
435
|
-
return connect();
|
|
436
|
-
}, 'webusb-discovery');
|
|
437
|
-
connectPromise.catch(() => undefined);
|
|
438
|
-
device = await requestQueue.waitForTask(task, () => connectPromise);
|
|
439
|
-
} else {
|
|
440
|
-
device = await connect();
|
|
441
|
-
}
|
|
414
|
+
device = await ensureConnected(context, method, connectId, pollingId, method.abortSignal);
|
|
442
415
|
if (method.abortSignal?.aborted) {
|
|
443
416
|
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
444
417
|
}
|
|
@@ -1227,7 +1200,6 @@ const ensureConnected = async (
|
|
|
1227
1200
|
const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
|
|
1228
1201
|
const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
|
|
1229
1202
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1230
|
-
let lastInitializeError: any;
|
|
1231
1203
|
Log.debug(
|
|
1232
1204
|
`EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `
|
|
1233
1205
|
);
|
|
@@ -1268,9 +1240,7 @@ const ensureConnected = async (
|
|
|
1268
1240
|
Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
|
|
1269
1241
|
try {
|
|
1270
1242
|
await initDeviceList(method);
|
|
1271
|
-
lastInitializeError = undefined;
|
|
1272
1243
|
} catch (error) {
|
|
1273
|
-
lastInitializeError = error;
|
|
1274
1244
|
Log.debug('device list error: ', error);
|
|
1275
1245
|
if (
|
|
1276
1246
|
[
|
|
@@ -1285,7 +1255,6 @@ const ensureConnected = async (
|
|
|
1285
1255
|
}
|
|
1286
1256
|
if (error.errorCode === HardwareErrorCode.TransportNotConfigured) {
|
|
1287
1257
|
await TransportManager.configure();
|
|
1288
|
-
lastInitializeError = undefined;
|
|
1289
1258
|
}
|
|
1290
1259
|
}
|
|
1291
1260
|
|
|
@@ -1383,22 +1352,14 @@ const ensureConnected = async (
|
|
|
1383
1352
|
clearTimeout(timer);
|
|
1384
1353
|
}
|
|
1385
1354
|
Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt;
|
|
1390
|
-
const fallbackError = needsPermissionPrompt
|
|
1391
|
-
? ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission)
|
|
1392
|
-
: ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
1393
|
-
const errorToReject =
|
|
1394
|
-
preserveWebUsbInitError && lastInitializeError ? lastInitializeError : fallbackError;
|
|
1395
|
-
// Only ask the host for a WebUSB grant when the failure is actually
|
|
1396
|
-
// "not found / needs permission". A preserved initialize error must
|
|
1397
|
-
// not fire that prompt with a different public code.
|
|
1398
|
-
if (needsPermissionPrompt && errorToReject === fallbackError) {
|
|
1355
|
+
// Browser WebUSB needs permission prompt, desktop WebUSB doesn't
|
|
1356
|
+
// skipWebDevicePrompt can override this behavior for special cases
|
|
1357
|
+
if (DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt) {
|
|
1399
1358
|
postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
|
|
1359
|
+
reject(ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
|
|
1360
|
+
} else {
|
|
1361
|
+
reject(ERRORS.TypedError(HardwareErrorCode.DeviceNotFound));
|
|
1400
1362
|
}
|
|
1401
|
-
reject(errorToReject);
|
|
1402
1363
|
return;
|
|
1403
1364
|
}
|
|
1404
1365
|
|
|
@@ -33,8 +33,6 @@ export default class TransportManager {
|
|
|
33
33
|
|
|
34
34
|
static reactNativeInit = false;
|
|
35
35
|
|
|
36
|
-
static webUsbInit = false;
|
|
37
|
-
|
|
38
36
|
static protocolV1MessageSchema: ProtocolV1MessageSchema = 'v1CurrentSchema';
|
|
39
37
|
|
|
40
38
|
static plugin: LowlevelTransportSharedPlugin | null = null;
|
|
@@ -44,17 +42,6 @@ export default class TransportManager {
|
|
|
44
42
|
this.defaultMessages = DataManager.getProtobufMessages();
|
|
45
43
|
this.currentMessages = this.defaultMessages;
|
|
46
44
|
this.protocolV1MessageSchema = 'v1CurrentSchema';
|
|
47
|
-
this.webUsbInit = false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static async ensureInitialized() {
|
|
51
|
-
const env = DataManager.getSettings('env');
|
|
52
|
-
if (env !== 'webusb' && env !== 'desktop-webusb') return;
|
|
53
|
-
if (this.webUsbInit) return;
|
|
54
|
-
// The emitter registers USB disconnect events; this must happen even when
|
|
55
|
-
// schema configuration is intentionally deferred during discovery.
|
|
56
|
-
await this.transport.init(WebUsbLogger, DevicePool.emitter);
|
|
57
|
-
this.webUsbInit = true;
|
|
58
45
|
}
|
|
59
46
|
|
|
60
47
|
static async configure() {
|
|
@@ -82,7 +69,9 @@ export default class TransportManager {
|
|
|
82
69
|
} else if (env === 'desktop-web-ble') {
|
|
83
70
|
await this.transport.init(WebBleLogger, DevicePool.emitter);
|
|
84
71
|
} else if (env === 'webusb' || env === 'desktop-webusb') {
|
|
85
|
-
|
|
72
|
+
// The emitter is what turns a navigator.usb 'disconnect' into a
|
|
73
|
+
// DEVICE.DISCONNECT; without it WebUSB never reports device removal.
|
|
74
|
+
await this.transport.init(WebUsbLogger, DevicePool.emitter);
|
|
86
75
|
} else {
|
|
87
76
|
await this.transport.init(HttpLogger);
|
|
88
77
|
}
|
package/src/device/Device.ts
CHANGED
|
@@ -1624,13 +1624,6 @@ export class Device extends EventEmitter {
|
|
|
1624
1624
|
} else if (!acquired) {
|
|
1625
1625
|
// Abort setup without acquiring a session just to send Cancel.
|
|
1626
1626
|
if (mainId && deviceConnector?.disconnect) {
|
|
1627
|
-
// The connector drops the link silently, so without this line a
|
|
1628
|
-
// user-cancel teardown is indistinguishable in field logs from an
|
|
1629
|
-
// idle keep-alive release or a device that left on its own.
|
|
1630
|
-
Log.debug(
|
|
1631
|
-
'interruptionFromUser: disconnecting device without acquire, mainId:',
|
|
1632
|
-
mainId
|
|
1633
|
-
);
|
|
1634
1627
|
await deviceConnector.disconnect(mainId);
|
|
1635
1628
|
}
|
|
1636
1629
|
if (this.connectionAttempt === attempt) this.markTransportDisconnected();
|