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.
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 +36 -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
@@ -0,0 +1,75 @@
1
+ // ArkTS mirror of cpp/patch_core/error_codes.h — keep in sync by hand
2
+ // (harmony/pushy/src/test/check-constant-parity.js asserts the values match).
3
+ // 与 Android/iOS 共用的稳定错误码:JS 层(src/error.ts)按 code 聚合遥测,
4
+ // 消息文案各平台可不同,只有码是契约。
5
+ //
6
+ // RNOH 的异步桥以纯字符串 reject,Error 上的 `code` 属性到 JS 侧很可能已丢。
7
+ // 因此每个错误的消息都以 `[CODE] ` 前缀开头,JS 鸿蒙分支据此解析回 code。
8
+ // 本文件不依赖任何其他模块,任何文件都可安全引用它而不会形成 import 环。
9
+
10
+ export const ERROR_INVALID_OPTIONS = 'INVALID_OPTIONS';
11
+ export const ERROR_DOWNLOAD_FAILED = 'DOWNLOAD_FAILED';
12
+ export const ERROR_PATCH_FAILED = 'PATCH_FAILED';
13
+ export const ERROR_FILE_OPERATION_FAILED = 'FILE_OPERATION_FAILED';
14
+ export const ERROR_SWITCH_VERSION_FAILED = 'SWITCH_VERSION_FAILED';
15
+ export const ERROR_MARK_SUCCESS_FAILED = 'MARK_SUCCESS_FAILED';
16
+ export const ERROR_RESTART_FAILED = 'RESTART_FAILED';
17
+ export const ERROR_RESET_FAILED = 'RESET_FAILED';
18
+ export const ERROR_INVALID_HASH_INFO = 'INVALID_HASH_INFO';
19
+ export const ERROR_UNSUPPORTED_PLATFORM = 'UNSUPPORTED_PLATFORM';
20
+ export const ERROR_APK_INSTALL_PERMISSION_REQUIRED =
21
+ 'APK_INSTALL_PERMISSION_REQUIRED';
22
+ export const ERROR_APK_INSTALL_FAILED = 'APK_INSTALL_FAILED';
23
+
24
+ /**
25
+ * 带稳定错误码的 Error:`code` 是属性,消息同时带 `[CODE] ` 前缀(桥丢属性时
26
+ * JS 仍能解析)。始终经 createUpdateError / toUpdateError 构造。
27
+ */
28
+ export class UpdateError extends Error {
29
+ readonly code: string;
30
+
31
+ constructor(code: string, message: string) {
32
+ super(`[${code}] ${message}`);
33
+ this.name = 'UpdateError';
34
+ this.code = code;
35
+ }
36
+ }
37
+
38
+ /** 任意抛出值的可读消息(Error 取 message,其余 String())。 */
39
+ export function getErrorMessage(error: unknown): string {
40
+ if (error instanceof Error) {
41
+ return error.message;
42
+ }
43
+ if (error && typeof error === 'object' && 'message' in error) {
44
+ return String((error as Record<string, unknown>).message);
45
+ }
46
+ return String(error);
47
+ }
48
+
49
+ export function createUpdateError(code: string, message: string): UpdateError {
50
+ return new UpdateError(code, message);
51
+ }
52
+
53
+ /**
54
+ * 给未带码的错误补上 `code`:已是 UpdateError 的原样返回(保留其更精确的码
55
+ * 与前缀,绝不叠两层前缀),其余按 defaultCode 重包并保留原消息。
56
+ */
57
+ export function toUpdateError(error: unknown, defaultCode: string): UpdateError {
58
+ if (error instanceof UpdateError) {
59
+ return error;
60
+ }
61
+ return new UpdateError(defaultCode, getErrorMessage(error));
62
+ }
63
+
64
+ /** 从 `[CODE] message` 形式的消息里解析错误码;无前缀返回空串。 */
65
+ export function parseErrorCodePrefix(message: string): string {
66
+ if (typeof message !== 'string' || !message.startsWith('[')) {
67
+ return '';
68
+ }
69
+ const end = message.indexOf('] ');
70
+ if (end <= 1) {
71
+ return '';
72
+ }
73
+ const code = message.substring(1, end);
74
+ return /^[A-Z][A-Z0-9_]*$/.test(code) ? code : '';
75
+ }
@@ -1,13 +1,16 @@
1
- type EventCallback = (data: any) => void;
1
+ // The subset of RNOH's RNInstance the SDK needs: device events for download
2
+ // progress. Typed structurally so the hypium tests can pass a mock.
3
+ export interface DeviceEventEmitter {
4
+ emitDeviceEvent(eventName: string, payload: Object): void;
5
+ }
2
6
 
