react-native-update 10.43.3 → 10.45.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/android/jni/Android.mk +0 -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/BundledResourceCopier.java +24 -12
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +33 -9
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +8 -4
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
- package/cpp/patch_core/archive_patch_core.cpp +16 -0
- package/cpp/patch_core/archive_patch_core.h +5 -0
- package/cpp/patch_core/jni_util.h +56 -0
- package/cpp/patch_core/patch_core.cpp +16 -1
- package/cpp/patch_core/patch_core.h +6 -0
- package/cpp/patch_core/patch_core_android.cpp +4 -37
- package/cpp/patch_core/state_ops.h +24 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +109 -2
- package/cpp/patch_core/update_core_android.cpp +43 -69
- package/harmony/pushy/src/main/cpp/pushy.cpp +163 -128
- package/harmony/pushy/src/main/ets/DownloadTask.ts +72 -19
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -6
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +10 -10
- package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +142 -111
- package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
- package/package.json +7 -3
- package/src/client.ts +161 -101
- package/src/context.ts +21 -7
- package/src/core.ts +5 -1
- package/src/index.ts +7 -1
- package/src/provider.tsx +91 -56
- package/src/utils.ts +58 -18
- package/android/jni/DownloadTask.c +0 -56
- package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
- package/harmony/pushy/src/main/cpp/pushy.c +0 -117
- package/harmony/pushy/src/main/cpp/pushy.h +0 -8
- package/src/__tests__/setup.ts +0 -44
|
@@ -8,21 +8,14 @@
|
|
|
8
8
|
#include "archive_patch_core.h"
|
|
9
9
|
#include "patch_core.h"
|
|
10
10
|
#include "state_core.h"
|
|
11
|
+
#include "state_ops.h"
|
|
11
12
|
|
|
12
13
|
extern "C" {
|
|
13
|
-
#include "hpatch.h"
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
namespace {
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
kSwitchVersion = 1,
|
|
20
|
-
kMarkSuccess = 2,
|
|
21
|
-
kRollback = 3,
|
|
22
|
-
kClearFirstTime = 4,
|
|
23
|
-
kClearRollbackMark = 5,
|
|
24
|
-
kResolveLaunch = 6,
|
|
25
|
-
};
|
|
18
|
+
using pushy::state_ops::StateOperation;
|
|
26
19
|
|
|
27
20
|
constexpr const char* kDefaultBundlePatchEntryName = "index.bundlejs.patch";
|
|
28
21
|
|
|
@@ -413,96 +406,6 @@ pushy::patch::PatchManifest BuildManifest(
|
|
|
413
406
|
return manifest;
|
|
414
407
|
}
|
|
415
408
|
|
|
416
|
-
napi_value HdiffPatch(napi_env env, napi_callback_info info) {
|
|
417
|
-
napi_value args[2] = {nullptr, nullptr};
|
|
418
|
-
size_t argc = 2;
|
|
419
|
-
if (!GetArgCount(env, info, &argc, args) || argc < 2) {
|
|
420
|
-
ThrowError(env, "Wrong number of arguments");
|
|
421
|
-
return nullptr;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
bool is_typed_array = false;
|
|
425
|
-
if (napi_is_typedarray(env, args[0], &is_typed_array) != napi_ok || !is_typed_array) {
|
|
426
|
-
ThrowError(env, "First argument must be a TypedArray");
|
|
427
|
-
return nullptr;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
uint8_t* origin_ptr = nullptr;
|
|
431
|
-
size_t origin_length = 0;
|
|
432
|
-
if (napi_get_typedarray_info(
|
|
433
|
-
env,
|
|
434
|
-
args[0],
|
|
435
|
-
nullptr,
|
|
436
|
-
&origin_length,
|
|
437
|
-
reinterpret_cast<void**>(&origin_ptr),
|
|
438
|
-
nullptr,
|
|
439
|
-
nullptr) != napi_ok) {
|
|
440
|
-
ThrowError(env, "Failed to get origin buffer");
|
|
441
|
-
return nullptr;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (napi_is_typedarray(env, args[1], &is_typed_array) != napi_ok || !is_typed_array) {
|
|
445
|
-
ThrowError(env, "Second argument must be a TypedArray");
|
|
446
|
-
return nullptr;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
uint8_t* patch_ptr = nullptr;
|
|
450
|
-
size_t patch_length = 0;
|
|
451
|
-
if (napi_get_typedarray_info(
|
|
452
|
-
env,
|
|
453
|
-
args[1],
|
|
454
|
-
nullptr,
|
|
455
|
-
&patch_length,
|
|
456
|
-
reinterpret_cast<void**>(&patch_ptr),
|
|
457
|
-
nullptr,
|
|
458
|
-
nullptr) != napi_ok) {
|
|
459
|
-
ThrowError(env, "Failed to get patch buffer");
|
|
460
|
-
return nullptr;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
hpatch_singleCompressedDiffInfo patch_info;
|
|
464
|
-
if (!((origin_length == 0) || origin_ptr) || !patch_ptr || patch_length == 0) {
|
|
465
|
-
ThrowError(env, "Corrupt patch");
|
|
466
|
-
return nullptr;
|
|
467
|
-
}
|
|
468
|
-
if (kHPatch_ok != hpatch_getInfo_by_mem(&patch_info, patch_ptr, patch_length)) {
|
|
469
|
-
ThrowError(env, "Error info in hpatch");
|
|
470
|
-
return nullptr;
|
|
471
|
-
}
|
|
472
|
-
if (origin_length != patch_info.oldDataSize) {
|
|
473
|
-
ThrowError(env, "Error oldDataSize in hpatch");
|
|
474
|
-
return nullptr;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
size_t new_size = static_cast<size_t>(patch_info.newDataSize);
|
|
478
|
-
if (sizeof(size_t) != sizeof(hpatch_StreamPos_t) &&
|
|
479
|
-
new_size != patch_info.newDataSize) {
|
|
480
|
-
ThrowError(env, "Error newDataSize in hpatch");
|
|
481
|
-
return nullptr;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
void* output_data = nullptr;
|
|
485
|
-
napi_value result = nullptr;
|
|
486
|
-
if (napi_create_arraybuffer(env, new_size, &output_data, &result) != napi_ok) {
|
|
487
|
-
ThrowError(env, "Failed to create result buffer");
|
|
488
|
-
return nullptr;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (kHPatch_ok != hpatch_by_mem(
|
|
492
|
-
origin_ptr,
|
|
493
|
-
origin_length,
|
|
494
|
-
static_cast<uint8_t*>(output_data),
|
|
495
|
-
new_size,
|
|
496
|
-
patch_ptr,
|
|
497
|
-
patch_length,
|
|
498
|
-
&patch_info)) {
|
|
499
|
-
ThrowError(env, "hpatch");
|
|
500
|
-
return nullptr;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
return result;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
409
|
napi_value SyncStateWithBinaryVersion(napi_env env, napi_callback_info info) {
|
|
507
410
|
napi_value args[3] = {nullptr, nullptr, nullptr};
|
|
508
411
|
size_t argc = 3;
|
|
@@ -670,9 +573,14 @@ napi_value BuildArchivePatchPlan(napi_env env, napi_callback_info info) {
|
|
|
670
573
|
|
|
671
574
|
const pushy::patch::PatchManifest manifest =
|
|
672
575
|
BuildManifest(copy_froms, copy_tos, deletes);
|
|
576
|
+
pushy::archive_patch::ArchivePatchType archive_type;
|
|
577
|
+
if (!pushy::archive_patch::TryParseArchivePatchType(patch_type, &archive_type)) {
|
|
578
|
+
ThrowError(env, "Unknown archive patch type");
|
|
579
|
+
return nullptr;
|
|
580
|
+
}
|
|
673
581
|
pushy::archive_patch::ArchivePatchPlan plan;
|
|
674
582
|
const pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
|
|
675
|
-
|
|
583
|
+
archive_type,
|
|
676
584
|
manifest,
|
|
677
585
|
entry_names,
|
|
678
586
|
&plan,
|
|
@@ -717,6 +625,47 @@ napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
|
|
|
717
625
|
return NewCopyGroupArray(env, groups);
|
|
718
626
|
}
|
|
719
627
|
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Async work plumbing for the heavy patch operations.
|
|
630
|
+
//
|
|
631
|
+
// applyPatchFromFileSource and cleanupOldEntries run hdiff / recursive file IO
|
|
632
|
+
// that can take hundreds of ms to seconds. The Pushy TurboModule executes on
|
|
633
|
+
// the UI thread, so running these synchronously froze the UI. These are now
|
|
634
|
+
// wrapped in napi_create_async_work: arguments are parsed on the JS thread, the
|
|
635
|
+
// heavy work runs on a libuv worker thread, and the returned Promise is settled
|
|
636
|
+
// back on the JS thread.
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
// Reject an already-created deferred with an Error(message). Used when async
|
|
640
|
+
// work fails to be created/queued, so the Promise never hangs pending.
|
|
641
|
+
void RejectDeferredWithMessage(
|
|
642
|
+
napi_env env,
|
|
643
|
+
napi_deferred deferred,
|
|
644
|
+
const char* message) {
|
|
645
|
+
napi_value error = nullptr;
|
|
646
|
+
napi_value message_value = nullptr;
|
|
647
|
+
napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &message_value);
|
|
648
|
+
napi_create_error(env, nullptr, message_value, &error);
|
|
649
|
+
napi_reject_deferred(env, deferred, error);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
struct ApplyPatchWork {
|
|
653
|
+
napi_async_work work = nullptr;
|
|
654
|
+
napi_deferred deferred = nullptr;
|
|
655
|
+
pushy::patch::FileSourcePatchOptions options;
|
|
656
|
+
pushy::patch::Status status{false, ""};
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
struct CleanupWork {
|
|
660
|
+
napi_async_work work = nullptr;
|
|
661
|
+
napi_deferred deferred = nullptr;
|
|
662
|
+
std::string root_dir;
|
|
663
|
+
std::string keep_current;
|
|
664
|
+
std::string keep_previous;
|
|
665
|
+
int32_t max_age_days = 0;
|
|
666
|
+
pushy::patch::Status status{false, ""};
|
|
667
|
+
};
|
|
668
|
+
|
|
720
669
|
napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
721
670
|
napi_value args[1] = {nullptr};
|
|
722
671
|
size_t argc = 1;
|
|
@@ -756,26 +705,69 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
756
705
|
return nullptr;
|
|
757
706
|
}
|
|
758
707
|
|
|
759
|
-
|
|
760
|
-
options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
|
|
761
|
-
options.source_root = source_root;
|
|
762
|
-
options.target_root = target_root;
|
|
763
|
-
options.origin_bundle_path = origin_bundle_path;
|
|
764
|
-
options.bundle_patch_path = bundle_patch_path;
|
|
765
|
-
options.bundle_output_path = bundle_output_path;
|
|
766
|
-
options.merge_source_subdir = merge_source_subdir;
|
|
767
|
-
options.enable_merge = enable_merge;
|
|
708
|
+
auto* work_data = new ApplyPatchWork();
|
|
709
|
+
work_data->options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
|
|
710
|
+
work_data->options.source_root = source_root;
|
|
711
|
+
work_data->options.target_root = target_root;
|
|
712
|
+
work_data->options.origin_bundle_path = origin_bundle_path;
|
|
713
|
+
work_data->options.bundle_patch_path = bundle_patch_path;
|
|
714
|
+
work_data->options.bundle_output_path = bundle_output_path;
|
|
715
|
+
work_data->options.merge_source_subdir = merge_source_subdir;
|
|
716
|
+
work_data->options.enable_merge = enable_merge;
|
|
768
717
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
ThrowError(env,
|
|
718
|
+
napi_value promise = nullptr;
|
|
719
|
+
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
|
|
720
|
+
delete work_data;
|
|
721
|
+
ThrowError(env, "Unable to create promise");
|
|
773
722
|
return nullptr;
|
|
774
723
|
}
|
|
775
724
|
|
|
776
|
-
napi_value
|
|
777
|
-
|
|
778
|
-
|
|
725
|
+
napi_value resource_name = nullptr;
|
|
726
|
+
napi_create_string_utf8(
|
|
727
|
+
env, "applyPatchFromFileSource", NAPI_AUTO_LENGTH, &resource_name);
|
|
728
|
+
if (napi_create_async_work(
|
|
729
|
+
env,
|
|
730
|
+
nullptr,
|
|
731
|
+
resource_name,
|
|
732
|
+
[](napi_env, void* data) {
|
|
733
|
+
auto* w = static_cast<ApplyPatchWork*>(data);
|
|
734
|
+
w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
|
|
735
|
+
},
|
|
736
|
+
[](napi_env cb_env, napi_status, void* data) {
|
|
737
|
+
auto* w = static_cast<ApplyPatchWork*>(data);
|
|
738
|
+
if (w->status.ok) {
|
|
739
|
+
napi_value undefined_value = nullptr;
|
|
740
|
+
napi_get_undefined(cb_env, &undefined_value);
|
|
741
|
+
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
|
|
742
|
+
} else {
|
|
743
|
+
napi_value error = nullptr;
|
|
744
|
+
napi_value message = nullptr;
|
|
745
|
+
napi_create_string_utf8(
|
|
746
|
+
cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
|
|
747
|
+
napi_create_error(cb_env, nullptr, message, &error);
|
|
748
|
+
napi_reject_deferred(cb_env, w->deferred, error);
|
|
749
|
+
}
|
|
750
|
+
napi_delete_async_work(cb_env, w->work);
|
|
751
|
+
delete w;
|
|
752
|
+
},
|
|
753
|
+
work_data,
|
|
754
|
+
&work_data->work) != napi_ok) {
|
|
755
|
+
// Work was never created: settle the promise and free the data so it does
|
|
756
|
+
// not leak / hang pending forever.
|
|
757
|
+
RejectDeferredWithMessage(
|
|
758
|
+
env, work_data->deferred, "Unable to create async work");
|
|
759
|
+
delete work_data;
|
|
760
|
+
return promise;
|
|
761
|
+
}
|
|
762
|
+
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
|
|
763
|
+
// Queued failed: the complete callback will never run, so clean up here.
|
|
764
|
+
napi_delete_async_work(env, work_data->work);
|
|
765
|
+
RejectDeferredWithMessage(
|
|
766
|
+
env, work_data->deferred, "Unable to queue async work");
|
|
767
|
+
delete work_data;
|
|
768
|
+
return promise;
|
|
769
|
+
}
|
|
770
|
+
return promise;
|
|
779
771
|
}
|
|
780
772
|
|
|
781
773
|
napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
@@ -804,19 +796,63 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
|
804
796
|
return nullptr;
|
|
805
797
|
}
|
|
806
798
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
799
|
+
auto* work_data = new CleanupWork();
|
|
800
|
+
work_data->root_dir = root_dir;
|
|
801
|
+
work_data->keep_current = keep_current;
|
|
802
|
+
work_data->keep_previous = keep_previous;
|
|
803
|
+
work_data->max_age_days = max_age_days;
|
|
804
|
+
|
|
805
|
+
napi_value promise = nullptr;
|
|
806
|
+
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
|
|
807
|
+
delete work_data;
|
|
808
|
+
ThrowError(env, "Unable to create promise");
|
|
814
809
|
return nullptr;
|
|
815
810
|
}
|
|
816
811
|
|
|
817
|
-
napi_value
|
|
818
|
-
|
|
819
|
-
|
|
812
|
+
napi_value resource_name = nullptr;
|
|
813
|
+
napi_create_string_utf8(
|
|
814
|
+
env, "cleanupOldEntries", NAPI_AUTO_LENGTH, &resource_name);
|
|
815
|
+
if (napi_create_async_work(
|
|
816
|
+
env,
|
|
817
|
+
nullptr,
|
|
818
|
+
resource_name,
|
|
819
|
+
[](napi_env, void* data) {
|
|
820
|
+
auto* w = static_cast<CleanupWork*>(data);
|
|
821
|
+
w->status = pushy::patch::CleanupOldEntries(
|
|
822
|
+
w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
|
|
823
|
+
},
|
|
824
|
+
[](napi_env cb_env, napi_status, void* data) {
|
|
825
|
+
auto* w = static_cast<CleanupWork*>(data);
|
|
826
|
+
if (w->status.ok) {
|
|
827
|
+
napi_value undefined_value = nullptr;
|
|
828
|
+
napi_get_undefined(cb_env, &undefined_value);
|
|
829
|
+
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
|
|
830
|
+
} else {
|
|
831
|
+
napi_value error = nullptr;
|
|
832
|
+
napi_value message = nullptr;
|
|
833
|
+
napi_create_string_utf8(
|
|
834
|
+
cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
|
|
835
|
+
napi_create_error(cb_env, nullptr, message, &error);
|
|
836
|
+
napi_reject_deferred(cb_env, w->deferred, error);
|
|
837
|
+
}
|
|
838
|
+
napi_delete_async_work(cb_env, w->work);
|
|
839
|
+
delete w;
|
|
840
|
+
},
|
|
841
|
+
work_data,
|
|
842
|
+
&work_data->work) != napi_ok) {
|
|
843
|
+
RejectDeferredWithMessage(
|
|
844
|
+
env, work_data->deferred, "Unable to create async work");
|
|
845
|
+
delete work_data;
|
|
846
|
+
return promise;
|
|
847
|
+
}
|
|
848
|
+
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
|
|
849
|
+
napi_delete_async_work(env, work_data->work);
|
|
850
|
+
RejectDeferredWithMessage(
|
|
851
|
+
env, work_data->deferred, "Unable to queue async work");
|
|
852
|
+
delete work_data;
|
|
853
|
+
return promise;
|
|
854
|
+
}
|
|
855
|
+
return promise;
|
|
820
856
|
}
|
|
821
857
|
|
|
822
858
|
bool ExportFunction(
|
|
@@ -841,8 +877,7 @@ bool ExportFunction(
|
|
|
841
877
|
} // namespace
|
|
842
878
|
|
|
843
879
|
napi_value Init(napi_env env, napi_value exports) {
|
|
844
|
-
if (!ExportFunction(env, exports, "
|
|
845
|
-
!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
880
|
+
if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
846
881
|
!ExportFunction(env, exports, "runStateCore", RunStateCore) ||
|
|
847
882
|
!ExportFunction(env, exports, "buildArchivePatchPlan", BuildArchivePatchPlan) ||
|
|
848
883
|
!ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
|
|
@@ -69,7 +69,7 @@ const FILE_COPY_BUFFER_SIZE = 64 * 1024;
|
|
|
69
69
|
|
|
70
70
|
export class DownloadTask {
|
|
71
71
|
private context: common.Context;
|
|
72
|
-
private hash
|
|
72
|
+
private hash = '';
|
|
73
73
|
private eventHub: EventHub;
|
|
74
74
|
|
|
75
75
|
constructor(context: common.Context) {
|
|
@@ -233,7 +233,7 @@ export class DownloadTask {
|
|
|
233
233
|
const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
|
|
234
234
|
try {
|
|
235
235
|
await this.writeFileContent(originBundlePath, originContent);
|
|
236
|
-
NativePatchCore.applyPatchFromFileSource({
|
|
236
|
+
await NativePatchCore.applyPatchFromFileSource({
|
|
237
237
|
copyFroms: [],
|
|
238
238
|
copyTos: [],
|
|
239
239
|
deletes: [],
|
|
@@ -244,7 +244,7 @@ export class DownloadTask {
|
|
|
244
244
|
bundleOutputPath: outputFile,
|
|
245
245
|
enableMerge: false,
|
|
246
246
|
});
|
|
247
|
-
} catch (error) {
|
|
247
|
+
} catch (error: any) {
|
|
248
248
|
error.message = `Failed to process bundle patch: ${error.message}`;
|
|
249
249
|
throw error;
|
|
250
250
|
} finally {
|
|
@@ -308,6 +308,26 @@ export class DownloadTask {
|
|
|
308
308
|
let received = 0;
|
|
309
309
|
let writeError: Error | null = null;
|
|
310
310
|
let writeQueue = Promise.resolve();
|
|
311
|
+
let lastReportedPercentage = -1;
|
|
312
|
+
let lastReportedBytes = 0;
|
|
313
|
+
|
|
314
|
+
// Emit at most one progress event per whole-percent change (or per 256KB
|
|
315
|
+
// when the total is unknown), and only from the dataReceive handler, so the
|
|
316
|
+
// two HarmonyOS callbacks don't each fire an event per chunk.
|
|
317
|
+
const reportProgress = () => {
|
|
318
|
+
if (contentLength > 0) {
|
|
319
|
+
const percentage = Math.round((received * 100) / contentLength);
|
|
320
|
+
if (percentage <= lastReportedPercentage) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
lastReportedPercentage = percentage;
|
|
324
|
+
} else if (received - lastReportedBytes < 256 * 1024) {
|
|
325
|
+
return;
|
|
326
|
+
} else {
|
|
327
|
+
lastReportedBytes = received;
|
|
328
|
+
}
|
|
329
|
+
this.onProgressUpdate(received, contentLength);
|
|
330
|
+
};
|
|
311
331
|
|
|
312
332
|
const closeWriter = async () => {
|
|
313
333
|
if (writer) {
|
|
@@ -316,8 +336,37 @@ export class DownloadTask {
|
|
|
316
336
|
}
|
|
317
337
|
};
|
|
318
338
|
|
|
339
|
+
// Watchdog: reject the download if no data is received for a while, so a
|
|
340
|
+
// stalled transfer after requestInStream resolves cannot hang the download
|
|
341
|
+
// Promise (and the JS caller) forever.
|
|
342
|
+
const INACTIVITY_TIMEOUT_MS = 60000;
|
|
343
|
+
let watchdogTimer: number | null = null;
|
|
344
|
+
let inactivityReject: ((error: Error) => void) | null = null;
|
|
345
|
+
const clearWatchdog = () => {
|
|
346
|
+
if (watchdogTimer !== null) {
|
|
347
|
+
clearTimeout(watchdogTimer);
|
|
348
|
+
watchdogTimer = null;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const refreshWatchdog = () => {
|
|
352
|
+
clearWatchdog();
|
|
353
|
+
watchdogTimer = setTimeout(() => {
|
|
354
|
+
if (inactivityReject) {
|
|
355
|
+
inactivityReject(
|
|
356
|
+
Error(
|
|
357
|
+
`Download stalled: no data received for ${INACTIVITY_TIMEOUT_MS}ms`,
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}, INACTIVITY_TIMEOUT_MS);
|
|
362
|
+
};
|
|
363
|
+
const inactivityPromise = new Promise<void>((_, reject) => {
|
|
364
|
+
inactivityReject = reject;
|
|
365
|
+
});
|
|
366
|
+
|
|
319
367
|
const dataEndPromise = new Promise<void>((resolve, reject) => {
|
|
320
368
|
httpRequest.on('dataEnd', () => {
|
|
369
|
+
clearWatchdog();
|
|
321
370
|
writeQueue
|
|
322
371
|
.then(async () => {
|
|
323
372
|
if (writeError) {
|
|
@@ -346,23 +395,25 @@ export class DownloadTask {
|
|
|
346
395
|
fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE,
|
|
347
396
|
);
|
|
348
397
|
|
|
349
|
-
httpRequest.on('headersReceive', (header:
|
|
398
|
+
httpRequest.on('headersReceive', (header: Object) => {
|
|
350
399
|
if (!header) {
|
|
351
400
|
return;
|
|
352
401
|
}
|
|
353
|
-
const
|
|
402
|
+
const headers = header as Record<string, string>;
|
|
403
|
+
const lengthKey = Object.keys(headers).find(
|
|
354
404
|
key => key.toLowerCase() === 'content-length',
|
|
355
405
|
);
|
|
356
406
|
if (!lengthKey) {
|
|
357
407
|
return;
|
|
358
408
|
}
|
|
359
|
-
const length = parseInt(
|
|
409
|
+
const length = parseInt(headers[lengthKey], 10);
|
|
360
410
|
if (!Number.isNaN(length)) {
|
|
361
411
|
contentLength = length;
|
|
362
412
|
}
|
|
363
413
|
});
|
|
364
414
|
|
|
365
415
|
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
|
|
416
|
+
refreshWatchdog();
|
|
366
417
|
if (writeError) {
|
|
367
418
|
return;
|
|
368
419
|
}
|
|
@@ -377,22 +428,21 @@ export class DownloadTask {
|
|
|
377
428
|
writeError = error as Error;
|
|
378
429
|
}
|
|
379
430
|
});
|
|
380
|
-
|
|
431
|
+
reportProgress();
|
|
381
432
|
});
|
|
382
433
|
|
|
383
434
|
httpRequest.on(
|
|
384
435
|
'dataReceiveProgress',
|
|
385
436
|
(data: http.DataReceiveProgressInfo) => {
|
|
437
|
+
// Only refine the known total here; progress events are emitted from
|
|
438
|
+
// the dataReceive handler to avoid double-firing.
|
|
386
439
|
if (data.totalSize > 0) {
|
|
387
440
|
contentLength = data.totalSize;
|
|
388
441
|
}
|
|
389
|
-
if (data.receiveSize > received) {
|
|
390
|
-
received = data.receiveSize;
|
|
391
|
-
}
|
|
392
|
-
this.onProgressUpdate(received, contentLength);
|
|
393
442
|
},
|
|
394
443
|
);
|
|
395
444
|
|
|
445
|
+
refreshWatchdog();
|
|
396
446
|
const responseCode = await httpRequest.requestInStream(params.url, {
|
|
397
447
|
method: http.RequestMethod.GET,
|
|
398
448
|
readTimeout: 60000,
|
|
@@ -405,7 +455,7 @@ export class DownloadTask {
|
|
|
405
455
|
throw Error(`Server error: ${responseCode}`);
|
|
406
456
|
}
|
|
407
457
|
|
|
408
|
-
await dataEndPromise;
|
|
458
|
+
await Promise.race([dataEndPromise, inactivityPromise]);
|
|
409
459
|
const stats = await fileIo.stat(params.targetFile);
|
|
410
460
|
const fileSize = stats.size;
|
|
411
461
|
if (contentLength > 0 && fileSize !== contentLength) {
|
|
@@ -417,6 +467,7 @@ export class DownloadTask {
|
|
|
417
467
|
console.error('Download failed:', error);
|
|
418
468
|
throw error;
|
|
419
469
|
} finally {
|
|
470
|
+
clearWatchdog();
|
|
420
471
|
try {
|
|
421
472
|
await closeWriter();
|
|
422
473
|
} catch (closeError) {
|
|
@@ -518,7 +569,7 @@ export class DownloadTask {
|
|
|
518
569
|
manifestArrays.deletes,
|
|
519
570
|
HARMONY_BUNDLE_PATCH_ENTRY,
|
|
520
571
|
);
|
|
521
|
-
NativePatchCore.applyPatchFromFileSource({
|
|
572
|
+
await NativePatchCore.applyPatchFromFileSource({
|
|
522
573
|
copyFroms: manifestArrays.copyFroms,
|
|
523
574
|
copyTos: manifestArrays.copyTos,
|
|
524
575
|
deletes: manifestArrays.deletes,
|
|
@@ -556,9 +607,11 @@ export class DownloadTask {
|
|
|
556
607
|
}
|
|
557
608
|
|
|
558
609
|
if (currentFrom.startsWith('resources/base/media/')) {
|
|
610
|
+
// Strip only the final extension: 'icon.round.png' -> 'icon.round',
|
|
611
|
+
// not 'icon' (getMediaByName expects the full resource name).
|
|
559
612
|
const mediaName = currentFrom
|
|
560
613
|
.replace('resources/base/media/', '')
|
|
561
|
-
.
|
|
614
|
+
.replace(/\.[^.]+$/, '');
|
|
562
615
|
const mediaBuffer = await resourceManager.getMediaByName(mediaName);
|
|
563
616
|
const parentDirs = [
|
|
564
617
|
...new Set(
|
|
@@ -599,7 +652,7 @@ export class DownloadTask {
|
|
|
599
652
|
}
|
|
600
653
|
}
|
|
601
654
|
}
|
|
602
|
-
} catch (error) {
|
|
655
|
+
} catch (error: any) {
|
|
603
656
|
error.message =
|
|
604
657
|
'Copy from resource failed:' +
|
|
605
658
|
currentFrom +
|
|
@@ -614,13 +667,13 @@ export class DownloadTask {
|
|
|
614
667
|
|
|
615
668
|
private async doCleanUp(params: DownloadTaskParams): Promise<void> {
|
|
616
669
|
try {
|
|
617
|
-
NativePatchCore.cleanupOldEntries(
|
|
670
|
+
await NativePatchCore.cleanupOldEntries(
|
|
618
671
|
params.unzipDirectory,
|
|
619
672
|
params.hash || '',
|
|
620
673
|
params.originHash || '',
|
|
621
674
|
3,
|
|
622
675
|
);
|
|
623
|
-
} catch (error) {
|
|
676
|
+
} catch (error: any) {
|
|
624
677
|
error.message = 'Cleanup failed:' + error.message;
|
|
625
678
|
console.error(error);
|
|
626
679
|
throw error;
|
|
@@ -648,7 +701,7 @@ export class DownloadTask {
|
|
|
648
701
|
default:
|
|
649
702
|
throw Error(`Unknown task type: ${params.type}`);
|
|
650
703
|
}
|
|
651
|
-
} catch (error) {
|
|
704
|
+
} catch (error: any) {
|
|
652
705
|
console.error('Task execution failed:', error.message);
|
|
653
706
|
if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
|
|
654
707
|
try {
|
|
@@ -657,7 +710,7 @@ export class DownloadTask {
|
|
|
657
710
|
} else {
|
|
658
711
|
await this.removeDirectory(params.unzipDirectory);
|
|
659
712
|
}
|
|
660
|
-
} catch (cleanupError) {
|
|
713
|
+
} catch (cleanupError: any) {
|
|
661
714
|
console.error('Cleanup after error failed:', cleanupError.message);
|
|
662
715
|
}
|
|
663
716
|
}
|
|
@@ -50,10 +50,6 @@ export interface FileSourcePatchRequest {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
interface NativePatchCoreBindings {
|
|
53
|
-
hdiffPatch(
|
|
54
|
-
origin: Uint8Array,
|
|
55
|
-
patch: Uint8Array,
|
|
56
|
-
): ArrayBuffer | Uint8Array;
|
|
57
53
|
syncStateWithBinaryVersion(
|
|
58
54
|
packageVersion: string,
|
|
59
55
|
buildTime: string,
|
|
@@ -75,13 +71,13 @@ interface NativePatchCoreBindings {
|
|
|
75
71
|
bundlePatchEntryName?: string,
|
|
76
72
|
): ArchivePatchPlanResult;
|
|
77
73
|
buildCopyGroups(copyFroms: string[], copyTos: string[]): CopyGroupResult[];
|
|
78
|
-
applyPatchFromFileSource(options: FileSourcePatchRequest): void
|
|
74
|
+
applyPatchFromFileSource(options: FileSourcePatchRequest): Promise<void>;
|
|
79
75
|
cleanupOldEntries(
|
|
80
76
|
rootDir: string,
|
|
81
77
|
keepCurrent: string,
|
|
82
78
|
keepPrevious: string,
|
|
83
79
|
maxAgeDays: number,
|
|
84
|
-
): void
|
|
80
|
+
): Promise<void>;
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
export default NativeUpdateCore as unknown as NativePatchCoreBindings;
|
|
@@ -93,23 +93,23 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
93
93
|
}
|
|
94
94
|
} else {
|
|
95
95
|
logger.debug(TAG, 'reloadBridge via restartAbility (release mode)');
|
|
96
|
-
|
|
96
|
+
// If the process truly restarts, this timer dies with it. It only fires
|
|
97
|
+
// when the app is still alive after 1.5s — i.e. restartApp resolved but
|
|
98
|
+
// was silently suppressed (HarmonyOS rate-limits restarts within a few
|
|
99
|
+
// seconds of cold start / of a previous call) — which is exactly when the
|
|
100
|
+
// soft reload must take over. So the timer is NOT cleared on the success
|
|
101
|
+
// path, only in the catch branch where the soft reload runs immediately.
|
|
97
102
|
const fallbackTimer = setTimeout(() => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
devToolsController.eventEmitter.emit("RELOAD", { reason: 'HotReload2' });
|
|
103
|
-
}
|
|
103
|
+
logger.warn(TAG, 'restartAbility did not restart the app within 1.5s, triggering soft reload fallback');
|
|
104
|
+
const devToolsController = (this.ctx as Record<string, any>).devToolsController;
|
|
105
|
+
if (devToolsController) {
|
|
106
|
+
devToolsController.eventEmitter.emit("RELOAD", { reason: 'HotReload2' });
|
|
104
107
|
}
|
|
105
108
|
}, 1500);
|
|
106
109
|
|
|
107
110
|
try {
|
|
108
111
|
await this.restartAbility();
|
|
109
|
-
restarted = true;
|
|
110
|
-
clearTimeout(fallbackTimer);
|
|
111
112
|
} catch (error) {
|
|
112
|
-
restarted = false;
|
|
113
113
|
clearTimeout(fallbackTimer);
|
|
114
114
|
logger.error(TAG, `restartAbility failed: ${getErrorMessage(error)}, triggering soft reload fallback`);
|
|
115
115
|
const devToolsController = (this.ctx as Record<string, any>).devToolsController;
|