react-native-update 10.55.0 → 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 +406 -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 +244 -86
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
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
|
-
/**
|
|
104
|
-
export function
|
|
105
|
-
versionDir
|
|
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
|
-
|
|
108
|
-
): void {
|
|
109
|
-
const record = readInstallRecord(versionDir);
|
|
142
|
+
): string {
|
|
110
143
|
if (record === null) {
|
|
111
|
-
throw
|
|
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
|
|
156
|
+
throw createUpdateError(
|
|
157
|
+
ERROR_SWITCH_VERSION_FAILED,
|
|
158
|
+
`Bundle version ${versionHash} completion record mismatch.`,
|
|
159
|
+
);
|
|
121
160
|
}
|
|
122
|
-
|
|
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.
|
|
126
|
-
if (actual.toLowerCase() !==
|
|
127
|
-
throw
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
273
|
+
const httpsOnly = isHttpsOnly(config);
|
|
274
|
+
const responseText = await runCheckRequest(config, appKey, body, httpsOnly);
|
|
226
275
|
if (!responseText) {
|
|
227
|
-
logger.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
374
|
+
logger.warn(TAG, 'reset during round, dropping result');
|
|
314
375
|
} else if (activate) {
|
|
315
|
-
logger.
|
|
376
|
+
logger.info(TAG, `downloaded ${hash} and set for next launch`);
|
|
316
377
|
} else {
|
|
317
|
-
logger.
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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-
|
|
3
|
-
// code can share it without creating an
|
|
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
|
|
21
|
+
throw createUpdateError(
|
|
22
|
+
ERROR_INVALID_OPTIONS,
|
|
23
|
+
`Invalid path component: ${name}`,
|
|
24
|
+
);
|
|
19
25
|
}
|
|
20
26
|
return name;
|
|
21
27
|
}
|