3
7
  export class EventHub {
4
8
  private static instance: EventHub;
5
- private listeners: Map<string, Set<EventCallback>>;
6
- private rnInstance: any;
9
+ // 指向最近一次构造 TurboModule 的 RNInstance:RNOH 重建 RN 实例时会构造
10
+ // 新的 TurboModule 并在此替换,已销毁实例上的 emit 由 RNOH 自行忽略。
11
+ private rnInstance: DeviceEventEmitter | undefined = undefined;
7
12
 
8
- private constructor() {
9
- this.listeners = new Map();
10
- }
13
+ private constructor() {}
11
14
 
12
15
  public static getInstance(): EventHub {
13
16
  if (!EventHub.instance) {
@@ -16,24 +19,23 @@ export class EventHub {
16
19
  return EventHub.instance;
17
20
  }
18
21
 
19
- public on(event: string, callback: EventCallback): void {
20
- if (!this.listeners.has(event)) {
21
- this.listeners.set(event, new Set());
22
- }
23
- this.listeners.get(event)?.add(callback);
24
- }
25
-
26
- public off(event: string, callback: EventCallback): void {
27
- this.listeners.get(event)?.delete(callback);
28
- }
29
-
30
- public emit(event: string, data: any): void {
22
+ public emit(event: string, data: Object): void {
31
23
  if (this.rnInstance) {
32
24
  this.rnInstance.emitDeviceEvent(event, data);
33
25
  }
34
26
  }
35
27
 
36
- setRNInstance(instance: any) {
28
+ setRNInstance(instance: DeviceEventEmitter | undefined): void {
37
29
  this.rnInstance = instance;
38
30
  }
39
- }
31
+
32
+ /**
33
+ * TurboModule 销毁时调用:只在仍指向该实例时清除——RNOH 重建 RN 实例时
34
+ * 新 TurboModule 可能先于旧实例的销毁回调构造,不能把新引用一并清掉。
35
+ */
36
+ clearRNInstance(instance: DeviceEventEmitter | undefined): void {
37
+ if (instance !== undefined && this.rnInstance === instance) {
38
+ this.rnInstance = undefined;
39
+ }
40
+ }
41
+ }
@@ -1,14 +1,19 @@
1
1
  import fileIo from '@ohos.file.fs';
2
2
  import { util } from '@kit.ArkTS';
3
3
  import NativePatchCore from './NativePatchCore';
4
+ import { ERROR_SWITCH_VERSION_FAILED, createUpdateError } from './ErrorCodes';
4
5
 
5
- // ArkTS mirror of cpp/patch_core/install_record.h — keep in sync by hand.
6
+ // ArkTS mirror of cpp/patch_core/install_record.h — keep in sync by hand
7
+ // (harmony/pushy/src/test/check-constant-parity.js asserts the values match).
6
8
  // 两阶段安装的完成记录:作为安装最后一步写进 staging 目录,随后 staging
7
9
  // 原子 rename 成版本目录;激活前重新校验 bundle 摘要。空文件 = 旧版 SDK
8
10
  // 写的历史标记,仍视为完整安装(无摘要可验)。
9
11
  export const INSTALL_RECORD_SCHEMA = 1;
10
12
  export const INSTALL_RECORD_FILE_NAME = '.pushy-complete';
11
13
  export const STAGING_SUFFIX = '.staging';
14
+ // 归档不得携带任何 `.pushy-` 前缀的条目(任意深度):这些名字保留给 SDK 自己
15
+ // 写的记录,与 Android 的规则一致。
16
+ export const RESERVED_ENTRY_PREFIX = '.pushy-';
12
17
  export const HARMONY_BUNDLE_FILE_NAME = 'bundle.harmony.js';
13
18
 
14
19
  export interface InstallRecordData {
@@ -22,6 +27,10 @@ export function stagingDirectoryFor(versionDir: string): string {
22
27
  return `${versionDir}${STAGING_SUFFIX}`;
23
28
  }
24
29
 
30
+ export function isReservedEntryName(name: string): boolean {
31
+ return name.startsWith(RESERVED_ENTRY_PREFIX);
32
+ }
33
+
25
34
  export function buildInstallRecord(
26
35
  versionHash: string,
27
36
  bundleSha256: string,
@@ -63,7 +72,25 @@ export async function writeInstallRecord(
63
72
  }
64
73
 
65
74
  /**
66
- * 解析记录:文件缺失/畸形返回 null;空文件(历史标记)返回 {}。
75
+ * 解析记录文本:空串(历史标记)返回 {};畸形/非对象返回 null。纯函数,
76
+ * 供 readInstallRecord 与单测共用。
77
+ */
78
+ export function parseInstallRecord(text: string): InstallRecordData | null {
79
+ if (text.length === 0) {
80
+ return {};
81
+ }
82
+ try {
83
+ const parsed = JSON.parse(text) as InstallRecordData | null;
84
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
85
+ ? parsed
86
+ : null;
87
+ } catch (e) {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 读取记录:文件缺失/畸形返回 null;空文件(历史标记)返回 {}。
67
94
  */
68
95
  export function readInstallRecord(versionDir: string): InstallRecordData | null {
69
96
  const path = `${versionDir}/${INSTALL_RECORD_FILE_NAME}`;
@@ -78,17 +105,17 @@ export function readInstallRecord(versionDir: string): InstallRecordData | null
78
105
  if (stat.size > 64 * 1024) {
79
106
  return null;
80
107
  }
81
- const text = fileIo.readTextSync(path);
82
- const parsed = JSON.parse(text) as InstallRecordData | null;
83
- return parsed !== null && typeof parsed === 'object' ? parsed : null;
108
+ return parseInstallRecord(fileIo.readTextSync(path));
84
109
  } catch (e) {
85
110
  return null;
86
111
  }
87
112
  }
88
113
 
89
- /** 存在性判定(启动/去重路径,不算摘要):记录存在且(非历史空文件时)指向本版本。 */
90
- export function isInstallComplete(versionDir: string, versionHash: string): boolean {
91
- const record = readInstallRecord(versionDir);
114
+ /** 记录是否宣告本版本完整安装(历史空记录视为是)。纯函数。 */
115
+ export function isRecordForVersion(
116
+ record: InstallRecordData | null,
117
+ versionHash: string,
118
+ ): boolean {
92
119
  if (record === null) {
93
120
  return false;
94
121
  }
@@ -100,30 +127,61 @@ export function isInstallComplete(versionDir: string, versionHash: string): bool
100
127
  );
101
128
  }
102
129
 
103
- /** 激活前校验:记录带摘要时重新计算 bundle 摘要;不通过则抛错说明原因。 */
104
- export function verifyInstallForActivation(
105
- versionDir: string,
130
+ /** 存在性判定(启动/去重路径,不算摘要):记录存在且(非历史空文件时)指向本版本。 */
131
+ export function isInstallComplete(versionDir: string, versionHash: string): boolean {
132
+ return isRecordForVersion(readInstallRecord(versionDir), versionHash);
133
+ }
134
+
135
+ /**
136
+ * 激活前的记录校验(不含摘要):返回需要复核的 bundle 摘要,历史空记录或
137
+ * 无摘要记录返回空串;记录缺失/不符抛 SWITCH_VERSION_FAILED。纯函数。
138
+ */
139
+ export function expectedBundleSha256ForActivation(
140
+ record: InstallRecordData | null,
106
141
  versionHash: string,
107
- bundlePath: string,
108
- ): void {
109
- const record = readInstallRecord(versionDir);
142
+ ): string {
110
143
  if (record === null) {
111
- throw Error(`Bundle version ${versionHash} has no valid completion record.`);
144
+ throw createUpdateError(
145
+ ERROR_SWITCH_VERSION_FAILED,
146
+ `Bundle version ${versionHash} has no valid completion record.`,
147
+ );
112
148
  }
113
149
  if (Object.keys(record).length === 0) {
114
- return;
150
+ return '';
115
151
  }
116
152
  if (
117
153
  record.schema !== INSTALL_RECORD_SCHEMA ||
118
154
  record.versionHash !== versionHash
119
155
  ) {
120
- throw Error(`Bundle version ${versionHash} completion record mismatch.`);
156
+ throw createUpdateError(
157
+ ERROR_SWITCH_VERSION_FAILED,
158
+ `Bundle version ${versionHash} completion record mismatch.`,
159
+ );
121
160
  }
122
- if (!record.bundleSha256) {
161
+ return record.bundleSha256 ?? '';
162
+ }
163
+
164
+ /**
165
+ * 激活前校验:记录带摘要时在 native 工作线程重新计算 bundle 摘要(整包
166
+ * 几十 MB,不能在 UI 线程同步算);不通过则抛错说明原因。
167
+ */
168
+ export async function verifyInstallForActivation(
169
+ versionDir: string,
170
+ versionHash: string,
171
+ bundlePath: string,
172
+ ): Promise<void> {
173
+ const expected = expectedBundleSha256ForActivation(
174
+ readInstallRecord(versionDir),
175
+ versionHash,
176
+ );
177
+ if (!expected) {
123
178
  return;
124
179
  }
125
- const actual = NativePatchCore.sha256HexFile(bundlePath);
126
- if (actual.toLowerCase() !== record.bundleSha256.toLowerCase()) {
127
- throw Error(`Bundle version ${versionHash} bundle digest mismatch.`);
180
+ const actual = await NativePatchCore.sha256HexFileAsync(bundlePath);
181
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
182
+ throw createUpdateError(
183
+ ERROR_SWITCH_VERSION_FAILED,
184
+ `Bundle version ${versionHash} bundle digest mismatch.`,
185
+ );
128
186
  }
129
187
  }
@@ -16,36 +16,62 @@ class Logger {
16
16
  this.isDebug = isDebug;
17
17
  }
18
18
 
19
- private normalizeArgs(args: string[]): [string, string] {
20
- if (args.length === 0) {
21
- return ['', ''];
22
- }
23
- if (args.length === 1) {
24
- return [args[0], ''];
25
- }
26
- return [args[0], args.slice(1).join(' ')];
19
+ // debug 级别默认关闭;宿主处于 RN 调试模式(RNOH isDebugModeEnabled)时由
20
+ // TurboModule 在启动时打开,方便接入调试。info/warn/error 永远输出。
21
+ // (HAR 自身没有可靠的构建期 DEBUG 信号:hvigor 只为宿主模块生成
22
+ // BuildProfile,在 HAR 里 import 'BuildProfile' 无法解析。)
23
+ setDebug(enabled: boolean): void {
24
+ this.isDebug = enabled;
25
+ }
26
+
27
+ private tagOf(args: string[]): string {
28
+ return args.length > 0 ? args[0] : '';
29
+ }
30
+
31
+ private messageOf(args: string[]): string {
32
+ return args.length > 1 ? args.slice(1).join(' ') : '';
27
33
  }
28
34
 
29
35
  debug(...args: string[]): void {
30
36
  if (this.isDebug) {
31
- const [tag, message] = this.normalizeArgs(args);
32
- hilog.debug(this.domain, this.prefix, this.format, tag, message);
37
+ hilog.debug(
38
+ this.domain,
39
+ this.prefix,
40
+ this.format,
41
+ this.tagOf(args),
42
+ this.messageOf(args),
43
+ );
33
44
  }
34
45
  }
35
46
 
36
47
  info(...args: string[]): void {
37
- const [tag, message] = this.normalizeArgs(args);
38
- hilog.info(this.domain, this.prefix, this.format, tag, message);
48
+ hilog.info(
49
+ this.domain,
50
+ this.prefix,
51
+ this.format,
52
+ this.tagOf(args),
53
+ this.messageOf(args),
54
+ );
39
55
  }
40
56
 
41
57
  warn(...args: string[]): void {
42
- const [tag, message] = this.normalizeArgs(args);
43
- hilog.warn(this.domain, this.prefix, this.format, tag, message);
58
+ hilog.warn(
59
+ this.domain,
60
+ this.prefix,
61
+ this.format,
62
+ this.tagOf(args),
63
+ this.messageOf(args),
64
+ );
44
65
  }
45
66
 
46
67
  error(...args: string[]): void {
47
- const [tag, message] = this.normalizeArgs(args);
48
- hilog.error(this.domain, this.prefix, this.format, tag, message);
68
+ hilog.error(
69
+ this.domain,
70
+ this.prefix,
71
+ this.format,
72
+ this.tagOf(args),
73
+ this.messageOf(args),
74
+ );
49
75
  }
50
76
  }
51
77
 
@@ -5,6 +5,11 @@ import NativePatchCore from './NativePatchCore';
5
5
  import type { UpdateContext } from './UpdateContext';
6
6
  import { isSafePathComponent } from './PathUtils';
7
7
  import { monotonicNowMs } from './MonotonicClock';
8
+ import {
9
+ ERROR_DOWNLOAD_FAILED,
10
+ createUpdateError,
11
+ getErrorMessage,
12
+ } from './ErrorCodes';
8
13
 
9
14
  // 原生冷启动检测(NATIVE_CHECKUPDATE_DESIGN §10):每进程一次,getBundleUrl
10
15
  // 后延迟数秒运行,完全不依赖 app bundle——坏热更把 JS 砸挂后,下次启动仍能
@@ -69,10 +74,19 @@ interface DecisionAttempt {
69
74
  urls?: string[];
70
75
  }
71
76
 
77
+ interface DecisionInfoConfig {
78
+ forceBoot?: boolean;
79
+ }
80
+
72
81
  interface DecisionInfo {
73
82
  name?: string;
74
83
  description?: string;
75
84
  metaInfo?: string;
85
+ // 服务端按版本下发的配置(forceBoot = 救砖指令)。
86
+ config?: DecisionInfoConfig;
87
+ // 写进 hash 信息:这个版本是被救援通道送进来的,JS 在 markSuccess 时据此
88
+ // 上报 forceBootRescue 回执(与 Android/iOS 一致)。
89
+ forceBootRescue?: boolean;
76
90
  }
77
91
 
78
92
  interface Decision {
@@ -130,7 +144,7 @@ export function scheduleNativeCheck(
130
144
  }
131
145
  runOnce(context, launchRolledBackVersion).catch((e: Object) => {
132
146
  // 救援路径自身绝不能把应用拖垮。
133
- logger.error(TAG, `native check failed: ${e}`);
147
+ logger.error(TAG, `native check failed: ${getErrorMessage(e)}`);
134
148
  });
135
149
  }, delayMs);
136
150
  }
@@ -161,7 +175,16 @@ async function runOnce(
161
175
  return;
162
176
  }
163
177
  // 从这里起本轮开始做真实工作:留下面包屑,死于轮中时下次启动零延迟续传。
164
- context.setKv(KEY_ROUND_INCOMPLETE, '1');
178
+ logger.info(
179
+ TAG,
180
+ `round started (resume=${!!context.getKv(KEY_ROUND_INCOMPLETE)})`,
181
+ );
182
+ // 面包屑只是"下次零延迟"的优化:落盘失败不能阻止救援轮次本身。
183
+ try {
184
+ await context.setKv(KEY_ROUND_INCOMPLETE, '1');
185
+ } catch (e) {
186
+ logger.warn(TAG, `cannot persist round marker: ${getErrorMessage(e)}`);
187
+ }
165
188
  try {
166
189
  await runConfiguredRound(
167
190
  context,
@@ -172,10 +195,35 @@ async function runOnce(
172
195
  appKey,
173
196
  );
174
197
  } finally {
175
- context.removeKv(KEY_ROUND_INCOMPLETE);
198
+ try {
199
+ await context.removeKv(KEY_ROUND_INCOMPLETE);
200
+ } catch (e) {
201
+ // 残留标记只意味着下次启动少等 5 秒。
202
+ logger.warn(TAG, `cannot clear round marker: ${getErrorMessage(e)}`);
203
+ }
176
204
  }
177
205
  }
178
206
 
207
+ // 配置端点全为 https 时,网络下发的端点列表/制品 URL 一律拒绝明文 http:
208
+ // @ohos.net.http 内部跟随重定向且不暴露最终 URL,这是鸿蒙侧唯一能做的
209
+ // https→http 降级防线(SR-1);其余依赖宿主 network_config.json 的
210
+ // cleartextTrafficPermitted:false(见 harmony/pushy/src/README.md)。
211
+ function isHttpsOnly(config: NativeConfig): boolean {
212
+ const endpoints = config.endpoints ?? [];
213
+ return (
214
+ endpoints.length > 0 &&
215
+ endpoints.every(endpoint => isHttpsUrl(endpoint))
216
+ );
217
+ }
218
+
219
+ function isHttpsUrl(url: string): boolean {
220
+ return typeof url === 'string' && url.toLowerCase().startsWith('https://');
221
+ }
222
+
223
+ function isCleartextUrl(url: string): boolean {
224
+ return typeof url === 'string' && url.toLowerCase().startsWith('http://');
225
+ }
226
+
179
227
  async function runConfiguredRound(
180
228
  context: UpdateContext,
181
229
  launchRolledBackVersion: string,
@@ -222,9 +270,10 @@ async function runConfiguredRound(
222
270
  return;
223
271
  }
224
272
 
225
- const responseText = await runCheckRequest(config, appKey, body);
273
+ const httpsOnly = isHttpsOnly(config);
274
+ const responseText = await runCheckRequest(config, appKey, body, httpsOnly);
226
275
  if (!responseText) {
227
- logger.debug(TAG, 'no endpoint reachable, giving up until next launch');
276
+ logger.warn(TAG, 'no endpoint reachable, giving up until next launch');
228
277
  return;
229
278
  }
230
279
  // Anchor cache freshness to response arrival, before download/patch work.
@@ -240,32 +289,38 @@ async function runConfiguredRound(
240
289
  }
241
290
  const decision = JSON.parse(decisionJson) as Decision;
242
291
  if (decision.action !== 'download') {
243
- context.commitNativeCheckResult(
292
+ await context.commitNativeCheckResult(
244
293
  resetGeneration,
245
294
  '',
246
295
  '',
247
296
  false,
248
297
  buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
249
298
  );
250
- logger.debug(TAG, `nothing to do (${decision.reason ?? ''})`);
299
+ logger.info(TAG, `nothing to do (${decision.reason ?? ''})`);
251
300
  return;
252
301
  }
253
302
  const hash = decision.hash ?? '';
254
303
  if (!isSafePathComponent(hash)) {
304
+ logger.warn(TAG, 'decision carries an unsafe hash, ignoring');
255
305
  return;
256
306
  }
257
307
 
258
308
  let downloaded = context.hasDownloadedVersion(hash);
259
- if (!downloaded) {
309
+ if (downloaded) {
310
+ logger.info(TAG, `${hash} already installed, skipping download`);
311
+ } else {
312
+ logger.info(TAG, `downloading ${hash}`);
260
313
  downloaded = await performAttempts(
261
314
  context,
262
315
  decision.attempts ?? [],
263
316
  hash,
264
317
  currentVersion,
318
+ httpsOnly,
265
319
  );
266
320
  }
267
321
  if (!downloaded) {
268
- context.commitNativeCheckResult(
322
+ logger.warn(TAG, `all download attempts for ${hash} failed`);
323
+ await context.commitNativeCheckResult(
269
324
  resetGeneration,
270
325
  '',
271
326
  '',
@@ -289,6 +344,12 @@ async function runConfiguredRound(
289
344
  if (typeof info.metaInfo === 'string') {
290
345
  hashInfo.metaInfo = info.metaInfo;
291
346
  }
347
+ // forceBoot 激活就是救砖路径:记进持久化元信息,这个版本活到 markSuccess
348
+ // 时 JS 上报 forceBootRescue。只认服务端下发的指令——静默策略的激活是
349
+ // 普通投递(Android/iOS 同一规则)。
350
+ if (info.config && info.config.forceBoot) {
351
+ hashInfo.forceBootRescue = true;
352
+ }
292
353
  hashInfoJson = JSON.stringify(hashInfo);
293
354
  }
294
355
 
@@ -298,7 +359,7 @@ async function runConfiguredRound(
298
359
  const activate = decision.activate === true;
299
360
  let committed = false;
300
361
  try {
301
- committed = context.commitNativeCheckResult(
362
+ committed = await context.commitNativeCheckResult(
302
363
  resetGeneration,
303
364
  hash,
304
365
  hashInfoJson,
@@ -306,15 +367,15 @@ async function runConfiguredRound(
306
367
  buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
307
368
  );
308
369
  } catch (e) {
309
- logger.error(TAG, `commit failed: ${e}`);
370
+ logger.error(TAG, `commit failed: ${getErrorMessage(e)}`);
310
371
  return;
311
372
  }
312
373
  if (!committed) {
313
- logger.debug(TAG, 'reset during round, dropping result');
374
+ logger.warn(TAG, 'reset during round, dropping result');
314
375
  } else if (activate) {
315
- logger.debug(TAG, `downloaded ${hash} and set for next launch`);
376
+ logger.info(TAG, `downloaded ${hash} and set for next launch`);
316
377
  } else {
317
- logger.debug(TAG, `downloaded ${hash}, activation left to JS`);
378
+ logger.info(TAG, `downloaded ${hash}, activation left to JS`);
318
379
  }
319
380
  }
320
381
 
@@ -396,6 +457,7 @@ async function runCheckRequest(
396
457
  config: NativeConfig,
397
458
  appKey: string,
398
459
  body: string,
460
+ httpsOnly: boolean,
399
461
  ): Promise<string | undefined> {
400
462
  const orderedJson = NativePatchCore.orderEndpointCandidates(
401
463
  JSON.stringify(config.endpoints ?? []),
@@ -450,6 +512,11 @@ async function runCheckRequest(
450
512
  if (!base || tried.has(base)) {
451
513
  continue;
452
514
  }
515
+ if (httpsOnly && !isHttpsUrl(base)) {
516
+ // 远程注入的端点不得把 https 配置降级成明文。
517
+ logger.warn(TAG, `ignoring non-https remote endpoint ${base}`);
518
+ continue;
519
+ }
453
520
  if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
454
521
  return undefined;
455
522
  }
@@ -475,12 +542,20 @@ async function runWithinDeadline(
475
542
  ): Promise<void> {
476
543
  const remainingMs = deadlineUptimeMs - monotonicNowMs();
477
544
  if (remainingMs <= 0) {
478
- throw Error('Download phase deadline expired before start');
545
+ throw createUpdateError(
546
+ ERROR_DOWNLOAD_FAILED,
547
+ 'Download phase deadline expired before start',
548
+ );
479
549
  }
480
550
  let deadlineTimer = 0;
481
551
  const deadlinePromise = new Promise<void>((_, reject) => {
482
552
  deadlineTimer = setTimeout(() => {
483
- reject(Error('Download phase deadline exceeded'));
553
+ reject(
554
+ createUpdateError(
555
+ ERROR_DOWNLOAD_FAILED,
556
+ 'Download phase deadline exceeded',
557
+ ),
558
+ );
484
559
  }, remainingMs);
485
560
  });
486
561
  try {
@@ -498,6 +573,7 @@ async function performAttempts(
498
573
  attempts: DecisionAttempt[],
499
574
  hash: string,
500
575
  originHash: string,
576
+ httpsOnly: boolean,
501
577
  ): Promise<boolean> {
502
578
  const incrementalDeadline = monotonicNowMs() + DOWNLOAD_PHASE_TIMEOUT_MS;
503
579
  let fullDeadline = 0;
@@ -519,6 +595,11 @@ async function performAttempts(
519
595
  if (!url) {
520
596
  continue;
521
597
  }
598
+ if (httpsOnly && isCleartextUrl(url)) {
599
+ // 决策里的明文制品 URL 不得把 https 配置降级(SR-1)。
600
+ logger.warn(TAG, `ignoring cleartext ${type} url ${url}`);
601
+ continue;
602
+ }
522
603
  if (monotonicNowMs() >= deadline) {
523
604
  if (isFullAttempt) {
524
605
  return false;
@@ -544,7 +625,7 @@ async function performAttempts(
544
625
  }
545
626
  return true;
546
627
  } catch (e) {
547
- logger.debug(TAG, `${type} attempt failed: ${e}`);
628
+ logger.warn(TAG, `${type} attempt failed: ${getErrorMessage(e)}`);
548
629
  }
549
630
  }
550
631
  }
@@ -74,16 +74,23 @@ interface NativePatchCoreBindings {
74
74
  ): ArchivePatchPlanResult;
75
75
  buildCopyGroups(copyFroms: string[], copyTos: string[]): CopyGroupResult[];
76
76
  applyPatchFromFileSource(options: FileSourcePatchRequest): Promise<void>;
77
+ /**
78
+ * 删除 rootDir 下超过 maxAgeDays 且名字不在 keepNames 里的条目(native
79
+ * 工作线程)。keepNames 必须包含所有可能仍在使用的版本:持久化的
80
+ * current/last 以及本进程实际启动的版本。
81
+ */
77
82
  cleanupOldEntries(
78
83
  rootDir: string,
79
- keepCurrent: string,
80
- keepPrevious: string,
84
+ keepNames: string[],
81
85
  maxAgeDays: number,
82
86
  ): Promise<void>;
83
87
  /** sha256(小写 hex)。同步:输入是已在内存的 rawfile bundle,哈希毫秒级 */
84
88
  sha256Hex(data: Uint8Array): string;
85
- // 文件流式 SHA-256(小写 hex;不可读时空串),供安装记录使用。
89
+ // 文件流式 SHA-256(小写 hex;不可读时空串)。同步,仅限小文件——整包/
90
+ // 整 bundle 用下面的异步版本,避免冻结 UI 线程。
86
91
  sha256HexFile(path: string): string;
92
+ // 同上,在 libuv 工作线程计算;文件不可读时 reject。
93
+ sha256HexFileAsync(path: string): Promise<string>;
87
94
  /** CRC32(zip/zlib 多项式)。pdiff 拷贝前与 copiesCrc 比对用 */
88
95
  crc32(data: Uint8Array | ArrayBuffer): number;
89
96
  /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
@@ -1,6 +1,9 @@
1
+ import { ERROR_INVALID_OPTIONS, createUpdateError } from './ErrorCodes';
2
+
1
3
  // Server-controlled identifiers are used as children of the update root. Keep
2
- // validation in a dependency-free module so startup orchestration and storage
3
- // code can share it without creating an import cycle.
4
+ // validation in a dependency-light module (ErrorCodes.ts has no imports) so
5
+ // startup orchestration and storage code can share it without creating an
6
+ // import cycle.
4
7
  export function isSafePathComponent(name: string): boolean {
5
8
  return (
6
9
  typeof name === 'string' &&
@@ -15,7 +18,10 @@ export function isSafePathComponent(name: string): boolean {
15
18
 
16
19
  export function assertSafePathComponent(name: string): string {
17
20
  if (!isSafePathComponent(name)) {
18
- throw Error(`Invalid path component: ${name}`);
21
+ throw createUpdateError(
22
+ ERROR_INVALID_OPTIONS,
23
+ `Invalid path component: ${name}`,
24
+ );
19
25
  }
20
26
  return name;
21
27
  }