react-native-update 10.55.1 → 10.56.1

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.
Files changed (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +399 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +43 -3
  63. package/src/client.ts +236 -82
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. 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
- export const VERSION_COMPLETE_FILE_NAME = '.pushy-complete';
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 [to, rawFrom] of Object.entries(copies)) {
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 不符返回 -1——那样追加的字节不可信。
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
- if (Number.isNaN(start) || start !== expectedStart) {
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 <= 0 ? -1 : 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 Error(`bundle missing after install: ${bundlePath}`);
199
+ throw createUpdateError(
200
+ ERROR_PATCH_FAILED,
201
+ `bundle missing after install: ${bundlePath}`,
202
+ );
174
203
  }
175
- const bundleSha256 = NativePatchCore.sha256HexFile(bundlePath);
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
- console.error('Failed to persist resume sidecar:', e);
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
- console.error('Failed to delete resume sidecar:', e);
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
- console.error('Failed to delete archive:', e);
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
- const res = fileIo.accessSync(path);
255
- if (res) {
256
- const stat = await fileIo.stat(path);
257
- if (stat.isDirectory()) {
258
- const files = await fileIo.listFile(path);
259
-
260
- const entries = files.filter(file => file !== '.' && file !== '..');
261
- const DELETE_CONCURRENCY = 32;
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
- console.error('Failed to delete directory:', error);
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
- * The completion marker is written by this SDK only, after the whole
310
- * install succeeded. zlib.decompressFile offers no per-entry filter, so an
311
- * archive that ships its own marker is detected right after extraction:
312
- * the marker is removed (so the failure cleanup below cannot mistake the
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 rejectReservedEntries(unzipDirectory: string): Promise<void> {
316
- const marker = `${unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`;
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 fileIo.unlink(marker);
338
+ size = await zlib.getOriginalSize(archiveFile);
322
339
  } catch (e) {
323
- // The directory is removed by the failure cleanup either way.
340
+ throw createUpdateError(
341
+ ERROR_PATCH_FAILED,
342
+ `cannot determine archive expansion size: ${getErrorMessage(e)}`,
343
+ );
324
344
  }
325
- const error: Error & { code?: string } = Error(
326
- `archive contains reserved entry ${VERSION_COMPLETE_FILE_NAME}`,
327
- );
328
- error.code = 'PATCH_FAILED';
329
- throw error;
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
- * 没有逐条钩子:解压前按归档大小与 2 倍启发式查磁盘,解压后统计条目数与
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 Error(`archive too large: ${archiveStat.size} bytes`);
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 Error(`patch manifest too large: ${manifestStat.size} bytes`);
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: any) {
471
- error.message = `Failed to process bundle patch: ${error.message}`;
472
- throw error;
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 Error(`Server rejected the download range for ${params.url}`);
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 Error('Download deadline expired before start');
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 = Error(`archive too large: exceeded ${MAX_ARCHIVE_BYTES}`);
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
- Error(
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: any) {
708
- console.error('closeWriter failed after write error:', closeErr);
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(Error('Download exceeded its whole-call deadline'));
786
- }, Math.max(1, deadlineUptimeMs - monotonicNowMs()));
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: 60000,
792
- connectTimeout: 60000,
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 Error(`Server error: ${responseCode}`);
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 = Error(`archive too large: ${totalAll} bytes`);
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 Error(
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 Error(
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
- console.error('Download failed:', error);
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
- console.error('Failed to close file:', closeError);
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
- private async doFullPatch(params: DownloadTaskParams): Promise<void> {
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.sha256HexFile(params.targetFile);
1070
+ this.artifactSha256 = await NativePatchCore.sha256HexFileAsync(
1071
+ params.targetFile,
1072
+ );
977
1073
  await this.extractArchive(params.targetFile, work);
978
- await this.deleteArchiveAndSidecar(params.targetFile);
1074
+ return work;
979
1075
  }
980
1076
 
981
- private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
982
- await this.downloadFile(params);
983
- try {
984
- await this.doPatchFromAppAfterDownload(params);
985
- } catch (error: any) {
986
- // 下载已完成,之后的失败都是 patch 应用失败(解压/hpatch/资源拷贝,
987
- // 含 copiesCrc 校验)。打上稳定码,JS 层遥测按 patch_fail 归类,
988
- // 而不是混进网络失败。
989
- error.code = 'PATCH_FAILED';
990
- throw error;
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 doPatchFromAppAfterDownload(
995
- params: DownloadTaskParams,
996
- ): Promise<void> {
997
- const work = this.stagingDirectory(params);
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
- await this.extractArchive(params.targetFile, work);
1002
- const [entryNames, manifestArrays] = await Promise.all([
1003
- this.listEntryNames(work),
1004
- this.readManifestArrays(work, true),
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 Error('bundle patch not found');
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.downloadFile(params);
1053
- try {
1054
- await this.doPatchFromPpkAfterDownload(params);
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
- console.info('Patch from PPK completed');
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
- const parentDirs = [
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 [firstTarget, ...restTargets] = targets;
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 [firstTarget, ...restTargets] = targets;
1157
- const parentDirs = [
1158
- ...new Set(
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
- console.error(`Failed to close raw fd for ${currentFrom}:`, closeError);
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: any) {
1181
- error.message =
1182
- 'Copy from resource failed:' +
1183
- currentFrom +
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 new Error(`resource content mismatch (crc32): ${from}`);
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: any) {
1218
- error.message = 'Cleanup failed:' + error.message;
1219
- console.error(error);
1220
- throw error;
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 Error(`Unknown task type: ${params.type}`);
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 (error: any) {
1256
- console.error('Task execution failed:', error.message);
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: any) {
1275
- console.error('Cleanup after error failed:', cleanupError.message);
1360
+ } catch (cleanupError) {
1361
+ logger.error(
1362
+ TAG,
1363
+ `Cleanup after error failed: ${getErrorMessage(cleanupError)}`,
1364
+ );
1276
1365
  }
1277
1366
  }
1278
1367
  throw error;