react-native-update 10.50.0 → 10.51.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/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/build.gradle +10 -0
- package/android/jni/Android.mk +5 -1
- 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/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +779 -61
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +341 -118
- package/src/endpoint.ts +7 -29
- package/src/provider.tsx +38 -2
- package/src/type.ts +26 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +1 -12
- package/src/isInRollout.ts +0 -87
- package/src/resolveCheckResult.ts +0 -30
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import preferences from '@ohos.data.preferences';
|
|
2
2
|
import fileIo from '@ohos.file.fs';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DownloadTask,
|
|
5
|
+
VERSION_COMPLETE_FILE_NAME,
|
|
6
|
+
} from './DownloadTask';
|
|
4
7
|
import common from '@ohos.app.ability.common';
|
|
5
8
|
import { DownloadTaskParams } from './DownloadTaskParams';
|
|
6
9
|
import { bundleManager } from '@kit.AbilityKit';
|
|
7
10
|
import { util } from '@kit.ArkTS';
|
|
8
11
|
import logger from './Logger';
|
|
12
|
+
import {
|
|
13
|
+
KEY_RESP_CACHE,
|
|
14
|
+
scheduleNativeCheck,
|
|
15
|
+
} from './NativeCheckOrchestrator';
|
|
9
16
|
import NativePatchCore, {
|
|
10
17
|
STATE_OP_CLEAR_FIRST_TIME,
|
|
11
18
|
STATE_OP_CLEAR_ROLLBACK_MARK,
|
|
@@ -15,32 +22,14 @@ import NativePatchCore, {
|
|
|
15
22
|
STATE_OP_SWITCH_VERSION,
|
|
16
23
|
StateCoreResult,
|
|
17
24
|
} from './NativePatchCore';
|
|
25
|
+
import { assertSafePathComponent } from './PathUtils';
|
|
26
|
+
|
|
27
|
+
export { isSafePathComponent } from './PathUtils';
|
|
18
28
|
|
|
19
29
|
type FlushablePreferences = preferences.Preferences & {
|
|
20
30
|
flushSync?: () => void;
|
|
21
31
|
};
|
|
22
32
|
|
|
23
|
-
// 服务端下发的 hash/originHash/fileName 会拼进 rootDir 作为子路径;凡是可能
|
|
24
|
-
// 逃出 rootDir 的值(路径分隔符、".."、".")必须在触碰文件系统前拒绝。
|
|
25
|
-
export function isSafePathComponent(name: string): boolean {
|
|
26
|
-
return (
|
|
27
|
-
typeof name === 'string' &&
|
|
28
|
-
name.length > 0 &&
|
|
29
|
-
name !== '.' &&
|
|
30
|
-
name !== '..' &&
|
|
31
|
-
!name.includes('/') &&
|
|
32
|
-
!name.includes('\\') &&
|
|
33
|
-
!name.includes('\0')
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function assertSafePathComponent(name: string): string {
|
|
38
|
-
if (!isSafePathComponent(name)) {
|
|
39
|
-
throw Error(`Invalid path component: ${name}`);
|
|
40
|
-
}
|
|
41
|
-
return name;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
export class UpdateContext {
|
|
45
34
|
private context: common.UIAbilityContext;
|
|
46
35
|
private rootDir: string;
|
|
@@ -52,6 +41,10 @@ export class UpdateContext {
|
|
|
52
41
|
// resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
|
|
53
42
|
// 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
|
|
54
43
|
private static launchVersion: string = '';
|
|
44
|
+
// 由 resetToPackagedBundle 递增。原生冷启动检测可能跑数分钟并已握有决策,
|
|
45
|
+
// 期间发生的 reset 必须赢:编排器采样该值,发现变化即放弃激活与响应缓存,
|
|
46
|
+
// 在飞的救援不会把刚被重置掉的版本装回去。
|
|
47
|
+
private static resetGeneration: number = 0;
|
|
55
48
|
private static cachedPackageVersion: string = '';
|
|
56
49
|
private static cachedBuildTime: string = '';
|
|
57
50
|
// 单例:确保 bundle provider 与 TurboModule 共用同一份 preferences 内存状态,
|
|
@@ -338,6 +331,15 @@ export class UpdateContext {
|
|
|
338
331
|
|
|
339
332
|
private async executeTask(params: DownloadTaskParams): Promise<void> {
|
|
340
333
|
await this.enqueueSerialTask(() => {
|
|
334
|
+
const isPatchTask =
|
|
335
|
+
params.type === DownloadTaskParams.TASK_TYPE_PATCH_FULL ||
|
|
336
|
+
params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP ||
|
|
337
|
+
params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
|
|
338
|
+
// Re-check only when this queued job actually starts. A JS download may
|
|
339
|
+
// have completed while the cold-start duplicate waited behind it.
|
|
340
|
+
if (isPatchTask && this.hasDownloadedVersion(params.hash)) {
|
|
341
|
+
return Promise.resolve();
|
|
342
|
+
}
|
|
341
343
|
const downloadTask = new DownloadTask(this.context);
|
|
342
344
|
return downloadTask.execute(params);
|
|
343
345
|
});
|
|
@@ -451,7 +453,16 @@ export class UpdateContext {
|
|
|
451
453
|
console.error('Failed to clear hash info on reset:', e);
|
|
452
454
|
}
|
|
453
455
|
}
|
|
456
|
+
// 先让在飞的原生检测轮次失效,再清理状态:随后在同一(单线程)执行序里
|
|
457
|
+
// 提交的轮次会看到新代数并整轮丢弃。
|
|
458
|
+
UpdateContext.resetGeneration += 1;
|
|
454
459
|
this.persistState(resetState, { clearFirstLoadMarker: true });
|
|
460
|
+
// 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
|
|
461
|
+
try {
|
|
462
|
+
this.preferences.deleteSync(KEY_RESP_CACHE);
|
|
463
|
+
} catch (e: any) {
|
|
464
|
+
console.error('Failed to clear native check cache on reset:', e);
|
|
465
|
+
}
|
|
455
466
|
UpdateContext.ignoreRollback = false;
|
|
456
467
|
|
|
457
468
|
// maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
|
|
@@ -469,7 +480,44 @@ export class UpdateContext {
|
|
|
469
480
|
this.trace('resetToPackagedBundle:after');
|
|
470
481
|
}
|
|
471
482
|
|
|
472
|
-
|
|
483
|
+
/** 供原生检测编排器采样/比对的 reset 代数(见 resetGeneration 注释)。 */
|
|
484
|
+
public getResetGeneration(): number {
|
|
485
|
+
return UpdateContext.resetGeneration;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 一次性提交原生检测轮次的全部持久化结果(版本元信息、激活、响应缓存):
|
|
490
|
+
* 先复核 reset 代数,再落所有写入。ArkTS 单线程 + 本方法内无 await,因此
|
|
491
|
+
* 校验与写入之间不存在可插入 reset 的窗口(iOS/Android 用锁达到同一效果)。
|
|
492
|
+
* 返回是否提交成功。
|
|
493
|
+
*/
|
|
494
|
+
public commitNativeCheckResult(
|
|
495
|
+
expectedGeneration: number,
|
|
496
|
+
hash: string,
|
|
497
|
+
hashInfoJson: string,
|
|
498
|
+
activate: boolean,
|
|
499
|
+
responseCacheJson: string,
|
|
500
|
+
): boolean {
|
|
501
|
+
if (UpdateContext.resetGeneration !== expectedGeneration) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (hash && hashInfoJson) {
|
|
505
|
+
this.setKv(`hash_${hash}`, hashInfoJson);
|
|
506
|
+
}
|
|
507
|
+
if (activate && hash) {
|
|
508
|
+
this.switchVersion(hash);
|
|
509
|
+
}
|
|
510
|
+
if (responseCacheJson) {
|
|
511
|
+
this.setKv(KEY_RESP_CACHE, responseCacheJson);
|
|
512
|
+
}
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public async downloadFullUpdate(
|
|
517
|
+
url: string,
|
|
518
|
+
hash: string,
|
|
519
|
+
deadlineUptimeMs: number = 0,
|
|
520
|
+
): Promise<void> {
|
|
473
521
|
try {
|
|
474
522
|
const params = this.createTaskParams(
|
|
475
523
|
DownloadTaskParams.TASK_TYPE_PATCH_FULL,
|
|
@@ -478,6 +526,7 @@ export class UpdateContext {
|
|
|
478
526
|
);
|
|
479
527
|
params.targetFile = `${this.rootDir}/${hash}.ppk`;
|
|
480
528
|
params.unzipDirectory = `${this.rootDir}/${hash}`;
|
|
529
|
+
params.deadlineUptimeMs = deadlineUptimeMs;
|
|
481
530
|
await this.executeTask(params);
|
|
482
531
|
} catch (e) {
|
|
483
532
|
console.error('Failed to download full update:', e);
|
|
@@ -503,6 +552,7 @@ export class UpdateContext {
|
|
|
503
552
|
url: string,
|
|
504
553
|
hash: string,
|
|
505
554
|
originHash: string,
|
|
555
|
+
deadlineUptimeMs: number = 0,
|
|
506
556
|
): Promise<void> {
|
|
507
557
|
const params = this.createTaskParams(
|
|
508
558
|
DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK,
|
|
@@ -513,12 +563,14 @@ export class UpdateContext {
|
|
|
513
563
|
params.targetFile = `${this.rootDir}/${originHash}_${hash}.ppk.patch`;
|
|
514
564
|
params.unzipDirectory = `${this.rootDir}/${hash}`;
|
|
515
565
|
params.originDirectory = `${this.rootDir}/${params.originHash}`;
|
|
566
|
+
params.deadlineUptimeMs = deadlineUptimeMs;
|
|
516
567
|
await this.executeTask(params);
|
|
517
568
|
}
|
|
518
569
|
|
|
519
570
|
public async downloadPatchFromPackage(
|
|
520
571
|
url: string,
|
|
521
572
|
hash: string,
|
|
573
|
+
deadlineUptimeMs: number = 0,
|
|
522
574
|
): Promise<void> {
|
|
523
575
|
try {
|
|
524
576
|
const params = this.createTaskParams(
|
|
@@ -528,6 +580,7 @@ export class UpdateContext {
|
|
|
528
580
|
);
|
|
529
581
|
params.targetFile = `${this.rootDir}/${hash}.app.patch`;
|
|
530
582
|
params.unzipDirectory = `${this.rootDir}/${hash}`;
|
|
583
|
+
params.deadlineUptimeMs = deadlineUptimeMs;
|
|
531
584
|
return await this.executeTask(params);
|
|
532
585
|
} catch (e) {
|
|
533
586
|
console.error('Failed to download package patch:', e);
|
|
@@ -535,6 +588,20 @@ export class UpdateContext {
|
|
|
535
588
|
}
|
|
536
589
|
}
|
|
537
590
|
|
|
591
|
+
// 原生冷启动检测(NativeCheckOrchestrator)用来跳过已就绪版本的重复下载
|
|
592
|
+
// ——alert 类策略下版本已下载但未激活,若不判在这里会每次冷启动重下一遍。
|
|
593
|
+
public hasDownloadedVersion(hash: string): boolean {
|
|
594
|
+
try {
|
|
595
|
+
const safeHash = assertSafePathComponent(hash);
|
|
596
|
+
return fileIo.accessSync(this.getBundlePath(safeHash))
|
|
597
|
+
&& fileIo.accessSync(
|
|
598
|
+
`${this.rootDir}/${safeHash}/${VERSION_COMPLETE_FILE_NAME}`,
|
|
599
|
+
);
|
|
600
|
+
} catch (e) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
538
605
|
public switchVersion(hash: string): void {
|
|
539
606
|
try {
|
|
540
607
|
const bundlePath = this.getBundlePath(assertSafePathComponent(hash));
|
|
@@ -565,58 +632,70 @@ export class UpdateContext {
|
|
|
565
632
|
public getBundleUrl() {
|
|
566
633
|
UpdateContext.isUsingBundleUrl = true;
|
|
567
634
|
this.trace('getBundleUrl:enter');
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
635
|
+
let nativeCheckRolledBackVersion = '';
|
|
636
|
+
try {
|
|
637
|
+
const stateBeforeLaunch = this.getStateSnapshot();
|
|
638
|
+
const launchState = NativePatchCore.runStateCore(
|
|
639
|
+
STATE_OP_RESOLVE_LAUNCH,
|
|
640
|
+
stateBeforeLaunch,
|
|
641
|
+
'',
|
|
642
|
+
UpdateContext.ignoreRollback,
|
|
643
|
+
true,
|
|
644
|
+
);
|
|
645
|
+
nativeCheckRolledBackVersion = launchState.rolledBackVersion || '';
|
|
646
|
+
if (launchState.didRollback) {
|
|
647
|
+
// The crash-protection rollback: the new version never called
|
|
648
|
+
// markSuccess. Keep this visible in release logs.
|
|
649
|
+
console.error(
|
|
650
|
+
`Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
|
|
651
|
+
` rolled back to ${launchState.currentVersion}`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
655
|
+
this.persistState(launchState, {
|
|
656
|
+
markFirstLoadMarker: launchState.consumedFirstTime,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
if (launchState.consumedFirstTime) {
|
|
660
|
+
UpdateContext.ignoreRollback = true;
|
|
661
|
+
}
|
|
662
|
+
this.trace(
|
|
663
|
+
`getBundleUrl:load=${launchState.loadVersion}` +
|
|
664
|
+
` consumed=${launchState.consumedFirstTime}` +
|
|
665
|
+
` rollback=${launchState.didRollback}`,
|
|
582
666
|
);
|
|
583
|
-
}
|
|
584
|
-
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
585
|
-
this.persistState(launchState, {
|
|
586
|
-
markFirstLoadMarker: launchState.consumedFirstTime,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
if (launchState.consumedFirstTime) {
|
|
590
|
-
UpdateContext.ignoreRollback = true;
|
|
591
|
-
}
|
|
592
|
-
this.trace(
|
|
593
|
-
`getBundleUrl:load=${launchState.loadVersion}` +
|
|
594
|
-
` consumed=${launchState.consumedFirstTime}` +
|
|
595
|
-
` rollback=${launchState.didRollback}`,
|
|
596
|
-
);
|
|
597
667
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
668
|
+
let version = launchState.loadVersion || '';
|
|
669
|
+
// Guard the rollback chain against cycles: a corrupted state returning an
|
|
670
|
+
// already-visited version would otherwise spin this loop forever during
|
|
671
|
+
// startup (Android has the same guard).
|
|
672
|
+
const visitedVersions = new Set<string>();
|
|
673
|
+
while (version && !visitedVersions.has(version)) {
|
|
674
|
+
visitedVersions.add(version);
|
|
675
|
+
const bundleFile = this.getBundlePath(version);
|
|
676
|
+
try {
|
|
677
|
+
if (!fileIo.accessSync(bundleFile)) {
|
|
678
|
+
console.error(`Bundle version ${version} not found.`);
|
|
679
|
+
version = this.rollBack();
|
|
680
|
+
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
UpdateContext.launchVersion = version;
|
|
684
|
+
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
685
|
+
return bundleFile;
|
|
686
|
+
} catch (e) {
|
|
687
|
+
console.error('Failed to access bundle file:', e);
|
|
609
688
|
version = this.rollBack();
|
|
610
|
-
|
|
689
|
+
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
611
690
|
}
|
|
612
|
-
UpdateContext.launchVersion = version;
|
|
613
|
-
return bundleFile;
|
|
614
|
-
} catch (e) {
|
|
615
|
-
console.error('Failed to access bundle file:', e);
|
|
616
|
-
version = this.rollBack();
|
|
617
691
|
}
|
|
692
|
+
nativeCheckRolledBackVersion = this.rolledBackVersion();
|
|
693
|
+
return '';
|
|
694
|
+
} finally {
|
|
695
|
+
// State corruption is exactly when the native rescue check is needed;
|
|
696
|
+
// schedule even if state parsing/rollback throws before a normal exit.
|
|
697
|
+
scheduleNativeCheck(this, nativeCheckRolledBackVersion);
|
|
618
698
|
}
|
|
619
|
-
return '';
|
|
620
699
|
}
|
|
621
700
|
|
|
622
701
|
public getCurrentVersion(): string {
|
package/harmony/pushy.har
CHANGED
|
Binary file
|