react-native-update 10.47.0 → 10.48.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/android/jni/Android.mk +2 -0
- package/android/jni/hpatch.c +55 -20
- package/android/jni/hpatch.h +0 -6
- 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 +29 -6
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +2 -0
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +12 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +47 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +12 -3
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +13 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +11 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/cpp/patch_core/error_codes.h +3 -0
- package/cpp/patch_core/hbc_transform.cpp +147 -0
- package/cpp/patch_core/hbc_transform.h +53 -0
- package/cpp/patch_core/hbc_transform_wire.cpp +327 -0
- package/cpp/patch_core/hbc_transform_wire.h +52 -0
- package/cpp/patch_core/patch_core.cpp +126 -4
- package/cpp/patch_core/patch_core.h +5 -0
- package/cpp/patch_core/patch_core_android.cpp +3 -1
- package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.meta.json +1 -0
- package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
- package/cpp/patch_core/tests/hbc_transform_test.cpp +384 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +150 -1
- package/cpp/patch_core/update_core_android.cpp +10 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +33 -39
- package/harmony/pushy/src/main/cpp/pushy.cpp +31 -6
- package/harmony/pushy/src/main/ets/DownloadTask.ts +34 -2
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -2
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +93 -1
- package/ios/RCTPushy/RCTPushyDownloader.mm +11 -2
- package/package.json +1 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +1 -0
- package/src/client.ts +167 -38
- package/src/context.ts +4 -0
- package/src/core.ts +4 -0
- package/src/error.ts +51 -5
- package/src/locales/en.ts +2 -0
- package/src/locales/zh.ts +2 -0
- package/src/provider.tsx +32 -15
- package/src/telemetry.ts +6 -1
- package/src/type.ts +3 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <vector>
|
|
7
7
|
|
|
8
8
|
#include "archive_patch_core.h"
|
|
9
|
+
#include "hbc_transform_wire.h"
|
|
9
10
|
#include "patch_core.h"
|
|
10
11
|
#include "state_core.h"
|
|
11
12
|
#include "state_ops.h"
|
|
@@ -69,7 +70,9 @@ std::string GetString(napi_env env, napi_value value, bool* ok) {
|
|
|
69
70
|
return std::string();
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
// length + 1 so NAPI's terminating '\0' lands inside owned storage instead
|
|
74
|
+
// of the string's past-the-end terminator slot (formally UB to write).
|
|
75
|
+
std::string result(length + 1, '\0');
|
|
73
76
|
size_t written = 0;
|
|
74
77
|
if (napi_get_value_string_utf8(
|
|
75
78
|
env, value, result.data(), length + 1, &written) != napi_ok) {
|
|
@@ -686,6 +689,7 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
686
689
|
std::string bundle_patch_path;
|
|
687
690
|
std::string bundle_output_path;
|
|
688
691
|
std::string merge_source_subdir;
|
|
692
|
+
std::string bundle_hbc_transform_meta;
|
|
689
693
|
bool enable_merge = true;
|
|
690
694
|
|
|
691
695
|
if (!GetOptionalStringArrayProperty(env, args[0], "copyFroms", ©_froms) ||
|
|
@@ -697,6 +701,8 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
697
701
|
!GetOptionalStringProperty(env, args[0], "bundlePatchPath", &bundle_patch_path) ||
|
|
698
702
|
!GetOptionalStringProperty(env, args[0], "bundleOutputPath", &bundle_output_path) ||
|
|
699
703
|
!GetOptionalStringProperty(env, args[0], "mergeSourceSubdir", &merge_source_subdir) ||
|
|
704
|
+
!GetOptionalStringProperty(
|
|
705
|
+
env, args[0], "bundleHbcTransformMeta", &bundle_hbc_transform_meta) ||
|
|
700
706
|
!GetOptionalBoolProperty(env, args[0], "enableMerge", true, &enable_merge)) {
|
|
701
707
|
return nullptr;
|
|
702
708
|
}
|
|
@@ -713,6 +719,7 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
713
719
|
work_data->options.bundle_patch_path = bundle_patch_path;
|
|
714
720
|
work_data->options.bundle_output_path = bundle_output_path;
|
|
715
721
|
work_data->options.merge_source_subdir = merge_source_subdir;
|
|
722
|
+
work_data->options.bundle_hbc_transform_meta = bundle_hbc_transform_meta;
|
|
716
723
|
work_data->options.enable_merge = enable_merge;
|
|
717
724
|
|
|
718
725
|
napi_value promise = nullptr;
|
|
@@ -733,9 +740,13 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
733
740
|
auto* w = static_cast<ApplyPatchWork*>(data);
|
|
734
741
|
w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
|
|
735
742
|
},
|
|
736
|
-
[](napi_env cb_env, napi_status, void* data) {
|
|
743
|
+
[](napi_env cb_env, napi_status status, void* data) {
|
|
737
744
|
auto* w = static_cast<ApplyPatchWork*>(data);
|
|
738
|
-
if (
|
|
745
|
+
if (status != napi_ok) {
|
|
746
|
+
// Cancelled/aborted before execute ran: w->status is
|
|
747
|
+
// meaningless; still settle the promise so it never hangs.
|
|
748
|
+
RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
|
|
749
|
+
} else if (w->status.ok) {
|
|
739
750
|
napi_value undefined_value = nullptr;
|
|
740
751
|
napi_get_undefined(cb_env, &undefined_value);
|
|
741
752
|
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
|
|
@@ -821,9 +832,11 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
|
821
832
|
w->status = pushy::patch::CleanupOldEntries(
|
|
822
833
|
w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
|
|
823
834
|
},
|
|
824
|
-
[](napi_env cb_env, napi_status, void* data) {
|
|
835
|
+
[](napi_env cb_env, napi_status status, void* data) {
|
|
825
836
|
auto* w = static_cast<CleanupWork*>(data);
|
|
826
|
-
if (
|
|
837
|
+
if (status != napi_ok) {
|
|
838
|
+
RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
|
|
839
|
+
} else if (w->status.ok) {
|
|
827
840
|
napi_value undefined_value = nullptr;
|
|
828
841
|
napi_get_undefined(cb_env, &undefined_value);
|
|
829
842
|
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
|
|
@@ -876,13 +889,25 @@ bool ExportFunction(
|
|
|
876
889
|
|
|
877
890
|
} // namespace
|
|
878
891
|
|
|
892
|
+
|
|
893
|
+
// 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道)
|
|
894
|
+
static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
|
|
895
|
+
napi_value result = nullptr;
|
|
896
|
+
napi_create_uint32(
|
|
897
|
+
env,
|
|
898
|
+
static_cast<uint32_t>(pushy::hbc::kSupportedDiffVersion),
|
|
899
|
+
&result);
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
902
|
+
|
|
879
903
|
napi_value Init(napi_env env, napi_value exports) {
|
|
880
904
|
if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
881
905
|
!ExportFunction(env, exports, "runStateCore", RunStateCore) ||
|
|
882
906
|
!ExportFunction(env, exports, "buildArchivePatchPlan", BuildArchivePatchPlan) ||
|
|
883
907
|
!ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
|
|
884
908
|
!ExportFunction(env, exports, "applyPatchFromFileSource", ApplyPatchFromFileSource) ||
|
|
885
|
-
!ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries)
|
|
909
|
+
!ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
|
|
910
|
+
!ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion)) {
|
|
886
911
|
return nullptr;
|
|
887
912
|
}
|
|
888
913
|
return exports;
|
|
@@ -16,6 +16,9 @@ export interface PatchManifestArrays {
|
|
|
16
16
|
copyFroms: string[];
|
|
17
17
|
copyTos: string[];
|
|
18
18
|
deletes: string[];
|
|
19
|
+
// __diff.json 中对应 bundle patch 条目的 hbcTransform 元数据(原始 JSON
|
|
20
|
+
// 字符串);为空时 native 走现状路径
|
|
21
|
+
hbcTransformMeta: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function parseManifestToArrays(
|
|
@@ -44,10 +47,23 @@ export function parseManifestToArrays(
|
|
|
44
47
|
copyTos.push(to);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
const hbcTransform = manifest.hbcTransform as
|
|
51
|
+
| Record<string, object>
|
|
52
|
+
| undefined;
|
|
53
|
+
const hbcTransformEntry =
|
|
54
|
+
hbcTransform && typeof hbcTransform === 'object'
|
|
55
|
+
? hbcTransform[HARMONY_BUNDLE_PATCH_ENTRY]
|
|
56
|
+
: undefined;
|
|
57
|
+
const hbcTransformMeta =
|
|
58
|
+
hbcTransformEntry && typeof hbcTransformEntry === 'object'
|
|
59
|
+
? JSON.stringify(hbcTransformEntry)
|
|
60
|
+
: '';
|
|
61
|
+
|
|
47
62
|
return {
|
|
48
63
|
copyFroms,
|
|
49
64
|
copyTos,
|
|
50
65
|
deletes,
|
|
66
|
+
hbcTransformMeta,
|
|
51
67
|
};
|
|
52
68
|
}
|
|
53
69
|
|
|
@@ -214,6 +230,7 @@ export class DownloadTask {
|
|
|
214
230
|
copyFroms: [],
|
|
215
231
|
copyTos: [],
|
|
216
232
|
deletes: [],
|
|
233
|
+
hbcTransformMeta: '',
|
|
217
234
|
};
|
|
218
235
|
}
|
|
219
236
|
|
|
@@ -229,6 +246,7 @@ export class DownloadTask {
|
|
|
229
246
|
workingDirectory: string,
|
|
230
247
|
bundlePatchPath: string,
|
|
231
248
|
outputFile: string,
|
|
249
|
+
hbcTransformMeta = '',
|
|
232
250
|
): Promise<void> {
|
|
233
251
|
const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
|
|
234
252
|
try {
|
|
@@ -243,6 +261,7 @@ export class DownloadTask {
|
|
|
243
261
|
bundlePatchPath,
|
|
244
262
|
bundleOutputPath: outputFile,
|
|
245
263
|
enableMerge: false,
|
|
264
|
+
bundleHbcTransformMeta: hbcTransformMeta,
|
|
246
265
|
});
|
|
247
266
|
} catch (error: any) {
|
|
248
267
|
error.message = `Failed to process bundle patch: ${error.message}`;
|
|
@@ -376,8 +395,15 @@ export class DownloadTask {
|
|
|
376
395
|
resolve();
|
|
377
396
|
})
|
|
378
397
|
.catch(async error => {
|
|
379
|
-
|
|
398
|
+
// reject 必须先于 closeWriter:此时 watchdog 已清除,若 close
|
|
399
|
+
// 也失败(同一磁盘故障的常见连锁)而 reject 未执行,下载 Promise
|
|
400
|
+
// 将永久挂起——正是 HM-2 要消灭的症状。
|
|
380
401
|
reject(error);
|
|
402
|
+
try {
|
|
403
|
+
await closeWriter();
|
|
404
|
+
} catch (closeErr: any) {
|
|
405
|
+
console.error('closeWriter failed after write error:', closeErr);
|
|
406
|
+
}
|
|
381
407
|
});
|
|
382
408
|
});
|
|
383
409
|
});
|
|
@@ -442,7 +468,6 @@ export class DownloadTask {
|
|
|
442
468
|
},
|
|
443
469
|
);
|
|
444
470
|
|
|
445
|
-
refreshWatchdog();
|
|
446
471
|
const responseCode = await httpRequest.requestInStream(params.url, {
|
|
447
472
|
method: http.RequestMethod.GET,
|
|
448
473
|
readTimeout: 60000,
|
|
@@ -455,6 +480,11 @@ export class DownloadTask {
|
|
|
455
480
|
throw Error(`Server error: ${responseCode}`);
|
|
456
481
|
}
|
|
457
482
|
|
|
483
|
+
// watchdog 到这里才首次启动:requestInStream 阶段已有 connect/readTimeout
|
|
484
|
+
// 覆盖;若提早启动,连接超过 60s 时 inactivityPromise 会在 race 尚无订阅者
|
|
485
|
+
// 时 reject(unhandled rejection),且 promise 一经 reject 无法复活——
|
|
486
|
+
// 即使随后数据正常流入,race 也必然以 "Download stalled" 失败。
|
|
487
|
+
refreshWatchdog();
|
|
458
488
|
await Promise.race([dataEndPromise, inactivityPromise]);
|
|
459
489
|
const stats = await fileIo.stat(params.targetFile);
|
|
460
490
|
const fileSize = stats.size;
|
|
@@ -534,6 +564,7 @@ export class DownloadTask {
|
|
|
534
564
|
params.unzipDirectory,
|
|
535
565
|
bundlePatchPath,
|
|
536
566
|
`${params.unzipDirectory}/bundle.harmony.js`,
|
|
567
|
+
manifestArrays.hbcTransformMeta,
|
|
537
568
|
);
|
|
538
569
|
await this.copyFromResource(
|
|
539
570
|
NativePatchCore.buildCopyGroups(
|
|
@@ -580,6 +611,7 @@ export class DownloadTask {
|
|
|
580
611
|
bundleOutputPath: `${params.unzipDirectory}/bundle.harmony.js`,
|
|
581
612
|
mergeSourceSubdir: plan.mergeSourceSubdir,
|
|
582
613
|
enableMerge: plan.enableMerge,
|
|
614
|
+
bundleHbcTransformMeta: manifestArrays.hbcTransformMeta,
|
|
583
615
|
});
|
|
584
616
|
console.info('Patch from PPK completed');
|
|
585
617
|
try {
|
|
@@ -47,6 +47,8 @@ export interface FileSourcePatchRequest {
|
|
|
47
47
|
bundleOutputPath: string;
|
|
48
48
|
mergeSourceSubdir?: string;
|
|
49
49
|
enableMerge?: boolean;
|
|
50
|
+
/** __diff.json 中该 bundle patch 条目的 hbcTransform 元数据(原始 JSON) */
|
|
51
|
+
bundleHbcTransformMeta?: string;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
interface NativePatchCoreBindings {
|
|
@@ -78,6 +80,8 @@ interface NativePatchCoreBindings {
|
|
|
78
80
|
keepPrevious: string,
|
|
79
81
|
maxAgeDays: number,
|
|
80
82
|
): Promise<void>;
|
|
83
|
+
/** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
|
|
84
|
+
getSupportedDiffVersion(): number;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
export default NativeUpdateCore as unknown as NativePatchCoreBindings;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import common from '@ohos.app.ability.common';
|
|
6
6
|
import { bundleManager } from '@kit.AbilityKit';
|
|
7
7
|
import logger from './Logger';
|
|
8
|
+
import NativePatchCore from './NativePatchCore';
|
|
8
9
|
import { UpdateContext } from './UpdateContext';
|
|
9
10
|
import { EventHub } from './EventHub';
|
|
10
11
|
|
|
@@ -150,6 +151,7 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
150
151
|
isFirstTime,
|
|
151
152
|
rolledBackVersion,
|
|
152
153
|
uuid,
|
|
154
|
+
supportedDiffVersion: NativePatchCore.getSupportedDiffVersion(),
|
|
153
155
|
};
|
|
154
156
|
const logResult = {
|
|
155
157
|
downloadRootDir: result.downloadRootDir,
|
|
@@ -231,6 +233,16 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
async resetToPackagedBundle(): Promise<void> {
|
|
237
|
+
logger.debug(TAG, ',call resetToPackagedBundle');
|
|
238
|
+
try {
|
|
239
|
+
this.context.resetToPackagedBundle();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error(TAG, `resetToPackagedBundle failed: ${getErrorMessage(error)}`);
|
|
242
|
+
throw Error(`resetToPackagedBundle failed: ${getErrorMessage(error)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
234
246
|
async downloadPatchFromPpk(options: {
|
|
235
247
|
updateUrl: string;
|
|
236
248
|
hash: string;
|
|
@@ -27,6 +27,10 @@ export class UpdateContext {
|
|
|
27
27
|
private static DEBUG: boolean = false;
|
|
28
28
|
private static isUsingBundleUrl: boolean = false;
|
|
29
29
|
private static ignoreRollback: boolean = false;
|
|
30
|
+
// 本进程实际加载的热更版本(getBundleUrl 解析成功时记录)。
|
|
31
|
+
// resetToPackagedBundle 不能删它的目录:热更包内的图片等资源是运行时按需
|
|
32
|
+
// 读盘的,静默(不重启)reset 若删掉会导致后续所有未加载过的资源失败。
|
|
33
|
+
private static launchVersion: string = '';
|
|
30
34
|
private static cachedPackageVersion: string = '';
|
|
31
35
|
private static cachedBuildTime: string = '';
|
|
32
36
|
// 单例:确保 bundle provider 与 TurboModule 共用同一份 preferences 内存状态,
|
|
@@ -296,9 +300,26 @@ export class UpdateContext {
|
|
|
296
300
|
return params;
|
|
297
301
|
}
|
|
298
302
|
|
|
303
|
+
// 串行化下载/补丁任务与破坏性清理(reset 的全量删除):Android 靠单线程
|
|
304
|
+
// download executor 天然串行,Harmony 的 NAPI 任务跑在 libuv worker 池上,
|
|
305
|
+
// 若不排队,reset 的 RemovePathRecursively 可能与正在写入的解压/打补丁并发,
|
|
306
|
+
// 产出"bundle 在、资源半删"的目录且可能被后续 switchVersion 激活。
|
|
307
|
+
private taskChain: Promise<void> = Promise.resolve();
|
|
308
|
+
|
|
309
|
+
private enqueueSerialTask<T>(job: () => Promise<T>): Promise<T> {
|
|
310
|
+
const run = this.taskChain.then(job);
|
|
311
|
+
this.taskChain = run.then(
|
|
312
|
+
() => undefined,
|
|
313
|
+
() => undefined,
|
|
314
|
+
);
|
|
315
|
+
return run;
|
|
316
|
+
}
|
|
317
|
+
|
|
299
318
|
private async executeTask(params: DownloadTaskParams): Promise<void> {
|
|
300
|
-
|
|
301
|
-
|
|
319
|
+
await this.enqueueSerialTask(() => {
|
|
320
|
+
const downloadTask = new DownloadTask(this.context);
|
|
321
|
+
return downloadTask.execute(params);
|
|
322
|
+
});
|
|
302
323
|
}
|
|
303
324
|
|
|
304
325
|
public syncStateWithBinaryVersion(
|
|
@@ -372,6 +393,61 @@ export class UpdateContext {
|
|
|
372
393
|
});
|
|
373
394
|
}
|
|
374
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 恢复到二进制内置包:清空整个更新状态机(下次启动即回内置 bundle)并删除
|
|
398
|
+
* 已下载版本——仅保留当前运行版本的目录(静默 reset 不能破坏运行中 bundle
|
|
399
|
+
* 的按需资源加载)。uuid 保留 —— 它标识安装实例、用于灰度分桶,reset 不应改变。
|
|
400
|
+
*/
|
|
401
|
+
public resetToPackagedBundle(): void {
|
|
402
|
+
this.trace('resetToPackagedBundle:before');
|
|
403
|
+
// 实时读取二进制身份(与 Android/iOS 对齐),而非 preferences 快照:
|
|
404
|
+
// meta.json 读取失败时快照可能为空,reset 持久化空值会让下次启动误判
|
|
405
|
+
// binary 变更多做一轮 cleanUp+persist。
|
|
406
|
+
const resetState: StateCoreResult = {
|
|
407
|
+
packageVersion: this.getPackageVersion(),
|
|
408
|
+
buildTime: this.getBuildTime(),
|
|
409
|
+
currentVersion: '',
|
|
410
|
+
lastVersion: '',
|
|
411
|
+
firstTime: false,
|
|
412
|
+
firstTimeOk: true,
|
|
413
|
+
rolledBackVersion: '',
|
|
414
|
+
};
|
|
415
|
+
// 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
|
|
416
|
+
// uuid —— 见 syncStateWithBinaryVersion 的注释)。getAllSync 在旧 SDK 上
|
|
417
|
+
// 可能不存在,此时残留的 hash_* 只是无害孤儿数据,不影响 reset 语义。
|
|
418
|
+
const prefsWithGetAll = this.preferences as preferences.Preferences & {
|
|
419
|
+
getAllSync?: () => Record<string, unknown>;
|
|
420
|
+
};
|
|
421
|
+
if (typeof prefsWithGetAll.getAllSync === 'function') {
|
|
422
|
+
try {
|
|
423
|
+
const all = prefsWithGetAll.getAllSync();
|
|
424
|
+
for (const key of Object.keys(all)) {
|
|
425
|
+
if (key.startsWith('hash_')) {
|
|
426
|
+
this.preferences.deleteSync(key);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch (e: any) {
|
|
430
|
+
console.error('Failed to clear hash info on reset:', e);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.persistState(resetState, { clearFirstLoadMarker: true });
|
|
434
|
+
UpdateContext.ignoreRollback = false;
|
|
435
|
+
|
|
436
|
+
// maxAgeDays=0:删除下载目录内容,仅保留当前运行版本的目录(残留目录由
|
|
437
|
+
// 下次常规清理回收)。挂到串行任务链尾,避免与在飞的解压/打补丁并发。
|
|
438
|
+
this.enqueueSerialTask(() =>
|
|
439
|
+
NativePatchCore.cleanupOldEntries(
|
|
440
|
+
this.rootDir,
|
|
441
|
+
UpdateContext.launchVersion,
|
|
442
|
+
'',
|
|
443
|
+
0,
|
|
444
|
+
),
|
|
445
|
+
).catch((error: Object) => {
|
|
446
|
+
console.error('reset cleanup failed:', error);
|
|
447
|
+
});
|
|
448
|
+
this.trace('resetToPackagedBundle:after');
|
|
449
|
+
}
|
|
450
|
+
|
|
375
451
|
public async downloadFullUpdate(url: string, hash: string): Promise<void> {
|
|
376
452
|
try {
|
|
377
453
|
const params = this.createTaskParams(
|
|
@@ -512,6 +588,7 @@ export class UpdateContext {
|
|
|
512
588
|
version = this.rollBack();
|
|
513
589
|
continue;
|
|
514
590
|
}
|
|
591
|
+
UpdateContext.launchVersion = version;
|
|
515
592
|
return bundleFile;
|
|
516
593
|
} catch (e) {
|
|
517
594
|
console.error('Failed to access bundle file:', e);
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#import "RCTPushyDownloader.h"
|
|
3
3
|
#import "ZipArchive.h"
|
|
4
4
|
#include "../../cpp/patch_core/archive_patch_core.h"
|
|
5
|
+
#include "../../cpp/patch_core/hbc_transform_wire.h"
|
|
5
6
|
#include "../../cpp/patch_core/error_codes.h"
|
|
6
7
|
#include "../../cpp/patch_core/patch_core.h"
|
|
7
8
|
#include "../../cpp/patch_core/state_core.h"
|
|
@@ -63,6 +64,12 @@ typedef NS_ENUM(NSInteger, PushyType) {
|
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
static std::atomic<bool> ignoreRollback{false};
|
|
67
|
+
// The version whose bundle this process actually loaded (resolved in
|
|
68
|
+
// +bundleURL). resetToPackagedBundle must not delete its directory: update
|
|
69
|
+
// assets (images/fonts) are read from it on demand at runtime, so wiping it
|
|
70
|
+
// under a silent (no-restart) reset would break every image the running app
|
|
71
|
+
// has not loaded yet. Guarded by the state lock.
|
|
72
|
+
static NSString *pushyLaunchVersion = nil;
|
|
66
73
|
|
|
67
74
|
// Serializes every read-modify-write of the persisted update state. The state
|
|
68
75
|
// machine itself is a pure function (state_core), but callers run on different
|
|
@@ -304,6 +311,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
304
311
|
[visitedVersions addObject:loadVersion];
|
|
305
312
|
NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
306
313
|
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
314
|
+
pushyLaunchVersion = loadVersion;
|
|
307
315
|
resolvedURL = [NSURL fileURLWithPath:bundlePath];
|
|
308
316
|
return;
|
|
309
317
|
} else {
|
|
@@ -347,6 +355,9 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
347
355
|
ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
|
|
348
356
|
ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
|
|
349
357
|
ret[@"uuid"] = [defaults objectForKey:keyUuid];
|
|
358
|
+
// 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道),
|
|
359
|
+
// JS 随 checkUpdate 以 diffV 上报,服务端按能力门控下发
|
|
360
|
+
ret[@"supportedDiffVersion"] = @(pushy::hbc::kSupportedDiffVersion);
|
|
350
361
|
NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
|
|
351
362
|
NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
|
|
352
363
|
ret[@"currentVersion"] = currentVersion;
|
|
@@ -376,7 +387,16 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
376
387
|
{
|
|
377
388
|
self = [super init];
|
|
378
389
|
if (self) {
|
|
379
|
-
|
|
390
|
+
// One process-wide serial queue, not per-instance: a bridge reload can
|
|
391
|
+
// briefly keep two RCTPushy instances alive, and destructive file work
|
|
392
|
+
// (resetToPackagedBundle's full cleanup) must stay serialized with the
|
|
393
|
+
// other instance's unzip/patch jobs.
|
|
394
|
+
static dispatch_queue_t sharedFileQueue;
|
|
395
|
+
static dispatch_once_t onceToken;
|
|
396
|
+
dispatch_once(&onceToken, ^{
|
|
397
|
+
sharedFileQueue = dispatch_queue_create("cn.reactnative.pushy.file", DISPATCH_QUEUE_SERIAL);
|
|
398
|
+
});
|
|
399
|
+
_fileQueue = sharedFileQueue;
|
|
380
400
|
}
|
|
381
401
|
return self;
|
|
382
402
|
}
|
|
@@ -514,6 +534,56 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
514
534
|
#endif
|
|
515
535
|
}
|
|
516
536
|
|
|
537
|
+
RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
538
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
539
|
+
{
|
|
540
|
+
// Reset to the bundle packaged in the binary: wipe the whole update state
|
|
541
|
+
// (so the next launch resolves to the built-in bundle) and delete the
|
|
542
|
+
// downloaded versions, keeping only the directory of the version this
|
|
543
|
+
// process is running from (a silent reset must not break its on-demand
|
|
544
|
+
// asset loads). Only the client uuid survives — it identifies the install
|
|
545
|
+
// for gray release bucketing and must not change on reset.
|
|
546
|
+
__block NSString *keepVersion = nil;
|
|
547
|
+
PushyWithStateLock(^{
|
|
548
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
549
|
+
keepVersion = pushyLaunchVersion;
|
|
550
|
+
|
|
551
|
+
// A default-constructed State is exactly the reset state (no current /
|
|
552
|
+
// last version, first_time=false, first_time_ok=true); keep the binary
|
|
553
|
+
// identity so the next launch does not re-trigger the package-updated
|
|
554
|
+
// sync path.
|
|
555
|
+
pushy::state::State state;
|
|
556
|
+
state.package_version = PushyToStdString([RCTPushy packageVersion]);
|
|
557
|
+
state.build_time = PushyToStdString([RCTPushy buildTime]);
|
|
558
|
+
PushyApplyStateToDefaults(defaults, state);
|
|
559
|
+
|
|
560
|
+
for (NSString *key in [defaults dictionaryRepresentation].allKeys) {
|
|
561
|
+
if ([key hasPrefix:keyHashInfo]) {
|
|
562
|
+
[defaults removeObjectForKey:key];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
[defaults removeObjectForKey:keyFirstLoadMarked];
|
|
566
|
+
[defaults removeObjectForKey:KeyPackageUpdatedMarked];
|
|
567
|
+
ignoreRollback = false;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
dispatch_async(_fileQueue, ^{
|
|
571
|
+
// maxAgeDays=0: remove every downloaded entry except the running
|
|
572
|
+
// version's directory (cleaned up by the next regular cleanup).
|
|
573
|
+
pushy::patch::Status status = pushy::patch::CleanupOldEntries(
|
|
574
|
+
PushyToStdString([RCTPushy downloadDir]),
|
|
575
|
+
PushyToStdString(keepVersion),
|
|
576
|
+
"",
|
|
577
|
+
0
|
|
578
|
+
);
|
|
579
|
+
if (!status.ok) {
|
|
580
|
+
RCTLogWarn(@"Pushy reset cleanup error: %s", status.message.c_str());
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
resolve(@true);
|
|
585
|
+
}
|
|
586
|
+
|
|
517
587
|
|
|
518
588
|
|
|
519
589
|
#pragma mark - private
|
|
@@ -742,6 +812,21 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
742
812
|
return;
|
|
743
813
|
}
|
|
744
814
|
|
|
815
|
+
// __diff.json 的 hbcTransform 元数据(HBC 变换域 patch,hdiffv2 轨道):
|
|
816
|
+
// 存在时透传给 patch 内核执行 T(origin) → hpatch → T⁻¹;缺失走现状路径。
|
|
817
|
+
NSDictionary *hbcTransform = json[@"hbcTransform"];
|
|
818
|
+
if ([hbcTransform isKindOfClass:[NSDictionary class]]) {
|
|
819
|
+
NSDictionary *meta = hbcTransform[BUNDLE_PATCH_NAME];
|
|
820
|
+
if ([meta isKindOfClass:[NSDictionary class]]) {
|
|
821
|
+
NSError *metaError = nil;
|
|
822
|
+
NSData *metaData = [NSJSONSerialization dataWithJSONObject:meta options:0 error:&metaError];
|
|
823
|
+
if (metaData != nil && metaError == nil) {
|
|
824
|
+
NSString *metaString = [[NSString alloc] initWithData:metaData encoding:NSUTF8StringEncoding];
|
|
825
|
+
options.bundle_hbc_transform_meta = PushyToStdString(metaString);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
745
830
|
pushy::patch::Status status = pushy::patch::ApplyPatchFromFileSource(options);
|
|
746
831
|
if (!status.ok) {
|
|
747
832
|
callback(PushyNSErrorFromStatus(status));
|
|
@@ -819,6 +904,13 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
819
904
|
NSError *unzipError = error;
|
|
820
905
|
if (!succeeded && unzipError == nil) {
|
|
821
906
|
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
|
|
907
|
+
} else if (unzipError != nil && unzipError.userInfo[PushyErrorCodeKey] == nil) {
|
|
908
|
+
// SSZipArchive's own NSError (corrupt zip, bad magic, ...) has
|
|
909
|
+
// no stable code; without one, downloadUpdate's fallback would
|
|
910
|
+
// classify it as DOWNLOAD_FAILED even though the download
|
|
911
|
+
// succeeded — keep the classification deterministic.
|
|
912
|
+
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
913
|
+
unzipError.localizedDescription ?: @"unzip failed");
|
|
822
914
|
}
|
|
823
915
|
completionHandler(unzipError);
|
|
824
916
|
}];
|
|
@@ -45,9 +45,12 @@ completionHandler:(void (^)(NSString *path, NSError *error))completionHandler
|
|
|
45
45
|
|
|
46
46
|
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
47
47
|
// Avoid hanging forever on a stalled connection (default resource timeout
|
|
48
|
-
// is 7 days).
|
|
48
|
+
// is 7 days). The 30s idle timeout matches Android's readTimeout and is
|
|
49
|
+
// what actually catches a stalled transfer; the total-duration cap matches
|
|
50
|
+
// Android's 10min callTimeout — 300s made a 30MB full package on a slow
|
|
51
|
+
// (<100KB/s) network fail on iOS while succeeding on Android.
|
|
49
52
|
sessionConfig.timeoutIntervalForRequest = 30;
|
|
50
|
-
sessionConfig.timeoutIntervalForResource =
|
|
53
|
+
sessionConfig.timeoutIntervalForResource = 600;
|
|
51
54
|
self.session = [NSURLSession sessionWithConfiguration:sessionConfig
|
|
52
55
|
delegate:self
|
|
53
56
|
delegateQueue:nil];
|
|
@@ -89,6 +92,12 @@ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
|
|
|
89
92
|
// Normalize an unknown total (NSURLSessionTransferSizeUnknown == -1) to 0 so
|
|
90
93
|
// the JS side does not compute a negative/NaN percentage.
|
|
91
94
|
long long total = totalBytesExpectedToWrite > 0 ? totalBytesExpectedToWrite : 0;
|
|
95
|
+
if (totalBytesWritten > total) {
|
|
96
|
+
// Encoded responses (gzip): the expected total counts compressed bytes
|
|
97
|
+
// while written counts decompressed ones, so the percentage would run
|
|
98
|
+
// past 100%. Treat the total as unknown instead.
|
|
99
|
+
total = 0;
|
|
100
|
+
}
|
|
92
101
|
if (total > 0) {
|
|
93
102
|
int percentage = (int)((totalBytesWritten * 100.0 / total) + 0.5);
|
|
94
103
|
if (percentage <= self.lastReportedPercentage) {
|
package/package.json
CHANGED
|
@@ -111,6 +111,8 @@ Pod::Spec.new do |s|
|
|
|
111
111
|
s.subspec 'RCTPushy' do |ss|
|
|
112
112
|
ss.source_files = ['ios/RCTPushy/*.{h,m,mm}',
|
|
113
113
|
'cpp/patch_core/archive_patch_core.{h,cpp}',
|
|
114
|
+
'cpp/patch_core/hbc_transform.{h,cpp}',
|
|
115
|
+
'cpp/patch_core/hbc_transform_wire.{h,cpp}',
|
|
114
116
|
'cpp/patch_core/patch_core.{h,cpp}',
|
|
115
117
|
'cpp/patch_core/state_core.{h,cpp}',
|
|
116
118
|
'android/jni/hpatch.{h,c}',
|
package/src/NativePushy.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Spec extends TurboModule {
|
|
|
19
19
|
restartApp(): Promise<void>;
|
|
20
20
|
setNeedUpdate(options: { hash: string }): Promise<void>;
|
|
21
21
|
markSuccess(): Promise<void>;
|
|
22
|
+
resetToPackagedBundle(): Promise<void>;
|
|
22
23
|
downloadPatchFromPpk(options: {
|
|
23
24
|
updateUrl: string;
|
|
24
25
|
hash: string;
|