@onekeyfe/hd-core 1.1.34-alpha.0 → 1.1.34-alpha.2
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__/firmware-update/firmware-update-v2-download-before-boot.test.ts +394 -0
- package/__tests__/networkUtils.test.ts +386 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +7 -3
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +318 -44
- package/dist/utils/assets.d.ts +2 -1
- package/dist/utils/assets.d.ts.map +1 -1
- package/dist/utils/networkUtils.d.ts +9 -1
- package/dist/utils/networkUtils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV2.ts +122 -27
- package/src/api/firmware/getBinary.ts +8 -3
- package/src/core/index.ts +11 -0
- package/src/utils/assets.ts +4 -1
- package/src/utils/networkUtils.ts +270 -14
|
@@ -7,9 +7,12 @@ import { findLatestRelease } from '../../utils/release';
|
|
|
7
7
|
import { getFirmwareUpdateField } from '../../utils/deviceFeaturesUtils';
|
|
8
8
|
|
|
9
9
|
import type { Features } from '../../types';
|
|
10
|
+
import type { HttpRequestOptions } from '../../utils/networkUtils';
|
|
10
11
|
import type { EFirmwareType } from '@onekeyfe/hd-shared';
|
|
11
12
|
import type { IFirmwareField } from '../../data-manager/DataManager';
|
|
12
13
|
|
|
14
|
+
export type FirmwareBinary = ArrayBuffer | Buffer;
|
|
15
|
+
|
|
13
16
|
export interface GetInfoProps {
|
|
14
17
|
features: Features;
|
|
15
18
|
updateType: 'firmware' | 'ble';
|
|
@@ -20,6 +23,7 @@ export interface GetInfoProps {
|
|
|
20
23
|
|
|
21
24
|
interface GetBinaryProps extends GetInfoProps {
|
|
22
25
|
version?: number[];
|
|
26
|
+
requestOptions?: HttpRequestOptions;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export const getBinary = async ({
|
|
@@ -28,6 +32,7 @@ export const getBinary = async ({
|
|
|
28
32
|
version,
|
|
29
33
|
isUpdateBootloader,
|
|
30
34
|
firmwareType,
|
|
35
|
+
requestOptions,
|
|
31
36
|
}: GetBinaryProps) => {
|
|
32
37
|
const releaseInfo = getInfo({
|
|
33
38
|
features,
|
|
@@ -55,9 +60,9 @@ export const getBinary = async ({
|
|
|
55
60
|
: isUpdateBootloader
|
|
56
61
|
? releaseInfo.bootloaderResource
|
|
57
62
|
: releaseInfo.url;
|
|
58
|
-
let fw;
|
|
63
|
+
let fw: FirmwareBinary;
|
|
59
64
|
try {
|
|
60
|
-
fw = await httpRequest(url, 'binary');
|
|
65
|
+
fw = await httpRequest(url, 'binary', requestOptions);
|
|
61
66
|
} catch {
|
|
62
67
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Method_FirmwareUpdate_DownloadFailed');
|
|
63
68
|
}
|
|
@@ -69,7 +74,7 @@ export const getBinary = async ({
|
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
export const getSysResourceBinary = async (url: string) => {
|
|
72
|
-
let fw;
|
|
77
|
+
let fw: FirmwareBinary;
|
|
73
78
|
try {
|
|
74
79
|
fw = await httpRequest(url, 'binary');
|
|
75
80
|
} catch {
|
package/src/core/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import semver from 'semver';
|
|
|
2
2
|
import EventEmitter from 'events';
|
|
3
3
|
import {
|
|
4
4
|
ERRORS,
|
|
5
|
+
ERROR_CODES_REQUIRE_DISCONNECT,
|
|
5
6
|
ERROR_CODES_REQUIRE_RELEASE,
|
|
6
7
|
HardwareError,
|
|
7
8
|
HardwareErrorCode,
|
|
@@ -832,6 +833,16 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
|
|
|
832
833
|
});
|
|
833
834
|
}
|
|
834
835
|
} catch (err) {
|
|
836
|
+
// Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
|
|
837
|
+
// failures — drop the wedged link here so retries cold-connect instead of
|
|
838
|
+
// reusing it forever.
|
|
839
|
+
if (
|
|
840
|
+
ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) &&
|
|
841
|
+
device.mainId &&
|
|
842
|
+
device.deviceConnector
|
|
843
|
+
) {
|
|
844
|
+
await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
|
|
845
|
+
}
|
|
835
846
|
if (err.errorCode === HardwareErrorCode.BleTimeoutError && retryCount < 6) {
|
|
836
847
|
const nextRetry = retryCount + 1;
|
|
837
848
|
Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
|
package/src/utils/assets.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { httpRequest as browserHttpRequest } from './networkUtils';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { HttpRequestOptions } from './networkUtils';
|
|
4
|
+
|
|
5
|
+
export const httpRequest = (url: string, type: string, options?: HttpRequestOptions): any =>
|
|
6
|
+
browserHttpRequest(url, type, options);
|
|
4
7
|
|
|
5
8
|
export const getTimeStamp = () => new Date().getTime();
|
|
@@ -1,25 +1,281 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
1
|
+
import axios, { type AxiosResponse } from 'axios';
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export type HttpRequestOptions = {
|
|
4
|
+
/** Axios adapter-defined request timeout retained for existing opt-in callers. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Deadline for DNS, connection, TLS, redirects, and the first response-body progress. */
|
|
7
|
+
connectTimeoutMs?: number;
|
|
8
|
+
/** Maximum idle interval between response-body progress events. */
|
|
9
|
+
readTimeoutMs?: number;
|
|
10
|
+
/** Wall-clock deadline across all attempts and retry backoff. */
|
|
11
|
+
overallTimeoutMs?: number;
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
retryDelayMs?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MAX_RETRY_COUNT = 3;
|
|
17
|
+
const REQUEST_PHASE_TIMEOUT_CODES = {
|
|
18
|
+
connect: 'ECONNECTTIMEDOUT',
|
|
19
|
+
read: 'EREADTIMEDOUT',
|
|
20
|
+
} as const;
|
|
21
|
+
const RETRYABLE_HTTP_STATUSES = new Set([500, 502, 503, 504]);
|
|
22
|
+
const RETRYABLE_NETWORK_ERROR_CODES = new Set([
|
|
23
|
+
REQUEST_PHASE_TIMEOUT_CODES.connect,
|
|
24
|
+
REQUEST_PHASE_TIMEOUT_CODES.read,
|
|
25
|
+
'ECONNABORTED',
|
|
26
|
+
'ECONNREFUSED',
|
|
27
|
+
'ECONNRESET',
|
|
28
|
+
'EHOSTUNREACH',
|
|
29
|
+
'ENETDOWN',
|
|
30
|
+
'ENETUNREACH',
|
|
31
|
+
'ENOTFOUND',
|
|
32
|
+
'EAI_AGAIN',
|
|
33
|
+
'ERR_NETWORK',
|
|
34
|
+
'ETIMEDOUT',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const createAbortError = () => {
|
|
38
|
+
const error = new Error('httpRequest aborted');
|
|
39
|
+
error.name = 'AbortError';
|
|
40
|
+
return error;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createOverallTimeoutError = (url: string, timeoutMs: number) => {
|
|
44
|
+
const error = new Error(`httpRequest overall timeout: ${url} ${timeoutMs}ms`);
|
|
45
|
+
error.name = 'HttpRequestOverallTimeoutError';
|
|
46
|
+
return error;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const createRequestPhaseTimeoutError = (
|
|
50
|
+
url: string,
|
|
51
|
+
phase: keyof typeof REQUEST_PHASE_TIMEOUT_CODES,
|
|
52
|
+
timeoutMs: number
|
|
53
|
+
) => {
|
|
54
|
+
const error = new Error(`httpRequest ${phase} timeout: ${url} ${timeoutMs}ms`);
|
|
55
|
+
error.name = 'HttpRequestPhaseTimeoutError';
|
|
56
|
+
return Object.assign(error, {
|
|
57
|
+
code: REQUEST_PHASE_TIMEOUT_CODES[phase],
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const waitForRetry = async (delayMs: number, attempt: number, signal?: AbortSignal) => {
|
|
62
|
+
const backoffMs = delayMs * 2 ** attempt;
|
|
63
|
+
if (backoffMs <= 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await new Promise<void>((resolve, reject) => {
|
|
67
|
+
const timeoutTimer = setTimeout(() => {
|
|
68
|
+
signal?.removeEventListener('abort', handleAbort);
|
|
69
|
+
resolve();
|
|
70
|
+
}, backoffMs);
|
|
71
|
+
const handleAbort = () => {
|
|
72
|
+
clearTimeout(timeoutTimer);
|
|
73
|
+
reject(createAbortError());
|
|
74
|
+
};
|
|
75
|
+
if (signal?.aborted) {
|
|
76
|
+
handleAbort();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
signal?.addEventListener('abort', handleAbort, { once: true });
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isRetryableRequestError = (error: unknown) => {
|
|
84
|
+
if (axios.isCancel(error)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
typeof error === 'object' &&
|
|
89
|
+
error !== null &&
|
|
90
|
+
'code' in error &&
|
|
91
|
+
typeof error.code === 'string' &&
|
|
92
|
+
RETRYABLE_NETWORK_ERROR_CODES.has(error.code)
|
|
93
|
+
) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (!axios.isAxiosError(error)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const status = error.response?.status;
|
|
100
|
+
if (status !== undefined) {
|
|
101
|
+
return RETRYABLE_HTTP_STATUSES.has(Number(status));
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const httpRequest = async <T = unknown>(
|
|
107
|
+
url: string,
|
|
108
|
+
type = 'text',
|
|
109
|
+
options: HttpRequestOptions = {}
|
|
110
|
+
): Promise<T> => {
|
|
4
111
|
const headers: any = {};
|
|
5
112
|
if (url.indexOf('ngrok-free.app') > -1) {
|
|
6
113
|
headers['ngrok-skip-browser-warning'] = true;
|
|
7
114
|
}
|
|
8
|
-
const response = await axios.request({
|
|
9
|
-
url,
|
|
10
|
-
withCredentials: false,
|
|
11
|
-
responseType: type === 'binary' ? 'arraybuffer' : 'json',
|
|
12
|
-
headers,
|
|
13
|
-
});
|
|
14
115
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
116
|
+
const timeoutMs =
|
|
117
|
+
Number.isSafeInteger(options.timeoutMs) && Number(options.timeoutMs) > 0
|
|
118
|
+
? Number(options.timeoutMs)
|
|
119
|
+
: undefined;
|
|
120
|
+
const connectTimeoutMs =
|
|
121
|
+
Number.isSafeInteger(options.connectTimeoutMs) && Number(options.connectTimeoutMs) > 0
|
|
122
|
+
? Number(options.connectTimeoutMs)
|
|
123
|
+
: undefined;
|
|
124
|
+
const readTimeoutMs =
|
|
125
|
+
Number.isSafeInteger(options.readTimeoutMs) && Number(options.readTimeoutMs) > 0
|
|
126
|
+
? Number(options.readTimeoutMs)
|
|
127
|
+
: undefined;
|
|
128
|
+
const overallTimeoutMs =
|
|
129
|
+
Number.isSafeInteger(options.overallTimeoutMs) && Number(options.overallTimeoutMs) > 0
|
|
130
|
+
? Number(options.overallTimeoutMs)
|
|
131
|
+
: undefined;
|
|
132
|
+
const maxRetries =
|
|
133
|
+
Number.isSafeInteger(options.maxRetries) && Number(options.maxRetries) > 0
|
|
134
|
+
? Math.min(Number(options.maxRetries), MAX_RETRY_COUNT)
|
|
135
|
+
: 0;
|
|
136
|
+
const retryDelayMs =
|
|
137
|
+
Number.isSafeInteger(options.retryDelayMs) && Number(options.retryDelayMs) > 0
|
|
138
|
+
? Number(options.retryDelayMs)
|
|
139
|
+
: 0;
|
|
140
|
+
const overallTimeoutError = overallTimeoutMs
|
|
141
|
+
? createOverallTimeoutError(url, overallTimeoutMs)
|
|
142
|
+
: undefined;
|
|
143
|
+
const overallDeadlineAt = overallTimeoutMs ? Date.now() + overallTimeoutMs : undefined;
|
|
144
|
+
const assertWithinOverallDeadline = () => {
|
|
145
|
+
if (overallDeadlineAt !== undefined && overallTimeoutError && Date.now() >= overallDeadlineAt) {
|
|
146
|
+
throw overallTimeoutError;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const request = async (attempt: number, signal?: AbortSignal): Promise<T> => {
|
|
151
|
+
assertWithinOverallDeadline();
|
|
152
|
+
if (signal?.aborted) {
|
|
153
|
+
throw createAbortError();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let response: AxiosResponse<T> | undefined;
|
|
157
|
+
let requestError: unknown;
|
|
158
|
+
let requestFailed = false;
|
|
159
|
+
let phaseTimeoutError: ReturnType<typeof createRequestPhaseTimeoutError> | undefined;
|
|
160
|
+
let phaseTimeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
161
|
+
const hasPhaseDeadline = Boolean(connectTimeoutMs || readTimeoutMs);
|
|
162
|
+
const attemptController = signal || hasPhaseDeadline ? new AbortController() : undefined;
|
|
163
|
+
let attemptSettled = false;
|
|
164
|
+
let lastProgressLoaded = 0;
|
|
165
|
+
const handleParentAbort = () => {
|
|
166
|
+
attemptController?.abort();
|
|
167
|
+
};
|
|
168
|
+
const clearPhaseTimeout = () => {
|
|
169
|
+
if (phaseTimeoutTimer) {
|
|
170
|
+
clearTimeout(phaseTimeoutTimer);
|
|
171
|
+
phaseTimeoutTimer = undefined;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const armPhaseTimeout = (
|
|
175
|
+
phase: keyof typeof REQUEST_PHASE_TIMEOUT_CODES,
|
|
176
|
+
phaseTimeoutMs: number | undefined
|
|
177
|
+
) => {
|
|
178
|
+
clearPhaseTimeout();
|
|
179
|
+
if (!phaseTimeoutMs || !attemptController) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
phaseTimeoutTimer = setTimeout(() => {
|
|
183
|
+
phaseTimeoutError = createRequestPhaseTimeoutError(url, phase, phaseTimeoutMs);
|
|
184
|
+
attemptController.abort();
|
|
185
|
+
}, phaseTimeoutMs);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (signal) {
|
|
189
|
+
signal.addEventListener('abort', handleParentAbort, { once: true });
|
|
190
|
+
}
|
|
191
|
+
armPhaseTimeout('connect', connectTimeoutMs);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
response = await axios.request<T>({
|
|
195
|
+
url,
|
|
196
|
+
withCredentials: false,
|
|
197
|
+
responseType: type === 'binary' ? 'arraybuffer' : 'json',
|
|
198
|
+
headers,
|
|
199
|
+
...(timeoutMs ? { timeout: timeoutMs } : {}),
|
|
200
|
+
...(attemptController ? { signal: attemptController.signal } : {}),
|
|
201
|
+
...(hasPhaseDeadline
|
|
202
|
+
? {
|
|
203
|
+
adapter: ['xhr', 'http'],
|
|
204
|
+
onDownloadProgress: (progressEvent: { loaded: number }) => {
|
|
205
|
+
if (
|
|
206
|
+
attemptSettled ||
|
|
207
|
+
attemptController?.signal.aborted ||
|
|
208
|
+
!Number.isFinite(progressEvent.loaded) ||
|
|
209
|
+
progressEvent.loaded <= lastProgressLoaded
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
lastProgressLoaded = progressEvent.loaded;
|
|
214
|
+
armPhaseTimeout('read', readTimeoutMs);
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
});
|
|
219
|
+
if (phaseTimeoutError) {
|
|
220
|
+
requestFailed = true;
|
|
221
|
+
requestError = phaseTimeoutError;
|
|
222
|
+
response = undefined;
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
requestFailed = true;
|
|
226
|
+
requestError = phaseTimeoutError ?? (signal?.aborted ? createAbortError() : error);
|
|
227
|
+
} finally {
|
|
228
|
+
attemptSettled = true;
|
|
229
|
+
clearPhaseTimeout();
|
|
230
|
+
signal?.removeEventListener('abort', handleParentAbort);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
assertWithinOverallDeadline();
|
|
234
|
+
|
|
235
|
+
if (requestFailed) {
|
|
236
|
+
if (attempt >= maxRetries || !isRetryableRequestError(requestError)) {
|
|
237
|
+
throw requestError;
|
|
238
|
+
}
|
|
239
|
+
await waitForRetry(retryDelayMs, attempt, signal);
|
|
240
|
+
assertWithinOverallDeadline();
|
|
241
|
+
return request(attempt + 1, signal);
|
|
18
242
|
}
|
|
19
|
-
|
|
243
|
+
|
|
244
|
+
if (!response) {
|
|
245
|
+
throw new Error(`httpRequest completed without a response: ${url}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (+response.status === 200) {
|
|
20
249
|
return response.data;
|
|
21
250
|
}
|
|
22
|
-
|
|
251
|
+
|
|
252
|
+
if (RETRYABLE_HTTP_STATUSES.has(Number(response.status)) && attempt < maxRetries) {
|
|
253
|
+
await waitForRetry(retryDelayMs, attempt, signal);
|
|
254
|
+
assertWithinOverallDeadline();
|
|
255
|
+
return request(attempt + 1, signal);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(`httpRequest error: ${url} ${response.statusText}`);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (!overallTimeoutMs) {
|
|
262
|
+
return request(0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const requestPromise = request(0, controller.signal);
|
|
267
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
268
|
+
try {
|
|
269
|
+
return await new Promise<T>((resolve, reject) => {
|
|
270
|
+
timeoutTimer = setTimeout(() => {
|
|
271
|
+
reject(overallTimeoutError);
|
|
272
|
+
controller.abort();
|
|
273
|
+
}, overallTimeoutMs);
|
|
274
|
+
requestPromise.then(resolve, reject);
|
|
275
|
+
});
|
|
276
|
+
} finally {
|
|
277
|
+
if (timeoutTimer) {
|
|
278
|
+
clearTimeout(timeoutTimer);
|
|
279
|
+
}
|
|
23
280
|
}
|
|
24
|
-
throw new Error(`httpRequest error: ${url} ${response.statusText}`);
|
|
25
281
|
};
|