@onekeyfe/hd-core 1.2.2-alpha.120 → 1.2.2-alpha.122
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 +57 -7
- package/__tests__/core-error-output.test.ts +77 -0
- package/__tests__/device-lifecycle-events.test.ts +8 -8
- package/__tests__/search-devices.test.ts +196 -8
- package/dist/api/SearchDevices.d.ts +2 -15
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +1 -1
- 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 +2 -0
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.js +207 -77
- package/package.json +4 -4
- package/src/api/SearchDevices.ts +121 -27
- package/src/api/allnetwork/AllNetworkGetAddress.ts +66 -59
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +8 -3
- package/src/core/index.ts +58 -19
- package/src/data-manager/TransportManager.ts +14 -3
- package/src/device/Device.ts +7 -0
package/src/api/SearchDevices.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { EDeviceType, canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import { BaseMethod } from './BaseMethod';
|
|
4
4
|
import TransportManager from '../data-manager/TransportManager';
|
|
@@ -6,10 +6,80 @@ 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';
|
|
9
10
|
import type DeviceConnector from '../device/DeviceConnector';
|
|
11
|
+
import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
|
|
12
|
+
import type { Device } from '../device/Device';
|
|
10
13
|
|
|
11
14
|
const Log = getLogger(LoggerNames.DevicePool);
|
|
12
15
|
|
|
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
|
+
|
|
13
83
|
export default class SearchDevices extends BaseMethod {
|
|
14
84
|
connector?: DeviceConnector;
|
|
15
85
|
|
|
@@ -19,19 +89,31 @@ export default class SearchDevices extends BaseMethod {
|
|
|
19
89
|
this.skipForceUpdateCheck = true;
|
|
20
90
|
}
|
|
21
91
|
|
|
22
|
-
async run() {
|
|
23
|
-
|
|
92
|
+
async run(): Promise<SearchDevice[]> {
|
|
93
|
+
const env = DataManager.getSettings('env');
|
|
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();
|
|
24
108
|
const deviceDiff = await this.connector?.enumerate();
|
|
25
109
|
const devicesDescriptor = deviceDiff?.descriptors ?? [];
|
|
26
110
|
|
|
27
|
-
const env = DataManager.getSettings('env');
|
|
28
|
-
|
|
29
111
|
/**
|
|
30
112
|
* No need to call features during Bluetooth scaning
|
|
31
113
|
* to avoid device pairing
|
|
32
114
|
*/
|
|
33
115
|
if (DataManager.isBleConnect(env)) {
|
|
34
|
-
const devices = [];
|
|
116
|
+
const devices: SearchDevice[] = [];
|
|
35
117
|
const seenIds = new Set<string>();
|
|
36
118
|
|
|
37
119
|
for (const device of devicesDescriptor) {
|
|
@@ -56,30 +138,42 @@ export default class SearchDevices extends BaseMethod {
|
|
|
56
138
|
return devices;
|
|
57
139
|
}
|
|
58
140
|
|
|
59
|
-
const deviceList = [];
|
|
141
|
+
const deviceList: SearchDevice[] = [];
|
|
60
142
|
for (const descriptor of devicesDescriptor) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
143
|
+
if (hasActiveWebUsbRequest && isOwnedByActiveWebUsbRequest(descriptor, requestQueue)) {
|
|
144
|
+
const cached = DevicePool.getDeviceByPath(descriptor.path);
|
|
145
|
+
const known = cached?.features ? cached : getOwningRequestDevice(descriptor, requestQueue);
|
|
146
|
+
const message = known ? toSearchDevice(known) : null;
|
|
147
|
+
// Do not probe a path an in-flight request already owns. A cache miss is still a
|
|
148
|
+
// connected device: report the owning request's device, or the path with no identity.
|
|
149
|
+
deviceList.push(message ?? toSearchDeviceFromDescriptor(descriptor));
|
|
150
|
+
} else {
|
|
151
|
+
try {
|
|
152
|
+
// Discovery is best effort. Browsers may retain WebUSB grants for devices that
|
|
153
|
+
// are offline, busy, or not ready, so one descriptor must not abort the scan.
|
|
154
|
+
const result = await DevicePool.getDevices([descriptor], descriptor.path, {
|
|
155
|
+
// Discovery must actively identify the protocol instead of trusting a caller hint.
|
|
156
|
+
connectProtocol: undefined,
|
|
157
|
+
forceProtocolDetection: true,
|
|
158
|
+
refreshRuntimeState: true,
|
|
159
|
+
});
|
|
160
|
+
for (const device of result.deviceList) {
|
|
161
|
+
const message = toSearchDevice(device);
|
|
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
|
+
}
|
|
80
174
|
}
|
|
81
175
|
}
|
|
82
176
|
|
|
83
|
-
return deviceList
|
|
177
|
+
return deviceList;
|
|
84
178
|
}
|
|
85
179
|
}
|
|
@@ -7,21 +7,74 @@ import AllNetworkGetAddressBase from './AllNetworkGetAddressBase';
|
|
|
7
7
|
import type { CoreApi } from '../../types';
|
|
8
8
|
import type {
|
|
9
9
|
AllNetworkAddress,
|
|
10
|
-
AllNetworkAddressParams,
|
|
11
10
|
AllNetworkGetAddressParams,
|
|
12
11
|
} from '../../types/api/allNetworkGetAddress';
|
|
13
12
|
|
|
14
|
-
type MethodParams =
|
|
15
|
-
methodName: keyof CoreApi;
|
|
16
|
-
params: Parameters<CoreApi[keyof CoreApi]>[0];
|
|
17
|
-
_originRequestParams: AllNetworkAddressParams;
|
|
18
|
-
_originalIndex: number;
|
|
19
|
-
};
|
|
13
|
+
type MethodParams = ReturnType<AllNetworkGetAddressBase['generateMethodName']>;
|
|
20
14
|
|
|
21
15
|
export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
16
|
+
private checkAborted() {
|
|
17
|
+
if (this.abortController?.signal.aborted) {
|
|
18
|
+
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async callAddressGroup(
|
|
23
|
+
methodName: keyof CoreApi,
|
|
24
|
+
params: MethodParams[],
|
|
25
|
+
rootFingerprint: number
|
|
26
|
+
): Promise<AllNetworkAddress[]> {
|
|
27
|
+
const methodCallParams = { bundle: params.map(param => ({ ...param.params })) };
|
|
28
|
+
if (!this.device.isProtocolV2() || params.length === 1) {
|
|
29
|
+
return this.callMethod(methodName, methodCallParams, rootFingerprint);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const postedAddressCounts = new Map<string, number>();
|
|
33
|
+
let runningIndividually = false;
|
|
34
|
+
const postMessage: typeof this.postMessage = message => {
|
|
35
|
+
if (message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT) {
|
|
36
|
+
const { path, address } = message.payload.data;
|
|
37
|
+
const key = JSON.stringify([path, address]);
|
|
38
|
+
const count = postedAddressCounts.get(key) ?? 0;
|
|
39
|
+
if (runningIndividually && count > 0) {
|
|
40
|
+
postedAddressCounts.set(key, count - 1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!runningIndividually) postedAddressCounts.set(key, count + 1);
|
|
44
|
+
}
|
|
45
|
+
this.postMessage(message);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Only silent reads may be replayed. Forward their notifications immediately,
|
|
49
|
+
// then suppress matching retry copies by count so repeated inputs still emit.
|
|
50
|
+
if (params.every(param => param._originRequestParams.showOnOneKey === false)) {
|
|
51
|
+
const response = await this.callMethod(
|
|
52
|
+
methodName,
|
|
53
|
+
methodCallParams,
|
|
54
|
+
rootFingerprint,
|
|
55
|
+
postMessage
|
|
56
|
+
);
|
|
57
|
+
// Skippable errors become failed items; link, cancellation and wallet errors throw.
|
|
58
|
+
if (response.some(item => item.success)) return response;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
runningIndividually = true;
|
|
62
|
+
const responses: AllNetworkAddress[] = [];
|
|
63
|
+
for (const param of params) {
|
|
64
|
+
this.checkAborted();
|
|
65
|
+
const response = await this.callMethod(
|
|
66
|
+
methodName,
|
|
67
|
+
{ bundle: [{ ...param.params }] },
|
|
68
|
+
rootFingerprint,
|
|
69
|
+
postMessage
|
|
70
|
+
);
|
|
71
|
+
responses.push(...response);
|
|
72
|
+
}
|
|
73
|
+
return responses;
|
|
74
|
+
}
|
|
75
|
+
|
|
22
76
|
async getAllNetworkAddress(rootFingerprint: number) {
|
|
23
77
|
const responses: AllNetworkAddress[] = [];
|
|
24
|
-
const resultMap: Record<string, AllNetworkAddress> = {};
|
|
25
78
|
const { bundle } = this.payload as AllNetworkGetAddressParams;
|
|
26
79
|
|
|
27
80
|
const methodParams = bundle.map((param, index) =>
|
|
@@ -44,55 +97,13 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
|
44
97
|
|
|
45
98
|
let processed = 0;
|
|
46
99
|
for (const [methodName, params] of methodGroups.entries()) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
if (this.abortController?.signal.aborted) {
|
|
54
|
-
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
55
|
-
}
|
|
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
|
-
}
|
|
88
|
-
|
|
89
|
-
if (this.abortController?.signal.aborted) {
|
|
90
|
-
throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
|
|
91
|
-
}
|
|
100
|
+
this.checkAborted();
|
|
101
|
+
const response = await this.callAddressGroup(methodName, params, rootFingerprint);
|
|
102
|
+
this.checkAborted();
|
|
92
103
|
|
|
93
104
|
for (let index = 0; index < params.length; index++) {
|
|
94
105
|
const { _originRequestParams, _originalIndex } = params[index];
|
|
95
|
-
|
|
106
|
+
responses[_originalIndex] = {
|
|
96
107
|
..._originRequestParams,
|
|
97
108
|
...response[index],
|
|
98
109
|
};
|
|
@@ -105,11 +116,7 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
|
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
for (let i = 0; i < bundle.length; i++) {
|
|
109
|
-
responses.push(resultMap[i]);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
119
|
this.abortController = null;
|
|
113
|
-
return
|
|
120
|
+
return responses;
|
|
114
121
|
}
|
|
115
122
|
}
|
|
@@ -348,7 +348,8 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
348
348
|
params: any & {
|
|
349
349
|
bundle: (any & { _originRequestParams: CommonResponseParams })[];
|
|
350
350
|
},
|
|
351
|
-
rootFingerprint: number
|
|
351
|
+
rootFingerprint: number,
|
|
352
|
+
postMessage = this.postMessage
|
|
352
353
|
) {
|
|
353
354
|
const method: BaseMethod = findMethod({
|
|
354
355
|
event: IFRAME.CALL,
|
|
@@ -362,7 +363,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
362
363
|
});
|
|
363
364
|
|
|
364
365
|
method.connector = this.connector;
|
|
365
|
-
method.postMessage =
|
|
366
|
+
method.postMessage = postMessage;
|
|
366
367
|
if (this.context) {
|
|
367
368
|
method.setContext?.(this.context);
|
|
368
369
|
}
|
|
@@ -506,7 +507,11 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
506
507
|
async run() {
|
|
507
508
|
this.loadingCleanupInBackground = false;
|
|
508
509
|
try {
|
|
509
|
-
if (
|
|
510
|
+
if (
|
|
511
|
+
this.device.isProtocolV2() &&
|
|
512
|
+
(this.device.getCurrentDeviceType() === EDeviceType.Pro2 ||
|
|
513
|
+
this.device.getCurrentDeviceType() === EDeviceType.Neo)
|
|
514
|
+
) {
|
|
510
515
|
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
511
516
|
if (supportsProtocolV2Message(protocolInfo, 60461)) {
|
|
512
517
|
const { commands } = this.device;
|
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 = 5 * 1000;
|
|
88
88
|
const PRO2_USB_SIGNING_COOLDOWN_MS = 1000;
|
|
89
89
|
|
|
90
90
|
// Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
|
|
@@ -183,6 +183,7 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
|
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
method.setContext?.(context);
|
|
186
|
+
method.context = context;
|
|
186
187
|
|
|
187
188
|
method.requestContext = createRequestContext(method.responseID, method.name, {
|
|
188
189
|
sdkInstanceId: context.sdkInstanceId,
|
|
@@ -203,7 +204,12 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
|
|
|
203
204
|
if (!method.useDevice) {
|
|
204
205
|
updateMethodRequestContext(method, { status: 'running' });
|
|
205
206
|
try {
|
|
206
|
-
const
|
|
207
|
+
const env = DataManager.getSettings('env');
|
|
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();
|
|
207
213
|
completeMethodRequestContext(method);
|
|
208
214
|
return createResponseMessage(method.responseID, true, response);
|
|
209
215
|
} catch (error) {
|
|
@@ -315,25 +321,27 @@ const waitForPendingPromise = async (
|
|
|
315
321
|
if (pendingPromise) {
|
|
316
322
|
Log.debug('pre pending call promise before call method, wait for it');
|
|
317
323
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
324
|
+
let timedOut = false;
|
|
325
|
+
let completed = false;
|
|
318
326
|
try {
|
|
319
327
|
await Promise.race([
|
|
320
328
|
pendingPromise,
|
|
321
|
-
new Promise<void>(
|
|
329
|
+
new Promise<void>(resolve => {
|
|
322
330
|
timer = setTimeout(() => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
HardwareErrorCode.DeviceBusy,
|
|
326
|
-
'Previous device cancellation is still draining'
|
|
327
|
-
)
|
|
328
|
-
);
|
|
331
|
+
timedOut = true;
|
|
332
|
+
resolve();
|
|
329
333
|
}, PRE_PENDING_CALL_TIMEOUT_MS);
|
|
330
334
|
}),
|
|
331
335
|
]);
|
|
332
|
-
|
|
333
|
-
// barrier on failure; a later call may proceed only after cleanup settles.
|
|
334
|
-
removePrePendingCallPromise?.(connectId, pendingPromise);
|
|
336
|
+
completed = !timedOut;
|
|
335
337
|
} finally {
|
|
336
338
|
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 });
|
|
337
345
|
}
|
|
338
346
|
Log.debug('pre pending call promise before call method done');
|
|
339
347
|
}
|
|
@@ -411,7 +419,26 @@ const onCallDevice = async (
|
|
|
411
419
|
* Polling to ensure successful connection
|
|
412
420
|
*/
|
|
413
421
|
const pollingId = pollingManager.start(connectId);
|
|
414
|
-
|
|
422
|
+
const env = DataManager.getSettings('env');
|
|
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
|
+
}
|
|
415
442
|
if (method.abortSignal?.aborted) {
|
|
416
443
|
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
|
|
417
444
|
}
|
|
@@ -1200,6 +1227,7 @@ const ensureConnected = async (
|
|
|
1200
1227
|
const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
|
|
1201
1228
|
const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
|
|
1202
1229
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1230
|
+
let lastInitializeError: any;
|
|
1203
1231
|
Log.debug(
|
|
1204
1232
|
`EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `
|
|
1205
1233
|
);
|
|
@@ -1240,7 +1268,9 @@ const ensureConnected = async (
|
|
|
1240
1268
|
Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
|
|
1241
1269
|
try {
|
|
1242
1270
|
await initDeviceList(method);
|
|
1271
|
+
lastInitializeError = undefined;
|
|
1243
1272
|
} catch (error) {
|
|
1273
|
+
lastInitializeError = error;
|
|
1244
1274
|
Log.debug('device list error: ', error);
|
|
1245
1275
|
if (
|
|
1246
1276
|
[
|
|
@@ -1255,6 +1285,7 @@ const ensureConnected = async (
|
|
|
1255
1285
|
}
|
|
1256
1286
|
if (error.errorCode === HardwareErrorCode.TransportNotConfigured) {
|
|
1257
1287
|
await TransportManager.configure();
|
|
1288
|
+
lastInitializeError = undefined;
|
|
1258
1289
|
}
|
|
1259
1290
|
}
|
|
1260
1291
|
|
|
@@ -1352,14 +1383,22 @@ const ensureConnected = async (
|
|
|
1352
1383
|
clearTimeout(timer);
|
|
1353
1384
|
}
|
|
1354
1385
|
Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1386
|
+
const preserveWebUsbInitError =
|
|
1387
|
+
DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
|
|
1388
|
+
const needsPermissionPrompt =
|
|
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) {
|
|
1358
1399
|
postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
|
|
1359
|
-
reject(ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
|
|
1360
|
-
} else {
|
|
1361
|
-
reject(ERRORS.TypedError(HardwareErrorCode.DeviceNotFound));
|
|
1362
1400
|
}
|
|
1401
|
+
reject(errorToReject);
|
|
1363
1402
|
return;
|
|
1364
1403
|
}
|
|
1365
1404
|
|
|
@@ -33,6 +33,8 @@ export default class TransportManager {
|
|
|
33
33
|
|
|
34
34
|
static reactNativeInit = false;
|
|
35
35
|
|
|
36
|
+
static webUsbInit = false;
|
|
37
|
+
|
|
36
38
|
static protocolV1MessageSchema: ProtocolV1MessageSchema = 'v1CurrentSchema';
|
|
37
39
|
|
|
38
40
|
static plugin: LowlevelTransportSharedPlugin | null = null;
|
|
@@ -42,6 +44,17 @@ export default class TransportManager {
|
|
|
42
44
|
this.defaultMessages = DataManager.getProtobufMessages();
|
|
43
45
|
this.currentMessages = this.defaultMessages;
|
|
44
46
|
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;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
static async configure() {
|
|
@@ -69,9 +82,7 @@ export default class TransportManager {
|
|
|
69
82
|
} else if (env === 'desktop-web-ble') {
|
|
70
83
|
await this.transport.init(WebBleLogger, DevicePool.emitter);
|
|
71
84
|
} else if (env === 'webusb' || env === 'desktop-webusb') {
|
|
72
|
-
|
|
73
|
-
// DEVICE.DISCONNECT; without it WebUSB never reports device removal.
|
|
74
|
-
await this.transport.init(WebUsbLogger, DevicePool.emitter);
|
|
85
|
+
await this.ensureInitialized();
|
|
75
86
|
} else {
|
|
76
87
|
await this.transport.init(HttpLogger);
|
|
77
88
|
}
|
package/src/device/Device.ts
CHANGED
|
@@ -1624,6 +1624,13 @@ 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
|
+
);
|
|
1627
1634
|
await deviceConnector.disconnect(mainId);
|
|
1628
1635
|
}
|
|
1629
1636
|
if (this.connectionAttempt === attempt) this.markTransportDisconnected();
|