react-native-update 10.55.1 → 10.56.0
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/README-CN.md +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +399 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +36 -3
- package/src/client.ts +236 -82
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
|
@@ -12,9 +12,12 @@ import NativePatchCore, {
|
|
|
12
12
|
CopyGroupResult,
|
|
13
13
|
} from './NativePatchCore';
|
|
14
14
|
import { monotonicNowMs } from './MonotonicClock';
|
|
15
|
+
import logger from './Logger';
|
|
15
16
|
import {
|
|
16
17
|
MAX_ARCHIVE_BYTES,
|
|
17
18
|
MAX_MANIFEST_BYTES,
|
|
19
|
+
UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES,
|
|
20
|
+
checkUncompressedSize,
|
|
18
21
|
ensureFreeSpace,
|
|
19
22
|
measureExtractedDirectory,
|
|
20
23
|
} from './ArchiveLimits';
|
|
@@ -24,8 +27,17 @@ import {
|
|
|
24
27
|
stagingDirectoryFor,
|
|
25
28
|
writeInstallRecord,
|
|
26
29
|
} from './InstallRecord';
|
|
30
|
+
import {
|
|
31
|
+
ERROR_DOWNLOAD_FAILED,
|
|
32
|
+
ERROR_FILE_OPERATION_FAILED,
|
|
33
|
+
ERROR_INVALID_OPTIONS,
|
|
34
|
+
ERROR_PATCH_FAILED,
|
|
35
|
+
createUpdateError,
|
|
36
|
+
getErrorMessage,
|
|
37
|
+
toUpdateError,
|
|
38
|
+
} from './ErrorCodes';
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
const TAG = 'DownloadTask';
|
|
29
41
|
|
|
30
42
|
export interface PatchManifestArrays {
|
|
31
43
|
copyFroms: string[];
|
|
@@ -60,7 +72,8 @@ export function parseManifestToArrays(
|
|
|
60
72
|
copiesCrcValue && typeof copiesCrcValue === 'object'
|
|
61
73
|
? (copiesCrcValue as Record<string, number>)
|
|
62
74
|
: ({} as Record<string, number>);
|
|
63
|
-
for (const
|
|
75
|
+
for (const to of Object.keys(copies)) {
|
|
76
|
+
const rawFrom = copies[to];
|
|
64
77
|
let from = String(rawFrom || '');
|
|
65
78
|
if (normalizeResourceCopies) {
|
|
66
79
|
from = from.replace('resources/rawfile/', '');
|
|
@@ -95,6 +108,11 @@ export function parseManifestToArrays(
|
|
|
95
108
|
};
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
interface PatchInputs {
|
|
112
|
+
entryNames: string[];
|
|
113
|
+
manifestArrays: PatchManifestArrays;
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
function toArrayBufferSlice(
|
|
99
117
|
payload: Uint8Array,
|
|
100
118
|
offset: number,
|
|
@@ -123,7 +141,9 @@ interface ResumeMeta {
|
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
// "bytes <start>-<end>/<total>"。返回总长(“*”记 0);缺失/畸形/起点与
|
|
126
|
-
// 本地 partial
|
|
144
|
+
// 本地 partial 不符、或 range 与 total 自相矛盾(end 在 start 之前、数值
|
|
145
|
+
// total 不大于 end)返回 -1——那样追加的字节不可信。RFC 9110 §14.4 要求
|
|
146
|
+
// 完整长度必须大于 last-pos。
|
|
127
147
|
export function parseContentRangeTotal(
|
|
128
148
|
header: string,
|
|
129
149
|
expectedStart: number,
|
|
@@ -138,7 +158,13 @@ export function parseContentRangeTotal(
|
|
|
138
158
|
return -1;
|
|
139
159
|
}
|
|
140
160
|
const start = parseInt(range.substring(0, dash).trim(), 10);
|
|
141
|
-
|
|
161
|
+
const end = parseInt(range.substring(dash + 1, slash).trim(), 10);
|
|
162
|
+
if (
|
|
163
|
+
Number.isNaN(start) ||
|
|
164
|
+
Number.isNaN(end) ||
|
|
165
|
+
start !== expectedStart ||
|
|
166
|
+
end < start
|
|
167
|
+
) {
|
|
142
168
|
return -1;
|
|
143
169
|
}
|
|
144
170
|
const totalPart = range.substring(slash + 1).trim();
|
|
@@ -146,7 +172,7 @@ export function parseContentRangeTotal(
|
|
|
146
172
|
return 0;
|
|
147
173
|
}
|
|
148
174
|
const total = parseInt(totalPart, 10);
|
|
149
|
-
return Number.isNaN(total) || total <=
|
|
175
|
+
return Number.isNaN(total) || total <= end ? -1 : total;
|
|
150
176
|
}
|
|
151
177
|
|
|
152
178
|
export class DownloadTask {
|
|
@@ -170,9 +196,13 @@ export class DownloadTask {
|
|
|
170
196
|
const work = this.stagingDirectory(params);
|
|
171
197
|
const bundlePath = `${work}/${HARMONY_BUNDLE_FILE_NAME}`;
|
|
172
198
|
if (!fileIo.accessSync(bundlePath)) {
|
|
173
|
-
throw
|
|
199
|
+
throw createUpdateError(
|
|
200
|
+
ERROR_PATCH_FAILED,
|
|
201
|
+
`bundle missing after install: ${bundlePath}`,
|
|
202
|
+
);
|
|
174
203
|
}
|
|
175
|
-
|
|
204
|
+
// 整 bundle 几十 MB:摘要在 native 工作线程算,不冻结 UI 线程。
|
|
205
|
+
const bundleSha256 = await NativePatchCore.sha256HexFileAsync(bundlePath);
|
|
176
206
|
await writeInstallRecord(
|
|
177
207
|
work,
|
|
178
208
|
buildInstallRecord(params.hash, bundleSha256, this.artifactSha256),
|
|
@@ -222,7 +252,7 @@ export class DownloadTask {
|
|
|
222
252
|
await this.writeFileContent(this.resumeSidecarPath(targetFile), encoded);
|
|
223
253
|
} catch (e) {
|
|
224
254
|
// 非致命:没有 sidecar,下次从零开始。
|
|
225
|
-
|
|
255
|
+
logger.error(TAG, `Failed to persist resume sidecar: ${getErrorMessage(e)}`);
|
|
226
256
|
}
|
|
227
257
|
}
|
|
228
258
|
|
|
@@ -233,7 +263,7 @@ export class DownloadTask {
|
|
|
233
263
|
await fileIo.unlink(sidecar);
|
|
234
264
|
}
|
|
235
265
|
} catch (e) {
|
|
236
|
-
|
|
266
|
+
logger.error(TAG, `Failed to delete resume sidecar: ${getErrorMessage(e)}`);
|
|
237
267
|
}
|
|
238
268
|
}
|
|
239
269
|
|
|
@@ -244,35 +274,26 @@ export class DownloadTask {
|
|
|
244
274
|
await fileIo.unlink(targetFile);
|
|
245
275
|
}
|
|
246
276
|
} catch (e) {
|
|
247
|
-
|
|
277
|
+
logger.error(TAG, `Failed to delete archive: ${getErrorMessage(e)}`);
|
|
248
278
|
}
|
|
249
279
|
await this.deleteResumeSidecar(targetFile);
|
|
250
280
|
}
|
|
251
281
|
|
|
282
|
+
// fileIo.rmdir 本身递归删除整个目录树;文件走 unlink。
|
|
252
283
|
private async removeDirectory(path: string): Promise<void> {
|
|
253
284
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
for (let i = 0; i < entries.length; i += DELETE_CONCURRENCY) {
|
|
263
|
-
const batch = entries.slice(i, i + DELETE_CONCURRENCY);
|
|
264
|
-
await Promise.all(
|
|
265
|
-
batch.map(file => this.removeDirectory(`${path}/${file}`)),
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
await fileIo.rmdir(path);
|
|
269
|
-
} else {
|
|
270
|
-
await fileIo.unlink(path);
|
|
271
|
-
}
|
|
285
|
+
if (!fileIo.accessSync(path)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const stat = await fileIo.stat(path);
|
|
289
|
+
if (stat.isDirectory()) {
|
|
290
|
+
await fileIo.rmdir(path);
|
|
291
|
+
} else {
|
|
292
|
+
await fileIo.unlink(path);
|
|
272
293
|
}
|
|
273
294
|
} catch (error) {
|
|
274
|
-
|
|
275
|
-
throw error;
|
|
295
|
+
logger.error(TAG, `Failed to delete ${path}: ${getErrorMessage(error)}`);
|
|
296
|
+
throw toUpdateError(error, ERROR_FILE_OPERATION_FAILED);
|
|
276
297
|
}
|
|
277
298
|
}
|
|
278
299
|
|
|
@@ -306,33 +327,36 @@ export class DownloadTask {
|
|
|
306
327
|
}
|
|
307
328
|
|
|
308
329
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* directory for a finished install) and the package is rejected.
|
|
330
|
+
* 解压前读取归档解压后的总字节数(zlib.getOriginalSize,API 12——RNOH 的
|
|
331
|
+
* 最低 API 已覆盖)。读不到就拒绝归档:zlib.decompressFile 没有逐条钩子,
|
|
332
|
+
* 不知道展开量就放行等于允许一个 20MB 的归档在 measureExtractedDirectory
|
|
333
|
+
* 跑到之前先把磁盘写满。
|
|
314
334
|
*/
|
|
315
|
-
private async
|
|
316
|
-
|
|
317
|
-
if (!fileIo.accessSync(marker)) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
335
|
+
private async readOriginalSize(archiveFile: string): Promise<number> {
|
|
336
|
+
let size: number | undefined;
|
|
320
337
|
try {
|
|
321
|
-
await
|
|
338
|
+
size = await zlib.getOriginalSize(archiveFile);
|
|
322
339
|
} catch (e) {
|
|
323
|
-
|
|
340
|
+
throw createUpdateError(
|
|
341
|
+
ERROR_PATCH_FAILED,
|
|
342
|
+
`cannot determine archive expansion size: ${getErrorMessage(e)}`,
|
|
343
|
+
);
|
|
324
344
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) {
|
|
346
|
+
throw createUpdateError(
|
|
347
|
+
ERROR_PATCH_FAILED,
|
|
348
|
+
`cannot determine archive expansion size: ${String(size)}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return size;
|
|
330
352
|
}
|
|
331
353
|
|
|
332
354
|
/**
|
|
333
355
|
* 解压 + 资源上限(cpp/patch_core/archive_limits.h)。zlib.decompressFile
|
|
334
|
-
*
|
|
335
|
-
*
|
|
356
|
+
* 没有逐条钩子:解压前用 getOriginalSize 把总解压量与整包压缩比挡在
|
|
357
|
+
* 上限内(20MB 归档 100:1 的 2GB 载荷不会先解出来再量),按已知解压量查
|
|
358
|
+
* 磁盘,解压量未知直接拒绝;解压后再统计条目数/单条大小/总字节数并拒绝
|
|
359
|
+
* 任意深度的 `.pushy-` 保留条目(超限即失败,staging 目录由失败清理删除)。
|
|
336
360
|
*/
|
|
337
361
|
private async extractArchive(
|
|
338
362
|
archiveFile: string,
|
|
@@ -340,12 +364,20 @@ export class DownloadTask {
|
|
|
340
364
|
): Promise<void> {
|
|
341
365
|
const archiveStat = await fileIo.stat(archiveFile);
|
|
342
366
|
if (archiveStat.size > MAX_ARCHIVE_BYTES) {
|
|
343
|
-
throw
|
|
367
|
+
throw createUpdateError(
|
|
368
|
+
ERROR_PATCH_FAILED,
|
|
369
|
+
`archive too large: ${archiveStat.size} bytes`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const originalSize = await this.readOriginalSize(archiveFile);
|
|
373
|
+
checkUncompressedSize(archiveStat.size, originalSize);
|
|
374
|
+
await ensureFreeSpace(unzipDirectory, originalSize);
|
|
375
|
+
try {
|
|
376
|
+
await zlib.decompressFile(archiveFile, unzipDirectory);
|
|
377
|
+
} catch (e) {
|
|
378
|
+
throw toUpdateError(e, ERROR_PATCH_FAILED);
|
|
344
379
|
}
|
|
345
|
-
await ensureFreeSpace(unzipDirectory, archiveStat.size * 2);
|
|
346
|
-
await zlib.decompressFile(archiveFile, unzipDirectory);
|
|
347
380
|
await measureExtractedDirectory(unzipDirectory);
|
|
348
|
-
await this.rejectReservedEntries(unzipDirectory);
|
|
349
381
|
}
|
|
350
382
|
|
|
351
383
|
private async recreateDirectory(path: string): Promise<void> {
|
|
@@ -436,7 +468,10 @@ export class DownloadTask {
|
|
|
436
468
|
|
|
437
469
|
const manifestStat = await fileIo.stat(manifestPath);
|
|
438
470
|
if (manifestStat.size > MAX_MANIFEST_BYTES) {
|
|
439
|
-
throw
|
|
471
|
+
throw createUpdateError(
|
|
472
|
+
ERROR_PATCH_FAILED,
|
|
473
|
+
`patch manifest too large: ${manifestStat.size} bytes`,
|
|
474
|
+
);
|
|
440
475
|
}
|
|
441
476
|
return parseManifestToArrays(
|
|
442
477
|
this.parseJsonEntry(await this.readFileContent(manifestPath)),
|
|
@@ -467,9 +502,11 @@ export class DownloadTask {
|
|
|
467
502
|
enableMerge: false,
|
|
468
503
|
bundleHbcTransformMeta: hbcTransformMeta,
|
|
469
504
|
});
|
|
470
|
-
} catch (error
|
|
471
|
-
|
|
472
|
-
|
|
505
|
+
} catch (error) {
|
|
506
|
+
throw createUpdateError(
|
|
507
|
+
ERROR_PATCH_FAILED,
|
|
508
|
+
`Failed to process bundle patch: ${getErrorMessage(error)}`,
|
|
509
|
+
);
|
|
473
510
|
} finally {
|
|
474
511
|
if (fileIo.accessSync(originBundlePath)) {
|
|
475
512
|
await fileIo.unlink(originBundlePath);
|
|
@@ -533,7 +570,10 @@ export class DownloadTask {
|
|
|
533
570
|
await this.deleteArchiveAndSidecar(params.targetFile);
|
|
534
571
|
const retry = await this.transferArchive(params, false);
|
|
535
572
|
if (retry !== 'done') {
|
|
536
|
-
throw
|
|
573
|
+
throw createUpdateError(
|
|
574
|
+
ERROR_DOWNLOAD_FAILED,
|
|
575
|
+
`Server rejected the download range for ${params.url}`,
|
|
576
|
+
);
|
|
537
577
|
}
|
|
538
578
|
}
|
|
539
579
|
this.downloadPhaseCompleted = true;
|
|
@@ -567,7 +607,10 @@ export class DownloadTask {
|
|
|
567
607
|
? params.deadlineUptimeMs
|
|
568
608
|
: monotonicNowMs() + DOWNLOAD_CALL_TIMEOUT_MS;
|
|
569
609
|
if (deadlineUptimeMs <= monotonicNowMs()) {
|
|
570
|
-
throw
|
|
610
|
+
throw createUpdateError(
|
|
611
|
+
ERROR_DOWNLOAD_FAILED,
|
|
612
|
+
'Download deadline expired before start',
|
|
613
|
+
);
|
|
571
614
|
}
|
|
572
615
|
|
|
573
616
|
// 续传状态盘点:有匹配的 sidecar 且 partial 未收齐才发 Range。
|
|
@@ -636,17 +679,37 @@ export class DownloadTask {
|
|
|
636
679
|
}
|
|
637
680
|
};
|
|
638
681
|
|
|
682
|
+
let freeSpaceReservedUntil = 0;
|
|
639
683
|
const enqueueWrite = (data: ArrayBuffer) => {
|
|
640
684
|
received += data.byteLength;
|
|
641
685
|
if (!writeError && baseOffset + received > MAX_ARCHIVE_BYTES) {
|
|
642
686
|
// 未知长度/分块传输的兜底:超过上限即停写,请求在下面结算时失败。
|
|
643
|
-
writeError =
|
|
687
|
+
writeError = createUpdateError(
|
|
688
|
+
ERROR_PATCH_FAILED,
|
|
689
|
+
`archive too large: exceeded ${MAX_ARCHIVE_BYTES}`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
// 未知长度:响应到达时只能预留安全余量,所以任何会写过已预留字节数的
|
|
693
|
+
// 写入之前先探测,每次至少预留接下来的 PROBE 字节(单个 chunk 更大时按
|
|
694
|
+
// chunk 算)——任何写入都吃不到余量(归档上限本身远大于余量)。失败保留
|
|
695
|
+
// partial 供下次续传。
|
|
696
|
+
const writtenBefore = received - data.byteLength;
|
|
697
|
+
const probeFreeSpace = totalAll <= 0 && received > freeSpaceReservedUntil;
|
|
698
|
+
const reserve = Math.max(
|
|
699
|
+
UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES,
|
|
700
|
+
data.byteLength,
|
|
701
|
+
);
|
|
702
|
+
if (probeFreeSpace) {
|
|
703
|
+
freeSpaceReservedUntil = writtenBefore + reserve;
|
|
644
704
|
}
|
|
645
705
|
writeQueue = writeQueue.then(async () => {
|
|
646
706
|
if (!writer || writeError) {
|
|
647
707
|
return;
|
|
648
708
|
}
|
|
649
709
|
try {
|
|
710
|
+
if (probeFreeSpace) {
|
|
711
|
+
await ensureFreeSpace(params.targetFile, reserve);
|
|
712
|
+
}
|
|
650
713
|
await fileIo.write(writer.fd, data);
|
|
651
714
|
} catch (error) {
|
|
652
715
|
writeError = error as Error;
|
|
@@ -672,7 +735,8 @@ export class DownloadTask {
|
|
|
672
735
|
watchdogTimer = setTimeout(() => {
|
|
673
736
|
if (inactivityReject) {
|
|
674
737
|
inactivityReject(
|
|
675
|
-
|
|
738
|
+
createUpdateError(
|
|
739
|
+
ERROR_DOWNLOAD_FAILED,
|
|
676
740
|
`Download stalled: no data received for ${INACTIVITY_TIMEOUT_MS}ms`,
|
|
677
741
|
),
|
|
678
742
|
);
|
|
@@ -704,12 +768,19 @@ export class DownloadTask {
|
|
|
704
768
|
reject(error);
|
|
705
769
|
try {
|
|
706
770
|
await closeWriter();
|
|
707
|
-
} catch (closeErr
|
|
708
|
-
|
|
771
|
+
} catch (closeErr) {
|
|
772
|
+
logger.error(
|
|
773
|
+
TAG,
|
|
774
|
+
`closeWriter failed after write error: ${getErrorMessage(closeErr)}`,
|
|
775
|
+
);
|
|
709
776
|
}
|
|
710
777
|
});
|
|
711
778
|
});
|
|
712
779
|
});
|
|
780
|
+
// 下面的 race 可能在 dataEnd 到来之前就以 deadline/inactivity 结束,此时
|
|
781
|
+
// dataEndPromise 的 reject 没有订阅者——标记为已处理,避免 unhandled
|
|
782
|
+
// rejection;真正的错误已经由 race 抛出。
|
|
783
|
+
dataEndPromise.catch(() => {});
|
|
713
784
|
|
|
714
785
|
try {
|
|
715
786
|
httpRequest.on('headersReceive', (header: Object) => {
|
|
@@ -780,16 +851,26 @@ export class DownloadTask {
|
|
|
780
851
|
}
|
|
781
852
|
}
|
|
782
853
|
|
|
854
|
+
const remainingMs = Math.max(1, deadlineUptimeMs - monotonicNowMs());
|
|
783
855
|
const deadlinePromise = new Promise<never>((_, reject) => {
|
|
784
856
|
deadlineTimer = setTimeout(() => {
|
|
785
|
-
reject(
|
|
786
|
-
|
|
857
|
+
reject(
|
|
858
|
+
createUpdateError(
|
|
859
|
+
ERROR_DOWNLOAD_FAILED,
|
|
860
|
+
'Download exceeded its whole-call deadline',
|
|
861
|
+
),
|
|
862
|
+
);
|
|
863
|
+
}, remainingMs);
|
|
787
864
|
});
|
|
865
|
+
// netstack 把 readTimeout 交给 libcurl 的 CURLOPT_TIMEOUT_MS(整次传输
|
|
866
|
+
// 总时长)而非空闲超时:固定 60s 会让慢网上的大全量包次次失败。两个
|
|
867
|
+
// 超时都取剩余 deadline;卡死由下面的 60s 不活动看门狗负责——两种
|
|
868
|
+
// 语义下都正确。
|
|
788
869
|
const responseCode = await Promise.race([
|
|
789
870
|
httpRequest.requestInStream(params.url, {
|
|
790
871
|
method: http.RequestMethod.GET,
|
|
791
|
-
readTimeout:
|
|
792
|
-
connectTimeout:
|
|
872
|
+
readTimeout: remainingMs,
|
|
873
|
+
connectTimeout: remainingMs,
|
|
793
874
|
header: requestHeader,
|
|
794
875
|
}),
|
|
795
876
|
deadlinePromise,
|
|
@@ -813,7 +894,10 @@ export class DownloadTask {
|
|
|
813
894
|
// 响应体从未落盘,partial + sidecar 原样保留——那就是续传状态。
|
|
814
895
|
discardBody = true;
|
|
815
896
|
pendingChunks = null;
|
|
816
|
-
throw
|
|
897
|
+
throw createUpdateError(
|
|
898
|
+
ERROR_DOWNLOAD_FAILED,
|
|
899
|
+
`Server error: ${responseCode}`,
|
|
900
|
+
);
|
|
817
901
|
}
|
|
818
902
|
|
|
819
903
|
let encodedBody = false;
|
|
@@ -861,7 +945,10 @@ export class DownloadTask {
|
|
|
861
945
|
);
|
|
862
946
|
}
|
|
863
947
|
if (totalAll > MAX_ARCHIVE_BYTES) {
|
|
864
|
-
writeError =
|
|
948
|
+
writeError = createUpdateError(
|
|
949
|
+
ERROR_PATCH_FAILED,
|
|
950
|
+
`archive too large: ${totalAll} bytes`,
|
|
951
|
+
);
|
|
865
952
|
throw writeError;
|
|
866
953
|
}
|
|
867
954
|
await ensureFreeSpace(
|
|
@@ -910,12 +997,14 @@ export class DownloadTask {
|
|
|
910
997
|
const stats = await fileIo.stat(params.targetFile);
|
|
911
998
|
if (!finalEncoded) {
|
|
912
999
|
if (contentLength > 0 && received !== contentLength) {
|
|
913
|
-
throw
|
|
1000
|
+
throw createUpdateError(
|
|
1001
|
+
ERROR_DOWNLOAD_FAILED,
|
|
914
1002
|
`Download incomplete: expected ${contentLength} bytes but got ${received} bytes`,
|
|
915
1003
|
);
|
|
916
1004
|
}
|
|
917
1005
|
if (totalAll > 0 && stats.size !== totalAll) {
|
|
918
|
-
throw
|
|
1006
|
+
throw createUpdateError(
|
|
1007
|
+
ERROR_DOWNLOAD_FAILED,
|
|
919
1008
|
`Download incomplete: expected ${totalAll} total bytes but got ${stats.size} bytes`,
|
|
920
1009
|
);
|
|
921
1010
|
}
|
|
@@ -936,8 +1025,8 @@ export class DownloadTask {
|
|
|
936
1025
|
}
|
|
937
1026
|
return 'done';
|
|
938
1027
|
} catch (error) {
|
|
939
|
-
|
|
940
|
-
throw error;
|
|
1028
|
+
logger.error(TAG, `Download failed: ${getErrorMessage(error)}`);
|
|
1029
|
+
throw toUpdateError(error, ERROR_DOWNLOAD_FAILED);
|
|
941
1030
|
} finally {
|
|
942
1031
|
clearWatchdog();
|
|
943
1032
|
if (deadlineTimer !== null) {
|
|
@@ -951,7 +1040,7 @@ export class DownloadTask {
|
|
|
951
1040
|
try {
|
|
952
1041
|
await closeWriter();
|
|
953
1042
|
} catch (closeError) {
|
|
954
|
-
|
|
1043
|
+
logger.error(TAG, `Failed to close file: ${getErrorMessage(closeError)}`);
|
|
955
1044
|
}
|
|
956
1045
|
httpRequest.off('headersReceive');
|
|
957
1046
|
httpRequest.off('dataReceive');
|
|
@@ -969,44 +1058,47 @@ export class DownloadTask {
|
|
|
969
1058
|
});
|
|
970
1059
|
}
|
|
971
1060
|
|
|
972
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* 三种安装共用的脚手架:下载 → 重建 staging → 归档摘要(工作线程)→ 解压
|
|
1063
|
+
* (含上限校验)。返回 staging 目录。下载完成后的失败一律是 patch 应用
|
|
1064
|
+
* 失败(见 execute 的归码)。
|
|
1065
|
+
*/
|
|
1066
|
+
private async downloadAndExtract(params: DownloadTaskParams): Promise<string> {
|
|
973
1067
|
await this.downloadFile(params);
|
|
974
1068
|
const work = this.stagingDirectory(params);
|
|
975
1069
|
await this.recreateDirectory(work);
|
|
976
|
-
this.artifactSha256 = NativePatchCore.
|
|
1070
|
+
this.artifactSha256 = await NativePatchCore.sha256HexFileAsync(
|
|
1071
|
+
params.targetFile,
|
|
1072
|
+
);
|
|
977
1073
|
await this.extractArchive(params.targetFile, work);
|
|
978
|
-
|
|
1074
|
+
return work;
|
|
979
1075
|
}
|
|
980
1076
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
1077
|
+
/** 解压目录里的条目名 + __diff.json 解析结果,供 patch plan 使用。 */
|
|
1078
|
+
private async readPatchInputs(
|
|
1079
|
+
work: string,
|
|
1080
|
+
normalizeResourceCopies: boolean,
|
|
1081
|
+
): Promise<PatchInputs> {
|
|
1082
|
+
const results = await Promise.all([
|
|
1083
|
+
this.listEntryNames(work),
|
|
1084
|
+
this.readManifestArrays(work, normalizeResourceCopies),
|
|
1085
|
+
]);
|
|
1086
|
+
return { entryNames: results[0], manifestArrays: results[1] };
|
|
992
1087
|
}
|
|
993
1088
|
|
|
994
|
-
private async
|
|
995
|
-
params
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
await this.recreateDirectory(work);
|
|
999
|
-
this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
|
|
1089
|
+
private async doFullPatch(params: DownloadTaskParams): Promise<void> {
|
|
1090
|
+
await this.downloadAndExtract(params);
|
|
1091
|
+
await this.deleteArchiveAndSidecar(params.targetFile);
|
|
1092
|
+
}
|
|
1000
1093
|
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
]);
|
|
1094
|
+
private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
|
|
1095
|
+
const work = await this.downloadAndExtract(params);
|
|
1096
|
+
const inputs = await this.readPatchInputs(work, true);
|
|
1097
|
+
const manifestArrays = inputs.manifestArrays;
|
|
1006
1098
|
|
|
1007
1099
|
NativePatchCore.buildArchivePatchPlan(
|
|
1008
1100
|
ARCHIVE_PATCH_TYPE_FROM_PACKAGE,
|
|
1009
|
-
entryNames,
|
|
1101
|
+
inputs.entryNames,
|
|
1010
1102
|
manifestArrays.copyFroms,
|
|
1011
1103
|
manifestArrays.copyTos,
|
|
1012
1104
|
manifestArrays.deletes,
|
|
@@ -1015,7 +1107,7 @@ export class DownloadTask {
|
|
|
1015
1107
|
|
|
1016
1108
|
const bundlePatchPath = `${work}/${HARMONY_BUNDLE_PATCH_ENTRY}`;
|
|
1017
1109
|
if (!fileIo.accessSync(bundlePatchPath)) {
|
|
1018
|
-
throw
|
|
1110
|
+
throw createUpdateError(ERROR_PATCH_FAILED, 'bundle patch not found');
|
|
1019
1111
|
}
|
|
1020
1112
|
const resourceManager = this.context.resourceManager;
|
|
1021
1113
|
const originContent = await resourceManager.getRawFileContent(
|
|
@@ -1049,32 +1141,13 @@ export class DownloadTask {
|
|
|
1049
1141
|
}
|
|
1050
1142
|
|
|
1051
1143
|
private async doPatchFromPpk(params: DownloadTaskParams): Promise<void> {
|
|
1052
|
-
await this.
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
} catch (error: any) {
|
|
1056
|
-
// 同 doPatchFromApp:下载完成后的失败按 patch 失败归类。
|
|
1057
|
-
error.code = 'PATCH_FAILED';
|
|
1058
|
-
throw error;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
private async doPatchFromPpkAfterDownload(
|
|
1063
|
-
params: DownloadTaskParams,
|
|
1064
|
-
): Promise<void> {
|
|
1065
|
-
const work = this.stagingDirectory(params);
|
|
1066
|
-
await this.recreateDirectory(work);
|
|
1067
|
-
this.artifactSha256 = NativePatchCore.sha256HexFile(params.targetFile);
|
|
1068
|
-
|
|
1069
|
-
await this.extractArchive(params.targetFile, work);
|
|
1070
|
-
const [entryNames, manifestArrays] = await Promise.all([
|
|
1071
|
-
this.listEntryNames(work),
|
|
1072
|
-
this.readManifestArrays(work, false),
|
|
1073
|
-
]);
|
|
1144
|
+
const work = await this.downloadAndExtract(params);
|
|
1145
|
+
const inputs = await this.readPatchInputs(work, false);
|
|
1146
|
+
const manifestArrays = inputs.manifestArrays;
|
|
1074
1147
|
|
|
1075
1148
|
const plan = NativePatchCore.buildArchivePatchPlan(
|
|
1076
1149
|
ARCHIVE_PATCH_TYPE_FROM_PPK,
|
|
1077
|
-
entryNames,
|
|
1150
|
+
inputs.entryNames,
|
|
1078
1151
|
manifestArrays.copyFroms,
|
|
1079
1152
|
manifestArrays.copyTos,
|
|
1080
1153
|
manifestArrays.deletes,
|
|
@@ -1093,7 +1166,7 @@ export class DownloadTask {
|
|
|
1093
1166
|
enableMerge: plan.enableMerge,
|
|
1094
1167
|
bundleHbcTransformMeta: manifestArrays.hbcTransformMeta,
|
|
1095
1168
|
});
|
|
1096
|
-
|
|
1169
|
+
logger.info(TAG, 'Patch from PPK completed');
|
|
1097
1170
|
await this.deleteArchiveAndSidecar(params.targetFile);
|
|
1098
1171
|
}
|
|
1099
1172
|
|
|
@@ -1122,14 +1195,7 @@ export class DownloadTask {
|
|
|
1122
1195
|
.replace(/\.[^.]+$/, '');
|
|
1123
1196
|
const mediaBuffer = await resourceManager.getMediaByName(mediaName);
|
|
1124
1197
|
this.verifyCopySourceCrc(mediaBuffer, expectedCrc, currentFrom);
|
|
1125
|
-
|
|
1126
|
-
...new Set(
|
|
1127
|
-
targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
|
|
1128
|
-
),
|
|
1129
|
-
];
|
|
1130
|
-
await Promise.all(
|
|
1131
|
-
parentDirs.map(dir => this.ensureDirectory(dir)),
|
|
1132
|
-
);
|
|
1198
|
+
await this.ensureParentDirectories(targets);
|
|
1133
1199
|
await Promise.all(
|
|
1134
1200
|
targets.map(target => this.writeFileContent(target, mediaBuffer)),
|
|
1135
1201
|
);
|
|
@@ -1143,7 +1209,8 @@ export class DownloadTask {
|
|
|
1143
1209
|
const rawContent =
|
|
1144
1210
|
await resourceManager.getRawFileContent(currentFrom);
|
|
1145
1211
|
this.verifyCopySourceCrc(rawContent, expectedCrc, currentFrom);
|
|
1146
|
-
const
|
|
1212
|
+
const firstTarget = targets[0];
|
|
1213
|
+
const restTargets = targets.slice(1);
|
|
1147
1214
|
await this.writeFileContent(firstTarget, rawContent);
|
|
1148
1215
|
await Promise.all(
|
|
1149
1216
|
restTargets.map(target => this.copySandboxFile(firstTarget, target)),
|
|
@@ -1153,15 +1220,9 @@ export class DownloadTask {
|
|
|
1153
1220
|
|
|
1154
1221
|
const fromContent = await resourceManager.getRawFd(currentFrom);
|
|
1155
1222
|
try {
|
|
1156
|
-
const
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
|
|
1160
|
-
),
|
|
1161
|
-
];
|
|
1162
|
-
await Promise.all(
|
|
1163
|
-
parentDirs.map(dir => this.ensureDirectory(dir))
|
|
1164
|
-
);
|
|
1223
|
+
const firstTarget = targets[0];
|
|
1224
|
+
const restTargets = targets.slice(1);
|
|
1225
|
+
await this.ensureParentDirectories(targets);
|
|
1165
1226
|
if (fileIo.accessSync(firstTarget)) {
|
|
1166
1227
|
await fileIo.unlink(firstTarget);
|
|
1167
1228
|
}
|
|
@@ -1173,23 +1234,35 @@ export class DownloadTask {
|
|
|
1173
1234
|
try {
|
|
1174
1235
|
await resourceManager.closeRawFd(currentFrom);
|
|
1175
1236
|
} catch (closeError) {
|
|
1176
|
-
|
|
1237
|
+
logger.error(
|
|
1238
|
+
TAG,
|
|
1239
|
+
`Failed to close raw fd for ${currentFrom}: ${getErrorMessage(closeError)}`,
|
|
1240
|
+
);
|
|
1177
1241
|
}
|
|
1178
1242
|
}
|
|
1179
1243
|
}
|
|
1180
|
-
} catch (error
|
|
1181
|
-
error
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
error.code +
|
|
1186
|
-
',' +
|
|
1187
|
-
error.message;
|
|
1188
|
-
console.error(error);
|
|
1189
|
-
throw error;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
const coded = toUpdateError(error, ERROR_PATCH_FAILED);
|
|
1246
|
+
const message = `Copy from resource failed: ${currentFrom}, ${getErrorMessage(error)}`;
|
|
1247
|
+
logger.error(TAG, message);
|
|
1248
|
+
throw createUpdateError(coded.code, message);
|
|
1190
1249
|
}
|
|
1191
1250
|
}
|
|
1192
1251
|
|
|
1252
|
+
/** 去重后并行创建所有目标文件的父目录。 */
|
|
1253
|
+
private async ensureParentDirectories(targets: string[]): Promise<void> {
|
|
1254
|
+
const parentDirs = new Set<string>();
|
|
1255
|
+
for (const target of targets) {
|
|
1256
|
+
const parent = target.substring(0, target.lastIndexOf('/'));
|
|
1257
|
+
if (parent) {
|
|
1258
|
+
parentDirs.add(parent);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
await Promise.all(
|
|
1262
|
+
Array.from(parentDirs).map(dir => this.ensureDirectory(dir)),
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1193
1266
|
// pdiff 拷贝源内容校验(__diff.json copiesCrc):不符即抛错,整次 patch
|
|
1194
1267
|
// 失败后 JS 策略链自动回退 full,绝不把漂移的资源装进版本目录。
|
|
1195
1268
|
private verifyCopySourceCrc(
|
|
@@ -1202,7 +1275,10 @@ export class DownloadTask {
|
|
|
1202
1275
|
}
|
|
1203
1276
|
const actualCrc = NativePatchCore.crc32(content);
|
|
1204
1277
|
if (actualCrc !== expectedCrc) {
|
|
1205
|
-
throw
|
|
1278
|
+
throw createUpdateError(
|
|
1279
|
+
ERROR_PATCH_FAILED,
|
|
1280
|
+
`resource content mismatch (crc32): ${from}`,
|
|
1281
|
+
);
|
|
1206
1282
|
}
|
|
1207
1283
|
}
|
|
1208
1284
|
|
|
@@ -1210,14 +1286,13 @@ export class DownloadTask {
|
|
|
1210
1286
|
try {
|
|
1211
1287
|
await NativePatchCore.cleanupOldEntries(
|
|
1212
1288
|
params.unzipDirectory,
|
|
1213
|
-
params.hash
|
|
1214
|
-
params.originHash || '',
|
|
1289
|
+
[params.hash, params.originHash].filter(name => !!name),
|
|
1215
1290
|
3,
|
|
1216
1291
|
);
|
|
1217
|
-
} catch (error
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
throw
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
const message = `Cleanup failed: ${getErrorMessage(error)}`;
|
|
1294
|
+
logger.error(TAG, message);
|
|
1295
|
+
throw createUpdateError(ERROR_FILE_OPERATION_FAILED, message);
|
|
1221
1296
|
}
|
|
1222
1297
|
}
|
|
1223
1298
|
|
|
@@ -1247,13 +1322,24 @@ export class DownloadTask {
|
|
|
1247
1322
|
await this.deleteResumeSidecar(params.targetFile);
|
|
1248
1323
|
break;
|
|
1249
1324
|
default:
|
|
1250
|
-
throw
|
|
1325
|
+
throw createUpdateError(
|
|
1326
|
+
ERROR_INVALID_OPTIONS,
|
|
1327
|
+
`Unknown task type: ${params.type}`,
|
|
1328
|
+
);
|
|
1251
1329
|
}
|
|
1252
1330
|
if (isPatchTask) {
|
|
1253
1331
|
await this.promoteStaging(params);
|
|
1254
1332
|
}
|
|
1255
|
-
} catch (
|
|
1256
|
-
|
|
1333
|
+
} catch (rawError) {
|
|
1334
|
+
// 稳定错误码:下载阶段的失败是 DOWNLOAD_FAILED;归档收齐之后的失败
|
|
1335
|
+
// (解压/hpatch/资源拷贝含 copiesCrc 校验/完成记录)一律 PATCH_FAILED,
|
|
1336
|
+
// JS 层 patch-health 遥测据此区分网络失败与补丁失败。已带码的错误
|
|
1337
|
+
// (磁盘空间、参数)原样保留。
|
|
1338
|
+
const error = toUpdateError(
|
|
1339
|
+
rawError,
|
|
1340
|
+
this.downloadPhaseCompleted ? ERROR_PATCH_FAILED : ERROR_DOWNLOAD_FAILED,
|
|
1341
|
+
);
|
|
1342
|
+
logger.error(TAG, `Task execution failed: ${error.message}`);
|
|
1257
1343
|
if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
|
|
1258
1344
|
try {
|
|
1259
1345
|
if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
|
|
@@ -1271,8 +1357,11 @@ export class DownloadTask {
|
|
|
1271
1357
|
await this.deleteArchiveAndSidecar(params.targetFile);
|
|
1272
1358
|
}
|
|
1273
1359
|
}
|
|
1274
|
-
} catch (cleanupError
|
|
1275
|
-
|
|
1360
|
+
} catch (cleanupError) {
|
|
1361
|
+
logger.error(
|
|
1362
|
+
TAG,
|
|
1363
|
+
`Cleanup after error failed: ${getErrorMessage(cleanupError)}`,
|
|
1364
|
+
);
|
|
1276
1365
|
}
|
|
1277
1366
|
}
|
|
1278
1367
|
throw error;
|