@onekeyfe/hd-core 1.2.0-alpha.56 → 1.2.0-alpha.58
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 +84 -2
- package/__tests__/DeviceCommands.test.ts +17 -6
- package/__tests__/core-initialization.test.ts +23 -0
- package/__tests__/device-connector-protocol.test.ts +81 -0
- package/__tests__/device-settings.test.ts +39 -18
- package/__tests__/protocol-v2-resources.test.ts +24 -0
- package/__tests__/protocol-v2.test.ts +46 -1
- package/__tests__/protocolV2FileWrite.test.ts +113 -0
- package/dist/api/FirmwareUpdateV4.d.ts +0 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +4 -2
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +7 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +2 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +448 -336
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +39 -69
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
- package/src/api/device/DeviceSettings.ts +5 -4
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +12 -1
- package/src/api/helpers/protocolV2FileWrite.ts +192 -73
- package/src/core/index.ts +9 -6
- package/src/data-manager/DataManager.ts +25 -4
- package/src/data-manager/TransportManager.ts +6 -0
- package/src/device/DeviceCommands.ts +20 -2
- package/src/device/DeviceConnector.ts +33 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-core",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.58",
|
|
4
4
|
"description": "Core processes and APIs for communicating with OneKey hardware devices.",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
29
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
28
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.58",
|
|
29
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.58",
|
|
30
30
|
"axios": "1.15.2",
|
|
31
31
|
"bignumber.js": "^9.0.2",
|
|
32
32
|
"bytebuffer": "^5.0.1",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"@types/w3c-web-usb": "^1.0.10",
|
|
45
45
|
"@types/web-bluetooth": "^0.0.21"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "43c11efeb7001741d4258c855b287993bc0a798e"
|
|
48
48
|
}
|
|
@@ -8,6 +8,7 @@ import { sha256 } from '@noble/hashes/sha256';
|
|
|
8
8
|
|
|
9
9
|
import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
|
|
10
10
|
import { validateProtocolV2FilesystemPath } from './helpers/filesystemValidation';
|
|
11
|
+
import { writeProtocolV2File } from './helpers/protocolV2FileWrite';
|
|
11
12
|
import { validateParams } from './helpers/paramsValidator';
|
|
12
13
|
import {
|
|
13
14
|
LoggerNames,
|
|
@@ -53,7 +54,6 @@ import type {
|
|
|
53
54
|
|
|
54
55
|
const Log = getLogger(LoggerNames.Method);
|
|
55
56
|
|
|
56
|
-
const SESSION_ERROR = 'session not found';
|
|
57
57
|
const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
|
|
58
58
|
const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
|
|
59
59
|
const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
|
|
@@ -974,15 +974,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
974
974
|
}
|
|
975
975
|
if (this.isProtocolV2BootloaderMode()) {
|
|
976
976
|
Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
|
|
977
|
+
this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
|
|
977
978
|
return false;
|
|
978
979
|
}
|
|
979
980
|
|
|
980
981
|
try {
|
|
981
982
|
this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
|
|
982
983
|
await this.protocolV2Reboot(DeviceRebootType.Bootloader);
|
|
983
|
-
this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
|
|
984
984
|
await wait(1000);
|
|
985
985
|
await this.waitForProtocolV2BootloaderMode();
|
|
986
|
+
this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
|
|
986
987
|
return true;
|
|
987
988
|
} catch (error) {
|
|
988
989
|
if (error instanceof HardwareError) {
|
|
@@ -1631,7 +1632,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1631
1632
|
onTransferredBytes,
|
|
1632
1633
|
}: ProtocolV2FileTransferParams) {
|
|
1633
1634
|
const chunkSize = this.getProtocolV2FirmwareChunkSize();
|
|
1634
|
-
let offset = 0;
|
|
1635
1635
|
const getUploadProgress = (fileOffset: number) => {
|
|
1636
1636
|
if (totalSize !== undefined && processedSize !== undefined) {
|
|
1637
1637
|
return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
|
|
@@ -1639,37 +1639,43 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1639
1639
|
return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
|
|
1640
1640
|
};
|
|
1641
1641
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
progress
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1642
|
+
try {
|
|
1643
|
+
await writeProtocolV2File({
|
|
1644
|
+
commands: this.device.getCommands(),
|
|
1645
|
+
path: filePath,
|
|
1646
|
+
data: payload,
|
|
1647
|
+
totalSize: payload.byteLength,
|
|
1648
|
+
chunkSize,
|
|
1649
|
+
overwrite: true,
|
|
1650
|
+
append: false,
|
|
1651
|
+
writeWithResponse: true,
|
|
1652
|
+
maxChunkRetries: 0,
|
|
1653
|
+
getUiPercentage: ({ offset, chunkLength }) =>
|
|
1654
|
+
getProtocolV2DeviceTransferProgress(
|
|
1655
|
+
(processedSize ?? 0) + offset,
|
|
1656
|
+
(processedSize ?? 0) + offset + chunkLength,
|
|
1657
|
+
totalSize ?? payload.byteLength
|
|
1658
|
+
),
|
|
1659
|
+
onProgress: progress => {
|
|
1660
|
+
const transferredBytes = (processedSize ?? 0) + progress.transferredBytes;
|
|
1661
|
+
onTransferredBytes?.(transferredBytes);
|
|
1662
|
+
this.postProgressMessage(getUploadProgress(progress.transferredBytes), 'transferData', {
|
|
1663
|
+
transferredBytes,
|
|
1664
|
+
totalBytes: totalSize ?? payload.byteLength,
|
|
1665
|
+
rateBytesPerSecond: progress.rateBytesPerSecond,
|
|
1666
|
+
elapsedMs: progress.elapsedMs,
|
|
1667
|
+
});
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
if (
|
|
1672
|
+
error instanceof HardwareError &&
|
|
1673
|
+
error.errorCode === HardwareErrorCode.RuntimeError &&
|
|
1674
|
+
error.message.includes('FilesystemFileWrite')
|
|
1675
|
+
) {
|
|
1676
|
+
throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, error.message);
|
|
1669
1677
|
}
|
|
1670
|
-
|
|
1671
|
-
onTransferredBytes?.((processedSize ?? 0) + offset);
|
|
1672
|
-
this.postProgressMessage(getUploadProgress(offset), 'transferData');
|
|
1678
|
+
throw error;
|
|
1673
1679
|
}
|
|
1674
1680
|
|
|
1675
1681
|
return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
|
|
@@ -1689,42 +1695,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1689
1695
|
return env ?? 'unknown';
|
|
1690
1696
|
}
|
|
1691
1697
|
|
|
1692
|
-
private async fileWriteChunk(
|
|
1693
|
-
filePath: string,
|
|
1694
|
-
totalFileSize: number,
|
|
1695
|
-
offset: number,
|
|
1696
|
-
chunk: ArrayBuffer | Buffer,
|
|
1697
|
-
overwrite: boolean,
|
|
1698
|
-
progress: number | null
|
|
1699
|
-
): Promise<TypedResponseMessage<'FilesystemFile'>> {
|
|
1700
|
-
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
1701
|
-
const writeRes = await typedCall(
|
|
1702
|
-
'FilesystemFileWrite',
|
|
1703
|
-
'FilesystemFile',
|
|
1704
|
-
{
|
|
1705
|
-
file: {
|
|
1706
|
-
path: filePath,
|
|
1707
|
-
offset,
|
|
1708
|
-
total_size: totalFileSize,
|
|
1709
|
-
data: chunk,
|
|
1710
|
-
},
|
|
1711
|
-
overwrite,
|
|
1712
|
-
append: false,
|
|
1713
|
-
ui_percentage: progress ?? undefined,
|
|
1714
|
-
},
|
|
1715
|
-
{ writeWithResponse: true }
|
|
1716
|
-
);
|
|
1717
|
-
if (writeRes.type !== 'FilesystemFile') {
|
|
1718
|
-
if ((writeRes as any).type === 'CallMethodError') {
|
|
1719
|
-
if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
|
|
1720
|
-
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
|
|
1724
|
-
}
|
|
1725
|
-
return writeRes;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
1698
|
private async recoverProtocolV2FileTransfer() {
|
|
1729
1699
|
const env = DataManager.getSettings('env');
|
|
1730
1700
|
if (DataManager.isBleConnect(env)) {
|
|
@@ -393,12 +393,14 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
|
|
|
393
393
|
// the root fingerprint, so each nested chain method must resume the
|
|
394
394
|
// requested standard or hidden wallet before sending its device command.
|
|
395
395
|
const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
|
|
396
|
+
const deriveCardano = method.name.startsWith('cardano');
|
|
396
397
|
const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
|
|
397
398
|
if (this.device.isProtocolV2() && shouldResumeWalletSession) {
|
|
398
399
|
const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
|
|
399
400
|
this.payload.passphraseState,
|
|
400
401
|
useEmptyPassphrase,
|
|
401
|
-
this.payload.skipPassphraseCheck
|
|
402
|
+
this.payload.skipPassphraseCheck,
|
|
403
|
+
deriveCardano
|
|
402
404
|
);
|
|
403
405
|
if (!passphraseStateSafety) {
|
|
404
406
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
@@ -7,7 +7,6 @@ import { validateParams } from '../helpers/paramsValidator';
|
|
|
7
7
|
import {
|
|
8
8
|
mapApplySettingsToState,
|
|
9
9
|
mapCommonSettingsToProtocolV2,
|
|
10
|
-
mapDeviceSettingsToState,
|
|
11
10
|
} from '../../device/DeviceStateMapper';
|
|
12
11
|
import { getProtocolV2SettingsBehavior } from '../../protocols/protocol-v2/settingsUnlockPolicy';
|
|
13
12
|
import {
|
|
@@ -163,6 +162,8 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
|
|
|
163
162
|
async run() {
|
|
164
163
|
try {
|
|
165
164
|
if (this.device.isProtocolV2()) {
|
|
165
|
+
const refreshStatusAndSettings = () =>
|
|
166
|
+
this.device.getDeviceState({ refreshSections: ['status', 'settings'] });
|
|
166
167
|
assertSettingsSupported(this.payload, DEVICE_SETTINGS_V1_ONLY_FIELDS, 'Protocol V2');
|
|
167
168
|
const capabilities = getDeviceSettingsCapabilities(
|
|
168
169
|
this.device.getCurrentDeviceType(),
|
|
@@ -194,7 +195,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
|
|
|
194
195
|
const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
|
|
195
196
|
page: DeviceSettingsPage.DevicePassphrase,
|
|
196
197
|
});
|
|
197
|
-
const updated = await
|
|
198
|
+
const updated = await refreshStatusAndSettings();
|
|
198
199
|
const lockedAfterDisabling =
|
|
199
200
|
requestedPassphrase === false &&
|
|
200
201
|
current.status.unlocked === true &&
|
|
@@ -219,7 +220,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
|
|
|
219
220
|
const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
|
|
220
221
|
page: DeviceSettingsPage.DeviceAirgap,
|
|
221
222
|
});
|
|
222
|
-
const updated = await
|
|
223
|
+
const updated = await refreshStatusAndSettings();
|
|
223
224
|
if (updated.settings.airgapMode !== requestedAirgap) {
|
|
224
225
|
throw TypedError(
|
|
225
226
|
HardwareErrorCode.RuntimeError,
|
|
@@ -235,7 +236,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
|
|
|
235
236
|
const res = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
|
|
236
237
|
settings,
|
|
237
238
|
});
|
|
238
|
-
|
|
239
|
+
await refreshStatusAndSettings();
|
|
239
240
|
return res.message;
|
|
240
241
|
}
|
|
241
242
|
|
|
@@ -15,6 +15,7 @@ import { BaseMethod } from '../BaseMethod';
|
|
|
15
15
|
import { DEVICE } from '../../events';
|
|
16
16
|
|
|
17
17
|
import type {
|
|
18
|
+
FirmwareProgress,
|
|
18
19
|
IFirmwareUpdateProgressType,
|
|
19
20
|
IFirmwareUpdateTipMessage,
|
|
20
21
|
} from '../../events/ui-request';
|
|
@@ -28,6 +29,11 @@ const Log = getLogger(LoggerNames.Method);
|
|
|
28
29
|
const SESSION_ERROR = 'session not found';
|
|
29
30
|
const FIRMWARE_UPDATE_CONFIRM = 'Firmware install confirmed';
|
|
30
31
|
|
|
32
|
+
type FirmwareTransferMetrics = Pick<
|
|
33
|
+
FirmwareProgress['payload'],
|
|
34
|
+
'transferredBytes' | 'totalBytes' | 'rateBytesPerSecond' | 'elapsedMs'
|
|
35
|
+
>;
|
|
36
|
+
|
|
31
37
|
const isDeviceDisconnectedError = (error: unknown) => {
|
|
32
38
|
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
33
39
|
return (
|
|
@@ -82,12 +88,17 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
|
|
|
82
88
|
* @description Post the progress message
|
|
83
89
|
* @param progress Post the percentage of the progress
|
|
84
90
|
*/
|
|
85
|
-
postProgressMessage = (
|
|
91
|
+
postProgressMessage = (
|
|
92
|
+
progress: number,
|
|
93
|
+
progressType: IFirmwareUpdateProgressType,
|
|
94
|
+
metrics?: FirmwareTransferMetrics
|
|
95
|
+
) => {
|
|
86
96
|
this.postMessage(
|
|
87
97
|
createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, {
|
|
88
98
|
device: this.device.toMessageObject() as KnownDevice,
|
|
89
99
|
progress,
|
|
90
100
|
progressType,
|
|
101
|
+
...metrics,
|
|
91
102
|
})
|
|
92
103
|
);
|
|
93
104
|
};
|
|
@@ -5,9 +5,12 @@ import {
|
|
|
5
5
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
6
6
|
|
|
7
7
|
import { DataManager } from '../../data-manager';
|
|
8
|
+
import { LoggerNames, getLogger } from '../../utils/logger';
|
|
8
9
|
|
|
9
10
|
import type { DeviceCommands } from '../../device/DeviceCommands';
|
|
10
11
|
|
|
12
|
+
const Log = getLogger(LoggerNames.Method);
|
|
13
|
+
|
|
11
14
|
export type ProtocolV2FileWriteData = ArrayBuffer | Uint8Array | Blob | string;
|
|
12
15
|
|
|
13
16
|
export type ProtocolV2FileWriteProgress = {
|
|
@@ -18,6 +21,12 @@ export type ProtocolV2FileWriteProgress = {
|
|
|
18
21
|
elapsedMs: number;
|
|
19
22
|
};
|
|
20
23
|
|
|
24
|
+
export type ProtocolV2FileWriteChunkContext = {
|
|
25
|
+
offset: number;
|
|
26
|
+
chunkLength: number;
|
|
27
|
+
totalSize: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
export type ProtocolV2FileWriteOptions = {
|
|
22
31
|
commands: Pick<DeviceCommands, 'typedCall'>;
|
|
23
32
|
path: string;
|
|
@@ -30,14 +39,54 @@ export type ProtocolV2FileWriteOptions = {
|
|
|
30
39
|
append?: boolean;
|
|
31
40
|
uiPercentage?: number;
|
|
32
41
|
timeoutMs?: number;
|
|
42
|
+
writeWithResponse?: boolean;
|
|
33
43
|
maxChunkRetries?: number;
|
|
34
44
|
paceMs?: number;
|
|
35
45
|
throwIfAborted?: () => void;
|
|
46
|
+
getUiPercentage?: (context: ProtocolV2FileWriteChunkContext) => number | undefined;
|
|
36
47
|
onProgress?: (progress: ProtocolV2FileWriteProgress) => void;
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
const MIN_FILE_CHUNK_SIZE = 64;
|
|
40
51
|
const FILE_TRANSFER_RATE_WINDOW_MS = 1000;
|
|
52
|
+
const FILE_TRANSFER_LOG_INTERVAL_MS = 10_000;
|
|
53
|
+
const SESSION_ERROR = 'session not found';
|
|
54
|
+
|
|
55
|
+
function formatFileTransferRate(bytesPerSecond: number) {
|
|
56
|
+
return (Math.max(bytesPerSecond, 0) / 1024).toFixed(2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getAverageFileTransferRate(transferredBytes: number, elapsedMs: number) {
|
|
60
|
+
if (elapsedMs <= 0) return 0;
|
|
61
|
+
return Math.round((Math.max(transferredBytes, 0) / elapsedMs) * 1000);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getFileTransferTransport() {
|
|
65
|
+
const env = DataManager.getSettings('env');
|
|
66
|
+
return env && DataManager.isBleConnect(env) ? 'BLE' : String(env ?? 'unknown');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function logFileTransferMetrics({
|
|
70
|
+
transport,
|
|
71
|
+
status,
|
|
72
|
+
transferredBytes,
|
|
73
|
+
totalBytes,
|
|
74
|
+
elapsedMs,
|
|
75
|
+
rateBytesPerSecond,
|
|
76
|
+
}: {
|
|
77
|
+
transport: string;
|
|
78
|
+
status: 'progress' | 'completed' | 'failed';
|
|
79
|
+
transferredBytes: number;
|
|
80
|
+
totalBytes: number;
|
|
81
|
+
elapsedMs: number;
|
|
82
|
+
rateBytesPerSecond: number;
|
|
83
|
+
}) {
|
|
84
|
+
Log.log(
|
|
85
|
+
`[FileWrite] metrics transport=${transport} status=${status} bytes=${transferredBytes}/${totalBytes} elapsed=${(
|
|
86
|
+
elapsedMs / 1000
|
|
87
|
+
).toFixed(2)}s speed=${formatFileTransferRate(rateBytesPerSecond)} KiB/s`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
41
90
|
|
|
42
91
|
export function isProtocolV2ResponseTimeout(error: unknown) {
|
|
43
92
|
if (!error || typeof error !== 'object') return false;
|
|
@@ -125,89 +174,159 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
|
|
|
125
174
|
let rateWindowStartedAt = startTime;
|
|
126
175
|
let rateWindowStartedBytes = 0;
|
|
127
176
|
let rateBytesPerSecond: number | undefined;
|
|
177
|
+
let lastConfirmedAt = startTime;
|
|
178
|
+
let logWindowStartedAt = startTime;
|
|
179
|
+
let logWindowStartedBytes = 0;
|
|
180
|
+
const transport = getFileTransferTransport();
|
|
128
181
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
try {
|
|
183
|
+
while (written < dataLength) {
|
|
184
|
+
options.throwIfAborted?.();
|
|
185
|
+
const chunk = data.slice(written, Math.min(written + chunkSize, dataLength));
|
|
186
|
+
const offset = startOffset + written;
|
|
187
|
+
const progress =
|
|
188
|
+
options.uiPercentage ??
|
|
189
|
+
options.getUiPercentage?.({
|
|
190
|
+
offset,
|
|
191
|
+
chunkLength: chunk.byteLength,
|
|
192
|
+
totalSize,
|
|
193
|
+
}) ??
|
|
194
|
+
getDeviceTransferProgress(offset, offset + chunk.byteLength, totalSize);
|
|
195
|
+
const request = {
|
|
196
|
+
file: { path: options.path, offset, total_size: totalSize, data: chunk },
|
|
197
|
+
overwrite: chunks === 0 ? options.overwrite ?? false : false,
|
|
198
|
+
append: options.append ?? false,
|
|
199
|
+
ui_percentage: progress,
|
|
200
|
+
};
|
|
201
|
+
const maxChunkRetries = Math.max(Math.floor(options.maxChunkRetries ?? 0), 0);
|
|
202
|
+
let retryCount = 0;
|
|
203
|
+
let response;
|
|
204
|
+
let isWritePending = true;
|
|
205
|
+
while (isWritePending) {
|
|
206
|
+
try {
|
|
207
|
+
const callOptions =
|
|
208
|
+
options.writeWithResponse === undefined
|
|
209
|
+
? { timeoutMs: options.timeoutMs }
|
|
210
|
+
: {
|
|
211
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
212
|
+
writeWithResponse: options.writeWithResponse,
|
|
213
|
+
};
|
|
214
|
+
response = await options.commands.typedCall(
|
|
215
|
+
'FilesystemFileWrite',
|
|
216
|
+
'FilesystemFile',
|
|
217
|
+
request,
|
|
218
|
+
callOptions
|
|
219
|
+
);
|
|
220
|
+
isWritePending = false;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (retryCount >= maxChunkRetries || !isProtocolV2ResponseTimeout(error)) throw error;
|
|
223
|
+
retryCount += 1;
|
|
224
|
+
options.throwIfAborted?.();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!response) {
|
|
228
|
+
throw ERRORS.TypedError(
|
|
229
|
+
HardwareErrorCode.RuntimeError,
|
|
230
|
+
'FilesystemFileWrite completed without a response'
|
|
153
231
|
);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
232
|
+
}
|
|
233
|
+
const responseType = (response as { type?: string }).type;
|
|
234
|
+
if (responseType && responseType !== 'FilesystemFile') {
|
|
235
|
+
const responseError = (response as { message?: { error?: unknown } }).message?.error;
|
|
236
|
+
if (typeof responseError === 'string' && responseError.includes(SESSION_ERROR)) {
|
|
237
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
|
|
238
|
+
}
|
|
239
|
+
throw ERRORS.TypedError(
|
|
240
|
+
HardwareErrorCode.RuntimeError,
|
|
241
|
+
`FilesystemFileWrite received unexpected response ${responseType}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
options.throwIfAborted?.();
|
|
245
|
+
lastMessage = response.message;
|
|
246
|
+
const rawProcessedByte = response.message?.processed_byte;
|
|
247
|
+
const processedByte = Number(rawProcessedByte);
|
|
248
|
+
if (
|
|
249
|
+
rawProcessedByte !== undefined &&
|
|
250
|
+
(!Number.isFinite(processedByte) ||
|
|
251
|
+
processedByte <= offset ||
|
|
252
|
+
processedByte > offset + chunk.byteLength)
|
|
253
|
+
) {
|
|
254
|
+
throw ERRORS.TypedError(
|
|
255
|
+
HardwareErrorCode.RuntimeError,
|
|
256
|
+
`FilesystemFileWrite invalid processed_byte ${processedByte}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
written =
|
|
260
|
+
rawProcessedByte === undefined ? written + chunk.byteLength : processedByte - startOffset;
|
|
261
|
+
chunks += 1;
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
lastConfirmedAt = now;
|
|
264
|
+
const elapsedMs = now - startTime;
|
|
265
|
+
const transferredBytes = Math.min(written, dataLength);
|
|
266
|
+
const rateWindowElapsedMs = now - rateWindowStartedAt;
|
|
267
|
+
if (rateWindowElapsedMs >= FILE_TRANSFER_RATE_WINDOW_MS) {
|
|
268
|
+
const rateWindowBytes = Math.max(transferredBytes - rateWindowStartedBytes, 0);
|
|
269
|
+
rateBytesPerSecond = Math.round((rateWindowBytes / rateWindowElapsedMs) * 1000);
|
|
270
|
+
rateWindowStartedAt = now;
|
|
271
|
+
rateWindowStartedBytes = transferredBytes;
|
|
272
|
+
} else if (rateBytesPerSecond === undefined && elapsedMs > 0) {
|
|
273
|
+
rateBytesPerSecond = Math.round((transferredBytes / elapsedMs) * 1000);
|
|
274
|
+
}
|
|
275
|
+
options.onProgress?.({
|
|
276
|
+
progress: getConfirmedProgress(startOffset + written, totalSize, written, dataLength),
|
|
277
|
+
transferredBytes,
|
|
278
|
+
totalBytes: dataLength,
|
|
279
|
+
rateBytesPerSecond,
|
|
280
|
+
elapsedMs,
|
|
281
|
+
});
|
|
282
|
+
const logWindowElapsedMs = now - logWindowStartedAt;
|
|
283
|
+
if (logWindowElapsedMs >= FILE_TRANSFER_LOG_INTERVAL_MS && transferredBytes < dataLength) {
|
|
284
|
+
const logWindowBytes = Math.max(transferredBytes - logWindowStartedBytes, 0);
|
|
285
|
+
logFileTransferMetrics({
|
|
286
|
+
transport,
|
|
287
|
+
status: 'progress',
|
|
288
|
+
transferredBytes,
|
|
289
|
+
totalBytes: dataLength,
|
|
290
|
+
elapsedMs,
|
|
291
|
+
rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
|
|
292
|
+
});
|
|
293
|
+
logWindowStartedAt = now;
|
|
294
|
+
logWindowStartedBytes = transferredBytes;
|
|
295
|
+
}
|
|
296
|
+
if (options.paceMs && options.paceMs > 0) {
|
|
297
|
+
await new Promise(resolve => {
|
|
298
|
+
setTimeout(resolve, options.paceMs);
|
|
299
|
+
});
|
|
159
300
|
}
|
|
160
301
|
}
|
|
161
|
-
|
|
162
|
-
throw ERRORS.TypedError(
|
|
163
|
-
HardwareErrorCode.RuntimeError,
|
|
164
|
-
'FilesystemFileWrite completed without a response'
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
options.throwIfAborted?.();
|
|
168
|
-
lastMessage = response.message;
|
|
169
|
-
const rawProcessedByte = response.message?.processed_byte;
|
|
170
|
-
const processedByte = Number(rawProcessedByte);
|
|
171
|
-
if (
|
|
172
|
-
rawProcessedByte !== undefined &&
|
|
173
|
-
(!Number.isFinite(processedByte) ||
|
|
174
|
-
processedByte <= offset ||
|
|
175
|
-
processedByte > offset + chunk.byteLength)
|
|
176
|
-
) {
|
|
177
|
-
throw ERRORS.TypedError(
|
|
178
|
-
HardwareErrorCode.RuntimeError,
|
|
179
|
-
`FilesystemFileWrite invalid processed_byte ${processedByte}`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
written =
|
|
183
|
-
rawProcessedByte === undefined ? written + chunk.byteLength : processedByte - startOffset;
|
|
184
|
-
chunks += 1;
|
|
302
|
+
} catch (error) {
|
|
185
303
|
const now = Date.now();
|
|
186
|
-
const elapsedMs = now - startTime;
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
rateWindowStartedBytes = transferredBytes;
|
|
194
|
-
} else if (rateBytesPerSecond === undefined && elapsedMs > 0) {
|
|
195
|
-
rateBytesPerSecond = Math.round((transferredBytes / elapsedMs) * 1000);
|
|
196
|
-
}
|
|
197
|
-
options.onProgress?.({
|
|
198
|
-
progress: getConfirmedProgress(startOffset + written, totalSize, written, dataLength),
|
|
199
|
-
transferredBytes,
|
|
304
|
+
const elapsedMs = Math.max(now - startTime, 0);
|
|
305
|
+
const logWindowElapsedMs = Math.max(now - logWindowStartedAt, 0);
|
|
306
|
+
const logWindowBytes = Math.max(written - logWindowStartedBytes, 0);
|
|
307
|
+
logFileTransferMetrics({
|
|
308
|
+
transport,
|
|
309
|
+
status: 'failed',
|
|
310
|
+
transferredBytes: written,
|
|
200
311
|
totalBytes: dataLength,
|
|
201
|
-
rateBytesPerSecond,
|
|
202
312
|
elapsedMs,
|
|
313
|
+
rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
|
|
203
314
|
});
|
|
204
|
-
|
|
205
|
-
await new Promise(resolve => {
|
|
206
|
-
setTimeout(resolve, options.paceMs);
|
|
207
|
-
});
|
|
208
|
-
}
|
|
315
|
+
throw error;
|
|
209
316
|
}
|
|
210
317
|
|
|
318
|
+
const elapsedMs = Math.max(lastConfirmedAt - startTime, 0);
|
|
319
|
+
const logWindowElapsedMs = Math.max(lastConfirmedAt - logWindowStartedAt, 0);
|
|
320
|
+
const logWindowBytes = Math.max(written - logWindowStartedBytes, 0);
|
|
321
|
+
logFileTransferMetrics({
|
|
322
|
+
transport,
|
|
323
|
+
status: 'completed',
|
|
324
|
+
transferredBytes: written,
|
|
325
|
+
totalBytes: dataLength,
|
|
326
|
+
elapsedMs,
|
|
327
|
+
rateBytesPerSecond: getAverageFileTransferRate(logWindowBytes, logWindowElapsedMs),
|
|
328
|
+
});
|
|
329
|
+
|
|
211
330
|
return {
|
|
212
331
|
...lastMessage,
|
|
213
332
|
path: options.path,
|
package/src/core/index.ts
CHANGED
|
@@ -652,6 +652,12 @@ const onCallDevice = async (
|
|
|
652
652
|
requestQueue.resolveRequest(method.responseID, messageResponse);
|
|
653
653
|
completeMethodRequestContext(method);
|
|
654
654
|
} catch (error) {
|
|
655
|
+
// Device.run may release the request before transport callbacks finish after a timeout.
|
|
656
|
+
// Ignore the stale callback because the caller already received the connection error.
|
|
657
|
+
if (!requestQueue.getTask(method.responseID)) {
|
|
658
|
+
Log.debug(`Call API - Ignore late inner method result`, error);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
655
661
|
Log.debug(`Call API - Inner Method Run Error`, error);
|
|
656
662
|
messageResponse = createResponseMessage(method.responseID, false, { error });
|
|
657
663
|
requestQueue.resolveRequest(method.responseID, messageResponse);
|
|
@@ -1647,12 +1653,8 @@ export const init = async (
|
|
|
1647
1653
|
plugin?: LowlevelTransportSharedPlugin
|
|
1648
1654
|
) => {
|
|
1649
1655
|
try {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
initTransport(Transport, plugin);
|
|
1653
|
-
} catch {
|
|
1654
|
-
Log.error('DataManager.load error');
|
|
1655
|
-
}
|
|
1656
|
+
await DataManager.load(settings);
|
|
1657
|
+
initTransport(Transport, plugin);
|
|
1656
1658
|
enableLog(DataManager.getSettings('debug'));
|
|
1657
1659
|
if (DataManager.getSettings('env') !== 'react-native') {
|
|
1658
1660
|
setLoggerPostMessage(postMessage);
|
|
@@ -1663,6 +1665,7 @@ export const init = async (
|
|
|
1663
1665
|
return _core;
|
|
1664
1666
|
} catch (error) {
|
|
1665
1667
|
Log.error('core init', error);
|
|
1668
|
+
throw error;
|
|
1666
1669
|
}
|
|
1667
1670
|
};
|
|
1668
1671
|
|