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.
- package/README-CN.md +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +399 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +43 -3
- package/src/client.ts +236 -82
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
|
@@ -25,23 +25,47 @@ import NativePatchCore, {
|
|
|
25
25
|
StateCoreResult,
|
|
26
26
|
} from './NativePatchCore';
|
|
27
27
|
import { assertSafePathComponent } from './PathUtils';
|
|
28
|
+
import {
|
|
29
|
+
ERROR_SWITCH_VERSION_FAILED,
|
|
30
|
+
createUpdateError,
|
|
31
|
+
getErrorMessage,
|
|
32
|
+
toUpdateError,
|
|
33
|
+
} from './ErrorCodes';
|
|
28
34
|
|
|
29
35
|
export { isSafePathComponent } from './PathUtils';
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
const TAG = 'UpdateContext';
|
|
38
|
+
// 常规清理保留最近 3 天内触碰过的条目(续传 partial、staging 同样按 mtime)。
|
|
39
|
+
const CLEANUP_MAX_AGE_DAYS = 3;
|
|
40
|
+
|
|
41
|
+
// Preferences.flushSync 是 API 14 才有、getAllSync 老 SDK 声明里没有:HAR 兼容
|
|
42
|
+
// API 12,按结构化视图探测(不用交叉类型——ArkTS 不支持)。
|
|
43
|
+
interface OptionalPreferencesApi {
|
|
32
44
|
flushSync?: () => void;
|
|
33
|
-
|
|
45
|
+
getAllSync?: () => Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface PersistOptions {
|
|
49
|
+
removeStaleHash?: boolean;
|
|
50
|
+
cleanUp?: boolean;
|
|
51
|
+
markFirstLoadMarker?: boolean;
|
|
52
|
+
clearFirstLoadMarker?: boolean;
|
|
53
|
+
}
|
|
34
54
|
|
|
35
55
|
export class UpdateContext {
|
|
36
56
|
private context: common.UIAbilityContext;
|
|
37
57
|
private rootDir: string;
|
|
38
58
|
private preferences!: preferences.Preferences;
|
|
59
|
+
// 宿主传入的调试标志(RNOH isDebugModeEnabled)。debug 下 markSuccess 空转、
|
|
60
|
+
// 不算内嵌 bundle 摘要,与 Android 的 debug 库对齐。HAR 自身没有构建期
|
|
61
|
+
// DEBUG 信号(hvigor 不为 HAR 生成 BuildProfile),所以只看宿主。
|
|
39
62
|
private static DEBUG: boolean = false;
|
|
40
63
|
private static isUsingBundleUrl: boolean = false;
|
|
41
64
|
private static ignoreRollback: boolean = false;
|
|
42
65
|
// 本进程实际加载的热更版本(getBundleUrl 解析成功时记录)。
|
|
43
66
|
// resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
|
|
44
67
|
// 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
|
|
68
|
+
// 常规清理同样必须保留它:一次进程内两次切换后 current/last 都不再是它。
|
|
45
69
|
private static launchVersion: string = '';
|
|
46
70
|
// 由 resetToPackagedBundle 递增。原生冷启动检测可能跑数分钟并已握有决策,
|
|
47
71
|
// 期间发生的 reset 必须赢:编排器采样该值,发现变化即放弃激活与响应缓存,
|
|
@@ -55,7 +79,18 @@ export class UpdateContext {
|
|
|
55
79
|
private static instanceCounter: number = 0;
|
|
56
80
|
private readonly instanceId: string;
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
/**
|
|
83
|
+
* @param isDebugHost 宿主的调试标志(RNOH isDebugModeEnabled)。bundle
|
|
84
|
+
* provider 先于 TurboModule 构造,不传该参数;TurboModule 随后显式传入时
|
|
85
|
+
* 会重新解析。
|
|
86
|
+
*/
|
|
87
|
+
public static getInstance(
|
|
88
|
+
context: common.UIAbilityContext,
|
|
89
|
+
isDebugHost?: boolean,
|
|
90
|
+
): UpdateContext {
|
|
91
|
+
if (isDebugHost !== undefined) {
|
|
92
|
+
UpdateContext.DEBUG = isDebugHost;
|
|
93
|
+
}
|
|
59
94
|
if (!UpdateContext.instance) {
|
|
60
95
|
UpdateContext.instance = new UpdateContext(context);
|
|
61
96
|
}
|
|
@@ -72,7 +107,7 @@ export class UpdateContext {
|
|
|
72
107
|
fileIo.mkdirSync(this.rootDir);
|
|
73
108
|
}
|
|
74
109
|
} catch (e) {
|
|
75
|
-
|
|
110
|
+
logger.error(TAG, `Failed to create root directory: ${getErrorMessage(e)}`);
|
|
76
111
|
}
|
|
77
112
|
this.initPreferences();
|
|
78
113
|
this.trace('ctor');
|
|
@@ -89,7 +124,7 @@ export class UpdateContext {
|
|
|
89
124
|
private trace(point: string): void {
|
|
90
125
|
const snap = this.getStateSnapshot();
|
|
91
126
|
logger.debug(
|
|
92
|
-
|
|
127
|
+
TAG,
|
|
93
128
|
`trace id=${this.instanceId} ${point}` +
|
|
94
129
|
` pkg=${snap.packageVersion} bt=${snap.buildTime}` +
|
|
95
130
|
` cv=${snap.currentVersion} lv=${snap.lastVersion}` +
|
|
@@ -115,11 +150,15 @@ export class UpdateContext {
|
|
|
115
150
|
// which disables rollback protection. Rethrow so the failure surfaces at
|
|
116
151
|
// construction time instead of later as an unrelated TypeError on the
|
|
117
152
|
// undefined `preferences` handle.
|
|
118
|
-
|
|
153
|
+
logger.error(TAG, `Failed to init preferences: ${getErrorMessage(e)}`);
|
|
119
154
|
throw e;
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
|
|
158
|
+
private optionalPreferencesApi(): OptionalPreferencesApi {
|
|
159
|
+
return this.preferences as unknown as OptionalPreferencesApi;
|
|
160
|
+
}
|
|
161
|
+
|
|
123
162
|
private getBundleFlags(): bundleManager.BundleFlag {
|
|
124
163
|
return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
|
|
125
164
|
}
|
|
@@ -135,7 +174,7 @@ export class UpdateContext {
|
|
|
135
174
|
UpdateContext.cachedPackageVersion = bundleInfo?.versionName || 'Unknown';
|
|
136
175
|
return UpdateContext.cachedPackageVersion;
|
|
137
176
|
} catch (error) {
|
|
138
|
-
|
|
177
|
+
logger.error(TAG, `Failed to get bundle info: ${getErrorMessage(error)}`);
|
|
139
178
|
return '';
|
|
140
179
|
}
|
|
141
180
|
}
|
|
@@ -155,7 +194,10 @@ export class UpdateContext {
|
|
|
155
194
|
return UpdateContext.cachedBuildTime;
|
|
156
195
|
}
|
|
157
196
|
} catch (error) {
|
|
158
|
-
|
|
197
|
+
logger.error(
|
|
198
|
+
TAG,
|
|
199
|
+
`Failed to read build time from raw file: ${getErrorMessage(error)}`,
|
|
200
|
+
);
|
|
159
201
|
}
|
|
160
202
|
return '';
|
|
161
203
|
}
|
|
@@ -213,33 +255,56 @@ export class UpdateContext {
|
|
|
213
255
|
this.flushBatchDepth += 1;
|
|
214
256
|
}
|
|
215
257
|
|
|
216
|
-
|
|
258
|
+
/** 关闭最外层批次时执行唯一一次落盘;返回该次落盘的 promise。 */
|
|
259
|
+
private endFlushBatch(reason: string): Promise<void> {
|
|
217
260
|
this.flushBatchDepth = Math.max(0, this.flushBatchDepth - 1);
|
|
218
261
|
if (this.flushBatchDepth === 0) {
|
|
219
|
-
this.flushPreferences(reason);
|
|
262
|
+
return this.flushPreferences(reason);
|
|
220
263
|
}
|
|
264
|
+
return Promise.resolve();
|
|
221
265
|
}
|
|
222
266
|
|
|
223
|
-
|
|
267
|
+
/**
|
|
268
|
+
* 落盘。flushSync(API 14)可用时同步完成;否则返回 preferences.flush() 的
|
|
269
|
+
* promise。有 promise 可拒绝的操作(切换、markSuccess、setKv…)必须 await
|
|
270
|
+
* 它——切换只有真正写进磁盘后才能向 JS 报成功,reloadUpdate 紧接着就会
|
|
271
|
+
* 杀进程(SR-5,Android 是 persistEditorOrThrow)。无法 await 的同步路径
|
|
272
|
+
* (启动解析、getConstants)用 flushInBackground,失败只记日志。
|
|
273
|
+
* 批次开着时不落盘,由 endFlushBatch 统一执行。
|
|
274
|
+
*/
|
|
275
|
+
private flushPreferences(reason: string): Promise<void> {
|
|
224
276
|
if (this.flushBatchDepth > 0) {
|
|
225
|
-
return;
|
|
277
|
+
return Promise.resolve();
|
|
226
278
|
}
|
|
227
|
-
const
|
|
228
|
-
if (typeof
|
|
279
|
+
const api = this.optionalPreferencesApi();
|
|
280
|
+
if (typeof api.flushSync === 'function') {
|
|
229
281
|
try {
|
|
230
|
-
|
|
231
|
-
return;
|
|
282
|
+
api.flushSync();
|
|
283
|
+
return Promise.resolve();
|
|
232
284
|
} catch (error) {
|
|
233
|
-
|
|
234
|
-
|
|
285
|
+
logger.error(
|
|
286
|
+
TAG,
|
|
287
|
+
`flushSync failed for ${reason}, falling back to flush(): ` +
|
|
288
|
+
getErrorMessage(error),
|
|
289
|
+
);
|
|
290
|
+
// fall through to the async flush rather than failing outright
|
|
235
291
|
}
|
|
236
292
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
return this.preferences.flush().then(
|
|
294
|
+
() => undefined,
|
|
295
|
+
(error: Object) => {
|
|
296
|
+
logger.error(
|
|
297
|
+
TAG,
|
|
298
|
+
`Failed to flush preferences for ${reason}: ${getErrorMessage(error)}`,
|
|
299
|
+
);
|
|
300
|
+
throw error;
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private flushInBackground(reason: string): void {
|
|
306
|
+
this.flushPreferences(reason).catch(() => {
|
|
307
|
+
// 已在 flushPreferences 里记录。
|
|
243
308
|
});
|
|
244
309
|
}
|
|
245
310
|
|
|
@@ -277,19 +342,8 @@ export class UpdateContext {
|
|
|
277
342
|
this.putNullableString('rolledBackVersion', state.rolledBackVersion);
|
|
278
343
|
}
|
|
279
344
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
options: {
|
|
283
|
-
clearExisting?: boolean;
|
|
284
|
-
removeStaleHash?: boolean;
|
|
285
|
-
cleanUp?: boolean;
|
|
286
|
-
markFirstLoadMarker?: boolean;
|
|
287
|
-
clearFirstLoadMarker?: boolean;
|
|
288
|
-
} = {},
|
|
289
|
-
): void {
|
|
290
|
-
if (options.clearExisting) {
|
|
291
|
-
this.preferences.clear();
|
|
292
|
-
}
|
|
345
|
+
/** 内存写入(不落盘):状态字段 + 可选的陈旧 hash / 首载标记 / 清理。 */
|
|
346
|
+
private writeState(state: StateCoreResult, options: PersistOptions): void {
|
|
293
347
|
this.applyState(state);
|
|
294
348
|
if (options.removeStaleHash && state.staleVersionToDelete) {
|
|
295
349
|
this.preferences.deleteSync(`hash_${state.staleVersionToDelete}`);
|
|
@@ -300,27 +354,31 @@ export class UpdateContext {
|
|
|
300
354
|
if (options.clearFirstLoadMarker) {
|
|
301
355
|
this.preferences.deleteSync('firstLoadMarked');
|
|
302
356
|
}
|
|
303
|
-
this.flushPreferences('persist state');
|
|
304
357
|
if (options.cleanUp) {
|
|
305
358
|
this.cleanUp();
|
|
306
359
|
}
|
|
307
360
|
}
|
|
308
361
|
|
|
362
|
+
private persistState(
|
|
363
|
+
state: StateCoreResult,
|
|
364
|
+
options: PersistOptions = {},
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
this.writeState(state, options);
|
|
367
|
+
return this.flushPreferences('persist state');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 跑一步状态机并写入内存;落盘由调用方决定(await 或后台)。 */
|
|
309
371
|
private runStateOperation(
|
|
310
372
|
operation: number,
|
|
311
373
|
stringArg: string = '',
|
|
312
|
-
options: {
|
|
313
|
-
removeStaleHash?: boolean;
|
|
314
|
-
cleanUp?: boolean;
|
|
315
|
-
clearFirstLoadMarker?: boolean;
|
|
316
|
-
} = {},
|
|
374
|
+
options: PersistOptions = {},
|
|
317
375
|
): StateCoreResult {
|
|
318
376
|
const nextState = NativePatchCore.runStateCore(
|
|
319
377
|
operation,
|
|
320
378
|
this.getStateSnapshot(),
|
|
321
379
|
stringArg,
|
|
322
380
|
);
|
|
323
|
-
this.
|
|
381
|
+
this.writeState(nextState, options);
|
|
324
382
|
return nextState;
|
|
325
383
|
}
|
|
326
384
|
|
|
@@ -336,10 +394,11 @@ export class UpdateContext {
|
|
|
336
394
|
return params;
|
|
337
395
|
}
|
|
338
396
|
|
|
339
|
-
// 串行化下载/补丁任务与破坏性清理(reset
|
|
340
|
-
// download executor 天然串行,Harmony 的 NAPI 任务跑在
|
|
341
|
-
//
|
|
342
|
-
//
|
|
397
|
+
// 串行化下载/补丁任务与破坏性清理(reset 的全量删除、按 mtime 的常规清理):
|
|
398
|
+
// Android 靠单线程 download executor 天然串行,Harmony 的 NAPI 任务跑在
|
|
399
|
+
// libuv worker 池上,若不排队,reset 的 RemovePathRecursively 可能与正在
|
|
400
|
+
// 写入的解压/打补丁并发,产出"bundle 在、资源半删"的目录且可能被后续
|
|
401
|
+
// switchVersion 激活。
|
|
343
402
|
private taskChain: Promise<void> = Promise.resolve();
|
|
344
403
|
|
|
345
404
|
private enqueueSerialTask<T>(job: () => Promise<T>): Promise<T> {
|
|
@@ -385,7 +444,7 @@ export class UpdateContext {
|
|
|
385
444
|
}
|
|
386
445
|
|
|
387
446
|
logger.info(
|
|
388
|
-
|
|
447
|
+
TAG,
|
|
389
448
|
`binary version changed, resetting update state id=${this.instanceId}`,
|
|
390
449
|
);
|
|
391
450
|
UpdateContext.ignoreRollback = false;
|
|
@@ -394,21 +453,23 @@ export class UpdateContext {
|
|
|
394
453
|
// rolledBackVersion)。不再 clearExisting,避免连带清除 uuid / firstLoadMarked /
|
|
395
454
|
// hash_* 等与 binary 版本无关的 KV —— 它们在多实例场景下本就脆弱,连带清除会
|
|
396
455
|
// 让 getConstants() 永远读到空,从而 isFirstTime=false、markSuccess 永不执行。
|
|
397
|
-
this.
|
|
456
|
+
this.writeState(nextState, {});
|
|
457
|
+
this.flushInBackground('sync binary version');
|
|
398
458
|
}
|
|
399
459
|
|
|
400
|
-
|
|
460
|
+
/** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */
|
|
461
|
+
public setKv(key: string, value: string): Promise<void> {
|
|
401
462
|
this.preferences.putSync(key, value);
|
|
402
|
-
this.flushPreferences(`set key ${key}`);
|
|
463
|
+
return this.flushPreferences(`set key ${key}`);
|
|
403
464
|
}
|
|
404
465
|
|
|
405
466
|
public getKv(key: string): string {
|
|
406
467
|
return this.readString(key);
|
|
407
468
|
}
|
|
408
469
|
|
|
409
|
-
public removeKv(key: string): void {
|
|
470
|
+
public removeKv(key: string): Promise<void> {
|
|
410
471
|
this.preferences.deleteSync(key);
|
|
411
|
-
this.flushPreferences(`remove key ${key}`);
|
|
472
|
+
return this.flushPreferences(`remove key ${key}`);
|
|
412
473
|
}
|
|
413
474
|
|
|
414
475
|
public isFirstTime(): boolean {
|
|
@@ -419,7 +480,7 @@ export class UpdateContext {
|
|
|
419
480
|
return this.getStateSnapshot().rolledBackVersion || '';
|
|
420
481
|
}
|
|
421
482
|
|
|
422
|
-
public markSuccess(): void {
|
|
483
|
+
public async markSuccess(): Promise<void> {
|
|
423
484
|
if (UpdateContext.DEBUG) {
|
|
424
485
|
return;
|
|
425
486
|
}
|
|
@@ -428,27 +489,32 @@ export class UpdateContext {
|
|
|
428
489
|
removeStaleHash: true,
|
|
429
490
|
cleanUp: true,
|
|
430
491
|
});
|
|
492
|
+
await this.flushPreferences('mark success');
|
|
431
493
|
}
|
|
432
494
|
|
|
433
|
-
public clearFirstTime(): void {
|
|
495
|
+
public async clearFirstTime(): Promise<void> {
|
|
434
496
|
this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', {
|
|
435
497
|
cleanUp: true,
|
|
436
498
|
clearFirstLoadMarker: true,
|
|
437
499
|
});
|
|
500
|
+
await this.flushPreferences('clear first time');
|
|
438
501
|
}
|
|
439
502
|
|
|
503
|
+
/** getConstants(同步)里调用:落盘失败只记日志。 */
|
|
440
504
|
public clearRollbackMark(): void {
|
|
441
505
|
this.runStateOperation(STATE_OP_CLEAR_ROLLBACK_MARK, '', {
|
|
442
506
|
cleanUp: true,
|
|
443
507
|
});
|
|
508
|
+
this.flushInBackground('clear rollback mark');
|
|
444
509
|
}
|
|
445
510
|
|
|
446
511
|
/**
|
|
447
512
|
* 恢复到二进制内置包:清空整个更新状态机(下次启动即回内置 bundle)并删除
|
|
448
513
|
* 已下载版本——仅保留当前运行版本的目录(静默 reset 不能破坏运行中 bundle
|
|
449
514
|
* 的按需资源加载)。uuid 保留 —— 它标识安装实例、用于灰度分桶,reset 不应改变。
|
|
515
|
+
* 状态与响应缓存一次落盘;落盘失败拒绝(JS 按 RESET_FAILED 处理)。
|
|
450
516
|
*/
|
|
451
|
-
public resetToPackagedBundle(): void {
|
|
517
|
+
public async resetToPackagedBundle(): Promise<void> {
|
|
452
518
|
this.trace('resetToPackagedBundle:before');
|
|
453
519
|
// 实时读取二进制身份(与 Android/iOS 对齐),而非 preferences 快照:
|
|
454
520
|
// meta.json 读取失败时快照可能为空,reset 持久化空值会让下次启动误判
|
|
@@ -462,47 +528,50 @@ export class UpdateContext {
|
|
|
462
528
|
firstTimeOk: true,
|
|
463
529
|
rolledBackVersion: '',
|
|
464
530
|
};
|
|
465
|
-
// 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
|
|
466
|
-
// uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
|
|
467
|
-
// 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
|
|
468
|
-
const prefsWithGetAll = this.preferences as preferences.Preferences & {
|
|
469
|
-
getAllSync?: () => Record<string, unknown>;
|
|
470
|
-
};
|
|
471
|
-
if (typeof prefsWithGetAll.getAllSync === 'function') {
|
|
472
|
-
try {
|
|
473
|
-
const all = prefsWithGetAll.getAllSync();
|
|
474
|
-
for (const key of Object.keys(all)) {
|
|
475
|
-
if (key.startsWith('hash_')) {
|
|
476
|
-
this.preferences.deleteSync(key);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
} catch (e: any) {
|
|
480
|
-
console.error('Failed to clear hash info on reset:', e);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
531
|
// 先让在飞的原生检测轮次失效,再清理状态:随后在同一(单线程)执行序里
|
|
484
532
|
// 提交的轮次会看到新代数并整轮丢弃。
|
|
485
533
|
UpdateContext.resetGeneration += 1;
|
|
486
|
-
this.
|
|
487
|
-
|
|
534
|
+
this.beginFlushBatch();
|
|
535
|
+
let flushed: Promise<void> = Promise.resolve();
|
|
488
536
|
try {
|
|
537
|
+
// 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
|
|
538
|
+
// uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
|
|
539
|
+
// 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
|
|
540
|
+
const api = this.optionalPreferencesApi();
|
|
541
|
+
if (typeof api.getAllSync === 'function') {
|
|
542
|
+
try {
|
|
543
|
+
const all = api.getAllSync();
|
|
544
|
+
for (const key of Object.keys(all)) {
|
|
545
|
+
if (key.startsWith('hash_')) {
|
|
546
|
+
this.preferences.deleteSync(key);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {
|
|
550
|
+
logger.error(
|
|
551
|
+
TAG,
|
|
552
|
+
`Failed to clear hash info on reset: ${getErrorMessage(e)}`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
this.writeState(resetState, { clearFirstLoadMarker: true });
|
|
557
|
+
// 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
|
|
489
558
|
this.preferences.deleteSync(KEY_RESP_CACHE);
|
|
490
|
-
}
|
|
491
|
-
|
|
559
|
+
} finally {
|
|
560
|
+
flushed = this.endFlushBatch('reset to packaged bundle');
|
|
492
561
|
}
|
|
493
562
|
UpdateContext.ignoreRollback = false;
|
|
563
|
+
await flushed;
|
|
494
564
|
|
|
495
565
|
// maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
|
|
496
566
|
// 下次常规清理回收)。挂到串行任务链尾,避免与在飞的解压/打补丁并发。
|
|
497
567
|
this.enqueueSerialTask(() =>
|
|
498
568
|
NativePatchCore.cleanupOldEntries(
|
|
499
569
|
this.rootDir,
|
|
500
|
-
UpdateContext.launchVersion,
|
|
501
|
-
'',
|
|
570
|
+
[UpdateContext.launchVersion].filter(name => name.length > 0),
|
|
502
571
|
0,
|
|
503
572
|
),
|
|
504
573
|
).catch((error: Object) => {
|
|
505
|
-
|
|
574
|
+
logger.error(TAG, `reset cleanup failed: ${getErrorMessage(error)}`);
|
|
506
575
|
});
|
|
507
576
|
this.trace('resetToPackagedBundle:after');
|
|
508
577
|
}
|
|
@@ -513,37 +582,48 @@ export class UpdateContext {
|
|
|
513
582
|
}
|
|
514
583
|
|
|
515
584
|
/**
|
|
516
|
-
* 一次性提交原生检测轮次的全部持久化结果(版本元信息、激活、响应缓存)
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
585
|
+
* 一次性提交原生检测轮次的全部持久化结果(版本元信息、激活、响应缓存)。
|
|
586
|
+
* 切换校验(含工作线程上的 bundle 摘要复核)放在第一个 putSync 之前:校验
|
|
587
|
+
* 失败时什么都不写,不会出现"元信息已落盘、切换没发生"的半提交。校验后
|
|
588
|
+
* 复核 reset 代数,之后到写入完成之间没有 await(ArkTS 单线程),因此不存在
|
|
589
|
+
* 可插入 reset 的窗口(iOS/Android 用锁达到同一效果)。三项写入以一次
|
|
590
|
+
* flush 落盘,失败拒绝。返回是否提交成功。
|
|
520
591
|
*/
|
|
521
|
-
public commitNativeCheckResult(
|
|
592
|
+
public async commitNativeCheckResult(
|
|
522
593
|
expectedGeneration: number,
|
|
523
594
|
hash: string,
|
|
524
595
|
hashInfoJson: string,
|
|
525
596
|
activate: boolean,
|
|
526
597
|
responseCacheJson: string,
|
|
527
|
-
): boolean {
|
|
598
|
+
): Promise<boolean> {
|
|
528
599
|
if (UpdateContext.resetGeneration !== expectedGeneration) {
|
|
529
600
|
return false;
|
|
530
601
|
}
|
|
602
|
+
const shouldSwitch = activate && !!hash;
|
|
603
|
+
if (shouldSwitch) {
|
|
604
|
+
await this.verifyActivation(hash);
|
|
605
|
+
if (UpdateContext.resetGeneration !== expectedGeneration) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
531
609
|
// One flush for the whole round (see beginFlushBatch): version info,
|
|
532
610
|
// activation and response cache land together or not at all.
|
|
533
611
|
this.beginFlushBatch();
|
|
612
|
+
let flushed: Promise<void> = Promise.resolve();
|
|
534
613
|
try {
|
|
535
614
|
if (hash && hashInfoJson) {
|
|
536
|
-
this.
|
|
615
|
+
this.preferences.putSync(`hash_${hash}`, hashInfoJson);
|
|
537
616
|
}
|
|
538
|
-
if (
|
|
539
|
-
this.
|
|
617
|
+
if (shouldSwitch) {
|
|
618
|
+
this.applySwitch(hash);
|
|
540
619
|
}
|
|
541
620
|
if (responseCacheJson) {
|
|
542
|
-
this.
|
|
621
|
+
this.preferences.putSync(KEY_RESP_CACHE, responseCacheJson);
|
|
543
622
|
}
|
|
544
623
|
} finally {
|
|
545
|
-
this.endFlushBatch('commit native check result');
|
|
624
|
+
flushed = this.endFlushBatch('commit native check result');
|
|
546
625
|
}
|
|
626
|
+
await flushed;
|
|
547
627
|
return true;
|
|
548
628
|
}
|
|
549
629
|
|
|
@@ -563,25 +643,11 @@ export class UpdateContext {
|
|
|
563
643
|
params.deadlineUptimeMs = deadlineUptimeMs;
|
|
564
644
|
await this.executeTask(params);
|
|
565
645
|
} catch (e) {
|
|
566
|
-
|
|
646
|
+
logger.error(TAG, `Failed to download full update: ${getErrorMessage(e)}`);
|
|
567
647
|
throw e;
|
|
568
648
|
}
|
|
569
649
|
}
|
|
570
650
|
|
|
571
|
-
public async downloadFile(
|
|
572
|
-
url: string,
|
|
573
|
-
hash: string,
|
|
574
|
-
fileName: string,
|
|
575
|
-
): Promise<void> {
|
|
576
|
-
const params = this.createTaskParams(
|
|
577
|
-
DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD,
|
|
578
|
-
url,
|
|
579
|
-
hash,
|
|
580
|
-
);
|
|
581
|
-
params.targetFile = this.rootDir + '/' + assertSafePathComponent(fileName);
|
|
582
|
-
await this.executeTask(params);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
651
|
public async downloadPatchFromPpk(
|
|
586
652
|
url: string,
|
|
587
653
|
hash: string,
|
|
@@ -617,13 +683,14 @@ export class UpdateContext {
|
|
|
617
683
|
params.deadlineUptimeMs = deadlineUptimeMs;
|
|
618
684
|
return await this.executeTask(params);
|
|
619
685
|
} catch (e) {
|
|
620
|
-
|
|
686
|
+
logger.error(
|
|
687
|
+
TAG,
|
|
688
|
+
`Failed to download package patch: ${getErrorMessage(e)}`,
|
|
689
|
+
);
|
|
621
690
|
throw e;
|
|
622
691
|
}
|
|
623
692
|
}
|
|
624
693
|
|
|
625
|
-
// 原生冷启动检测(NativeCheckOrchestrator)用来跳过已就绪版本的重复下载
|
|
626
|
-
// ——alert 类策略下版本已下载但未激活,若不判在这里会每次冷启动重下一遍。
|
|
627
694
|
// 运行中热更版本安装记录里的 bundleSha256(崩溃归因用);内置 bundle /
|
|
628
695
|
// 历史安装 / 记录不可读时为空串。
|
|
629
696
|
public currentBundleSha256(hash: string): string {
|
|
@@ -640,6 +707,8 @@ export class UpdateContext {
|
|
|
640
707
|
}
|
|
641
708
|
}
|
|
642
709
|
|
|
710
|
+
// 原生冷启动检测(NativeCheckOrchestrator)用来跳过已就绪版本的重复下载
|
|
711
|
+
// ——alert 类策略下版本已下载但未激活,若不判在这里会每次冷启动重下一遍。
|
|
643
712
|
public hasDownloadedVersion(hash: string): boolean {
|
|
644
713
|
try {
|
|
645
714
|
const safeHash = assertSafePathComponent(hash);
|
|
@@ -650,40 +719,64 @@ export class UpdateContext {
|
|
|
650
719
|
}
|
|
651
720
|
}
|
|
652
721
|
|
|
653
|
-
|
|
722
|
+
/**
|
|
723
|
+
* 激活前置校验(同步,不写任何东西):bundle 在盘上,且要么是本 SDK 记录为
|
|
724
|
+
* 完整安装的版本(bundle + 记录),要么是标记机制之前就激活过的历史版本
|
|
725
|
+
* (current/last 里有它)。其他无记录的目录可能是崩溃留下的半成品。
|
|
726
|
+
* 返回是否还需复核安装记录里的 bundle 摘要(历史版本无记录可核)。
|
|
727
|
+
*/
|
|
728
|
+
private assertActivatable(safeHash: string): boolean {
|
|
729
|
+
if (!fileIo.accessSync(this.getBundlePath(safeHash))) {
|
|
730
|
+
throw createUpdateError(
|
|
731
|
+
ERROR_SWITCH_VERSION_FAILED,
|
|
732
|
+
`Bundle version ${safeHash} not found.`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const legacyActivated =
|
|
736
|
+
this.readString('currentVersion') === safeHash ||
|
|
737
|
+
this.readString('lastVersion') === safeHash;
|
|
738
|
+
if (!this.hasDownloadedVersion(safeHash) && !legacyActivated) {
|
|
739
|
+
throw createUpdateError(
|
|
740
|
+
ERROR_SWITCH_VERSION_FAILED,
|
|
741
|
+
`Bundle version ${safeHash} is incomplete.`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return !legacyActivated;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* 完整的激活校验(含 native 工作线程上的 bundle 摘要复核),不写状态。
|
|
749
|
+
* 记录里的 bundle 摘要必须与盘上字节一致,才能把下次启动指向它。
|
|
750
|
+
*/
|
|
751
|
+
public async verifyActivation(hash: string): Promise<void> {
|
|
752
|
+
const safeHash = assertSafePathComponent(hash);
|
|
753
|
+
if (this.assertActivatable(safeHash)) {
|
|
754
|
+
await verifyInstallForActivation(
|
|
755
|
+
`${this.rootDir}/${safeHash}`,
|
|
756
|
+
safeHash,
|
|
757
|
+
this.getBundlePath(safeHash),
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** 内存里切换状态(不落盘);调用方负责 flush。 */
|
|
763
|
+
private applySwitch(hash: string): void {
|
|
764
|
+
this.trace(`switchVersion:before ${hash}`);
|
|
765
|
+
this.runStateOperation(STATE_OP_SWITCH_VERSION, hash);
|
|
766
|
+
UpdateContext.ignoreRollback = false;
|
|
767
|
+
this.trace(`switchVersion:after ${hash}`);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** 校验 + 切换 + 落盘;任一步失败拒绝(SWITCH_VERSION_FAILED)。 */
|
|
771
|
+
public async switchVersion(hash: string): Promise<void> {
|
|
654
772
|
try {
|
|
655
773
|
const safeHash = assertSafePathComponent(hash);
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
// Same rule as Android/iOS: only a version this SDK recorded as
|
|
661
|
-
// completely installed (bundle + marker) may be activated. Versions
|
|
662
|
-
// activated before markers existed are grandfathered through
|
|
663
|
-
// current/last state; any other markerless directory may be a
|
|
664
|
-
// crash-left partial install.
|
|
665
|
-
const legacyActivated =
|
|
666
|
-
this.readString('currentVersion') === safeHash ||
|
|
667
|
-
this.readString('lastVersion') === safeHash;
|
|
668
|
-
if (!this.hasDownloadedVersion(safeHash) && !legacyActivated) {
|
|
669
|
-
throw Error(`Bundle version ${hash} is incomplete.`);
|
|
670
|
-
}
|
|
671
|
-
if (!legacyActivated) {
|
|
672
|
-
// 记录里的 bundle 摘要必须与盘上字节一致,才能把下次启动指向它。
|
|
673
|
-
verifyInstallForActivation(
|
|
674
|
-
`${this.rootDir}/${safeHash}`,
|
|
675
|
-
safeHash,
|
|
676
|
-
bundlePath,
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
this.trace(`switchVersion:before ${hash}`);
|
|
681
|
-
this.runStateOperation(STATE_OP_SWITCH_VERSION, hash);
|
|
682
|
-
UpdateContext.ignoreRollback = false;
|
|
683
|
-
this.trace(`switchVersion:after ${hash}`);
|
|
774
|
+
await this.verifyActivation(safeHash);
|
|
775
|
+
this.applySwitch(safeHash);
|
|
776
|
+
await this.flushPreferences(`switch version ${safeHash}`);
|
|
684
777
|
} catch (e) {
|
|
685
|
-
|
|
686
|
-
throw e;
|
|
778
|
+
logger.error(TAG, `Failed to switch version: ${getErrorMessage(e)}`);
|
|
779
|
+
throw toUpdateError(e, ERROR_SWITCH_VERSION_FAILED);
|
|
687
780
|
}
|
|
688
781
|
}
|
|
689
782
|
|
|
@@ -692,7 +785,7 @@ export class UpdateContext {
|
|
|
692
785
|
this.trace(`consumeFirstLoadMarker:marked=${marked}`);
|
|
693
786
|
if (marked) {
|
|
694
787
|
this.preferences.deleteSync('firstLoadMarked');
|
|
695
|
-
this.
|
|
788
|
+
this.flushInBackground('clear first load marker');
|
|
696
789
|
}
|
|
697
790
|
return marked;
|
|
698
791
|
}
|
|
@@ -714,15 +807,17 @@ export class UpdateContext {
|
|
|
714
807
|
if (launchState.didRollback) {
|
|
715
808
|
// The crash-protection rollback: the new version never called
|
|
716
809
|
// markSuccess. Keep this visible in release logs.
|
|
717
|
-
|
|
810
|
+
logger.error(
|
|
811
|
+
TAG,
|
|
718
812
|
`Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
|
|
719
813
|
` rolled back to ${launchState.currentVersion}`,
|
|
720
814
|
);
|
|
721
815
|
}
|
|
722
816
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
817
|
+
// 同步启动路径:落盘失败只记日志(状态已在内存里生效)。
|
|
723
818
|
this.persistState(launchState, {
|
|
724
819
|
markFirstLoadMarker: launchState.consumedFirstTime,
|
|
725
|
-
});
|
|
820
|
+
}).catch(() => {});
|
|
726
821
|
}
|
|
727
822
|
if (launchState.consumedFirstTime) {
|
|
728
823
|
UpdateContext.ignoreRollback = true;
|
|
@@ -743,7 +838,7 @@ export class UpdateContext {
|
|
|
743
838
|
const bundleFile = this.getBundlePath(version);
|
|
744
839
|
try {
|
|
745
840
|
if (!fileIo.accessSync(bundleFile)) {
|
|
746
|
-
|
|
841
|
+
logger.error(TAG, `Bundle version ${version} not found.`);
|
|
747
842
|
version = this.rollBack();
|
|
748
843
|
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
749
844
|
continue;
|
|
@@ -752,7 +847,7 @@ export class UpdateContext {
|
|
|
752
847
|
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
753
848
|
return bundleFile;
|
|
754
849
|
} catch (e) {
|
|
755
|
-
|
|
850
|
+
logger.error(TAG, `Failed to access bundle file: ${getErrorMessage(e)}`);
|
|
756
851
|
version = this.rollBack();
|
|
757
852
|
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
758
853
|
}
|
|
@@ -775,25 +870,36 @@ export class UpdateContext {
|
|
|
775
870
|
private rollBack(): string {
|
|
776
871
|
const stateBefore = this.getStateSnapshot();
|
|
777
872
|
const nextState = this.runStateOperation(STATE_OP_ROLLBACK);
|
|
778
|
-
|
|
873
|
+
this.flushInBackground('rollback');
|
|
874
|
+
logger.error(
|
|
875
|
+
TAG,
|
|
779
876
|
`Rolling back version ${stateBefore.currentVersion} to ${nextState.currentVersion}`,
|
|
780
877
|
);
|
|
781
878
|
return nextState.currentVersion || '';
|
|
782
879
|
}
|
|
783
880
|
|
|
881
|
+
/**
|
|
882
|
+
* 常规清理(best-effort 后台维护,无人等待其完成):删除 3 天未触碰且不在
|
|
883
|
+
* 保留名单里的条目。保留 current/last 与本进程启动的版本。挂到串行任务链
|
|
884
|
+
* 上——下载可能正在追加超过 3 天的续传 partial 或填充 staging,按 mtime
|
|
885
|
+
* 的清理若并发跑会把它们当陈旧条目删掉(Android 与下载共用同一单线程
|
|
886
|
+
* executor)。
|
|
887
|
+
*/
|
|
784
888
|
public cleanUp(): void {
|
|
785
889
|
const state = this.getStateSnapshot();
|
|
786
|
-
|
|
787
|
-
// Cleanup is best-effort background maintenance and no caller depends on its
|
|
788
|
-
// completion, so fire-and-forget it off the UI thread and just log failures
|
|
789
|
-
// instead of blocking the state operation (or cold start) on disk I/O.
|
|
790
|
-
NativePatchCore.cleanupOldEntries(
|
|
791
|
-
this.rootDir,
|
|
890
|
+
const keepNames = [
|
|
792
891
|
state.currentVersion || '',
|
|
793
892
|
state.lastVersion || '',
|
|
794
|
-
|
|
893
|
+
UpdateContext.launchVersion,
|
|
894
|
+
].filter(name => name.length > 0);
|
|
895
|
+
this.enqueueSerialTask(() =>
|
|
896
|
+
NativePatchCore.cleanupOldEntries(
|
|
897
|
+
this.rootDir,
|
|
898
|
+
keepNames,
|
|
899
|
+
CLEANUP_MAX_AGE_DAYS,
|
|
900
|
+
),
|
|
795
901
|
).catch((error: Object) => {
|
|
796
|
-
|
|
902
|
+
logger.error(TAG, `cleanupOldEntries failed: ${getErrorMessage(error)}`);
|
|
797
903
|
});
|
|
798
904
|
}
|
|
799
905
|
|
|
@@ -812,7 +918,10 @@ export class UpdateContext {
|
|
|
812
918
|
);
|
|
813
919
|
return bundleInfo?.updateTime ?? 0;
|
|
814
920
|
} catch (error) {
|
|
815
|
-
|
|
921
|
+
logger.error(
|
|
922
|
+
TAG,
|
|
923
|
+
`Failed to get bundle update time: ${getErrorMessage(error)}`,
|
|
924
|
+
);
|
|
816
925
|
return 0;
|
|
817
926
|
}
|
|
818
927
|
}
|
|
@@ -842,7 +951,7 @@ export class UpdateContext {
|
|
|
842
951
|
hash = NativePatchCore.sha256Hex(content);
|
|
843
952
|
} catch (error) {
|
|
844
953
|
// rawfile 缺 bundle(未打包 release bundle)是合法的"未知",不是错误。
|
|
845
|
-
|
|
954
|
+
logger.info(TAG, `Cannot hash embedded bundle: ${getErrorMessage(error)}`);
|
|
846
955
|
return '';
|
|
847
956
|
}
|
|
848
957
|
if (hash) {
|
|
@@ -850,7 +959,8 @@ export class UpdateContext {
|
|
|
850
959
|
UpdateContext.KEY_BUNDLE_HASH_CACHE,
|
|
851
960
|
cachePrefix + hash,
|
|
852
961
|
);
|
|
853
|
-
|
|
962
|
+
// 缓存落盘失败不影响返回值(下次重算即可)。
|
|
963
|
+
this.flushInBackground('cache bundle hash');
|
|
854
964
|
}
|
|
855
965
|
return hash;
|
|
856
966
|
}
|