react-native-update 10.43.3 → 10.44.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/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +23 -8
- 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 +2 -2
- 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_android.cpp +4 -37
- package/cpp/patch_core/state_ops.h +24 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +32 -2
- package/cpp/patch_core/update_core_android.cpp +43 -69
- package/harmony/pushy/src/main/cpp/pushy.cpp +162 -35
- package/harmony/pushy/src/main/ets/DownloadTask.ts +59 -9
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -2
- 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/RCTPushyDownloader.mm +41 -2
- package/package.json +2 -1
- package/src/client.ts +130 -75
- package/src/provider.tsx +40 -26
- package/src/utils.ts +2 -2
- 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
|
@@ -4,56 +4,16 @@
|
|
|
4
4
|
#include <vector>
|
|
5
5
|
|
|
6
6
|
#include "archive_patch_core.h"
|
|
7
|
+
#include "jni_util.h"
|
|
7
8
|
#include "state_core.h"
|
|
9
|
+
#include "state_ops.h"
|
|
8
10
|
|
|
9
11
|
namespace {
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
kClearFirstTime = 4,
|
|
16
|
-
kClearRollbackMark = 5,
|
|
17
|
-
kResolveLaunch = 6,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
std::string JStringToString(JNIEnv* env, jstring value) {
|
|
21
|
-
if (value == nullptr) {
|
|
22
|
-
return std::string();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const char* chars = env->GetStringUTFChars(value, nullptr);
|
|
26
|
-
if (chars == nullptr) {
|
|
27
|
-
return std::string();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
std::string result(chars);
|
|
31
|
-
env->ReleaseStringUTFChars(value, chars);
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
std::vector<std::string> JArrayToVector(JNIEnv* env, jobjectArray values) {
|
|
36
|
-
std::vector<std::string> result;
|
|
37
|
-
if (values == nullptr) {
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const jsize size = env->GetArrayLength(values);
|
|
42
|
-
result.reserve(static_cast<size_t>(size));
|
|
43
|
-
for (jsize index = 0; index < size; ++index) {
|
|
44
|
-
auto* item = static_cast<jstring>(env->GetObjectArrayElement(values, index));
|
|
45
|
-
result.push_back(JStringToString(env, item));
|
|
46
|
-
env->DeleteLocalRef(item);
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
void ThrowRuntimeException(JNIEnv* env, const std::string& message) {
|
|
52
|
-
jclass exception = env->FindClass("java/lang/RuntimeException");
|
|
53
|
-
if (exception != nullptr) {
|
|
54
|
-
env->ThrowNew(exception, message.c_str());
|
|
55
|
-
}
|
|
56
|
-
}
|
|
13
|
+
using pushy::jni_util::JArrayToVector;
|
|
14
|
+
using pushy::jni_util::JStringToString;
|
|
15
|
+
using pushy::jni_util::ThrowRuntimeException;
|
|
16
|
+
using pushy::state_ops::StateOperation;
|
|
57
17
|
|
|
58
18
|
void SetStringField(
|
|
59
19
|
JNIEnv* env,
|
|
@@ -64,6 +24,9 @@ void SetStringField(
|
|
|
64
24
|
jfieldID field =
|
|
65
25
|
env->GetFieldID(target_class, field_name, "Ljava/lang/String;");
|
|
66
26
|
if (field == nullptr) {
|
|
27
|
+
// Clear the pending NoSuchFieldError so subsequent JNI calls are not
|
|
28
|
+
// executed with an exception pending (undefined behavior / CheckJNI abort).
|
|
29
|
+
env->ExceptionClear();
|
|
67
30
|
return;
|
|
68
31
|
}
|
|
69
32
|
if (value.empty()) {
|
|
@@ -83,9 +46,11 @@ void SetBooleanField(
|
|
|
83
46
|
const char* field_name,
|
|
84
47
|
bool value) {
|
|
85
48
|
jfieldID field = env->GetFieldID(target_class, field_name, "Z");
|
|
86
|
-
if (field
|
|
87
|
-
env->
|
|
49
|
+
if (field == nullptr) {
|
|
50
|
+
env->ExceptionClear();
|
|
51
|
+
return;
|
|
88
52
|
}
|
|
53
|
+
env->SetBooleanField(target, field, value ? JNI_TRUE : JNI_FALSE);
|
|
89
54
|
}
|
|
90
55
|
|
|
91
56
|
std::string GetStringField(
|
|
@@ -96,6 +61,7 @@ std::string GetStringField(
|
|
|
96
61
|
jfieldID field =
|
|
97
62
|
env->GetFieldID(target_class, field_name, "Ljava/lang/String;");
|
|
98
63
|
if (field == nullptr) {
|
|
64
|
+
env->ExceptionClear();
|
|
99
65
|
return std::string();
|
|
100
66
|
}
|
|
101
67
|
auto* value = static_cast<jstring>(env->GetObjectField(target, field));
|
|
@@ -112,7 +78,11 @@ bool GetBooleanField(
|
|
|
112
78
|
jclass target_class,
|
|
113
79
|
const char* field_name) {
|
|
114
80
|
jfieldID field = env->GetFieldID(target_class, field_name, "Z");
|
|
115
|
-
|
|
81
|
+
if (field == nullptr) {
|
|
82
|
+
env->ExceptionClear();
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return env->GetBooleanField(target, field) == JNI_TRUE;
|
|
116
86
|
}
|
|
117
87
|
|
|
118
88
|
pushy::state::State ReadState(
|
|
@@ -241,13 +211,9 @@ jobject NewArchivePatchPlanResult(
|
|
|
241
211
|
|
|
242
212
|
jobject NewCopyGroupResult(
|
|
243
213
|
JNIEnv* env,
|
|
214
|
+
jclass result_class,
|
|
215
|
+
jclass string_class,
|
|
244
216
|
const pushy::archive_patch::CopyGroup& group) {
|
|
245
|
-
jclass result_class =
|
|
246
|
-
env->FindClass("cn/reactnative/modules/update/CopyGroupResult");
|
|
247
|
-
if (result_class == nullptr) {
|
|
248
|
-
return nullptr;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
217
|
jmethodID constructor = env->GetMethodID(result_class, "<init>", "()V");
|
|
252
218
|
if (constructor == nullptr) {
|
|
253
219
|
return nullptr;
|
|
@@ -262,7 +228,6 @@ jobject NewCopyGroupResult(
|
|
|
262
228
|
jfieldID to_paths_field =
|
|
263
229
|
env->GetFieldID(result_class, "toPaths", "[Ljava/lang/String;");
|
|
264
230
|
if (to_paths_field != nullptr) {
|
|
265
|
-
jclass string_class = env->FindClass("java/lang/String");
|
|
266
231
|
jobjectArray to_paths = env->NewObjectArray(
|
|
267
232
|
static_cast<jsize>(group.to_paths.size()), string_class, nullptr);
|
|
268
233
|
if (to_paths != nullptr) {
|
|
@@ -275,22 +240,20 @@ jobject NewCopyGroupResult(
|
|
|
275
240
|
env->SetObjectField(result, to_paths_field, to_paths);
|
|
276
241
|
env->DeleteLocalRef(to_paths);
|
|
277
242
|
}
|
|
243
|
+
} else {
|
|
244
|
+
env->ExceptionClear();
|
|
278
245
|
}
|
|
279
246
|
|
|
280
247
|
return result;
|
|
281
248
|
}
|
|
282
249
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return pushy::archive_patch::ArchivePatchType::kPatchFromPpk;
|
|
291
|
-
default:
|
|
292
|
-
return pushy::archive_patch::ArchivePatchType::kFull;
|
|
293
|
-
}
|
|
250
|
+
bool ToArchivePatchType(
|
|
251
|
+
jint patch_type,
|
|
252
|
+
pushy::archive_patch::ArchivePatchType* out) {
|
|
253
|
+
// Delegate to the shared parser so Android and HarmonyOS agree on which
|
|
254
|
+
// integer values are valid and neither silently falls back to kFull.
|
|
255
|
+
return pushy::archive_patch::TryParseArchivePatchType(
|
|
256
|
+
static_cast<int>(patch_type), out);
|
|
294
257
|
}
|
|
295
258
|
|
|
296
259
|
pushy::patch::PatchManifest BuildManifest(
|
|
@@ -411,9 +374,14 @@ Java_cn_reactnative_modules_update_DownloadTask_buildArchivePatchPlan(
|
|
|
411
374
|
|
|
412
375
|
pushy::patch::PatchManifest manifest =
|
|
413
376
|
BuildManifest(from_values, to_values, JArrayToVector(env, deletes));
|
|
377
|
+
pushy::archive_patch::ArchivePatchType archive_type;
|
|
378
|
+
if (!ToArchivePatchType(patch_type, &archive_type)) {
|
|
379
|
+
ThrowRuntimeException(env, "Unknown archive patch type");
|
|
380
|
+
return nullptr;
|
|
381
|
+
}
|
|
414
382
|
pushy::archive_patch::ArchivePatchPlan plan;
|
|
415
383
|
pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
|
|
416
|
-
|
|
384
|
+
archive_type,
|
|
417
385
|
manifest,
|
|
418
386
|
JArrayToVector(env, entry_names),
|
|
419
387
|
&plan);
|
|
@@ -454,6 +422,11 @@ Java_cn_reactnative_modules_update_DownloadTask_buildCopyGroups(
|
|
|
454
422
|
return nullptr;
|
|
455
423
|
}
|
|
456
424
|
|
|
425
|
+
jclass string_class = env->FindClass("java/lang/String");
|
|
426
|
+
if (string_class == nullptr) {
|
|
427
|
+
return nullptr;
|
|
428
|
+
}
|
|
429
|
+
|
|
457
430
|
jobjectArray result = env->NewObjectArray(
|
|
458
431
|
static_cast<jsize>(groups.size()), result_class, nullptr);
|
|
459
432
|
if (result == nullptr) {
|
|
@@ -461,9 +434,10 @@ Java_cn_reactnative_modules_update_DownloadTask_buildCopyGroups(
|
|
|
461
434
|
}
|
|
462
435
|
|
|
463
436
|
for (jsize index = 0; index < static_cast<jsize>(groups.size()); ++index) {
|
|
464
|
-
jobject group = NewCopyGroupResult(env, groups[index]);
|
|
437
|
+
jobject group = NewCopyGroupResult(env, result_class, string_class, groups[index]);
|
|
465
438
|
env->SetObjectArrayElement(result, index, group);
|
|
466
439
|
env->DeleteLocalRef(group);
|
|
467
440
|
}
|
|
441
|
+
env->DeleteLocalRef(string_class);
|
|
468
442
|
return result;
|
|
469
443
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
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
14
|
#include "hpatch.h"
|
|
@@ -15,14 +16,7 @@ extern "C" {
|
|
|
15
16
|
|
|
16
17
|
namespace {
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
kSwitchVersion = 1,
|
|
20
|
-
kMarkSuccess = 2,
|
|
21
|
-
kRollback = 3,
|
|
22
|
-
kClearFirstTime = 4,
|
|
23
|
-
kClearRollbackMark = 5,
|
|
24
|
-
kResolveLaunch = 6,
|
|
25
|
-
};
|
|
19
|
+
using pushy::state_ops::StateOperation;
|
|
26
20
|
|
|
27
21
|
constexpr const char* kDefaultBundlePatchEntryName = "index.bundlejs.patch";
|
|
28
22
|
|
|
@@ -670,9 +664,14 @@ napi_value BuildArchivePatchPlan(napi_env env, napi_callback_info info) {
|
|
|
670
664
|
|
|
671
665
|
const pushy::patch::PatchManifest manifest =
|
|
672
666
|
BuildManifest(copy_froms, copy_tos, deletes);
|
|
667
|
+
pushy::archive_patch::ArchivePatchType archive_type;
|
|
668
|
+
if (!pushy::archive_patch::TryParseArchivePatchType(patch_type, &archive_type)) {
|
|
669
|
+
ThrowError(env, "Unknown archive patch type");
|
|
670
|
+
return nullptr;
|
|
671
|
+
}
|
|
673
672
|
pushy::archive_patch::ArchivePatchPlan plan;
|
|
674
673
|
const pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
|
|
675
|
-
|
|
674
|
+
archive_type,
|
|
676
675
|
manifest,
|
|
677
676
|
entry_names,
|
|
678
677
|
&plan,
|
|
@@ -717,6 +716,47 @@ napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
|
|
|
717
716
|
return NewCopyGroupArray(env, groups);
|
|
718
717
|
}
|
|
719
718
|
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
// Async work plumbing for the heavy patch operations.
|
|
721
|
+
//
|
|
722
|
+
// applyPatchFromFileSource and cleanupOldEntries run hdiff / recursive file IO
|
|
723
|
+
// that can take hundreds of ms to seconds. The Pushy TurboModule executes on
|
|
724
|
+
// the UI thread, so running these synchronously froze the UI. These are now
|
|
725
|
+
// wrapped in napi_create_async_work: arguments are parsed on the JS thread, the
|
|
726
|
+
// heavy work runs on a libuv worker thread, and the returned Promise is settled
|
|
727
|
+
// back on the JS thread.
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
// Reject an already-created deferred with an Error(message). Used when async
|
|
731
|
+
// work fails to be created/queued, so the Promise never hangs pending.
|
|
732
|
+
void RejectDeferredWithMessage(
|
|
733
|
+
napi_env env,
|
|
734
|
+
napi_deferred deferred,
|
|
735
|
+
const char* message) {
|
|
736
|
+
napi_value error = nullptr;
|
|
737
|
+
napi_value message_value = nullptr;
|
|
738
|
+
napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &message_value);
|
|
739
|
+
napi_create_error(env, nullptr, message_value, &error);
|
|
740
|
+
napi_reject_deferred(env, deferred, error);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
struct ApplyPatchWork {
|
|
744
|
+
napi_async_work work = nullptr;
|
|
745
|
+
napi_deferred deferred = nullptr;
|
|
746
|
+
pushy::patch::FileSourcePatchOptions options;
|
|
747
|
+
pushy::patch::Status status{false, ""};
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
struct CleanupWork {
|
|
751
|
+
napi_async_work work = nullptr;
|
|
752
|
+
napi_deferred deferred = nullptr;
|
|
753
|
+
std::string root_dir;
|
|
754
|
+
std::string keep_current;
|
|
755
|
+
std::string keep_previous;
|
|
756
|
+
int32_t max_age_days = 0;
|
|
757
|
+
pushy::patch::Status status{false, ""};
|
|
758
|
+
};
|
|
759
|
+
|
|
720
760
|
napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
721
761
|
napi_value args[1] = {nullptr};
|
|
722
762
|
size_t argc = 1;
|
|
@@ -756,26 +796,69 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
756
796
|
return nullptr;
|
|
757
797
|
}
|
|
758
798
|
|
|
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;
|
|
799
|
+
auto* work_data = new ApplyPatchWork();
|
|
800
|
+
work_data->options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
|
|
801
|
+
work_data->options.source_root = source_root;
|
|
802
|
+
work_data->options.target_root = target_root;
|
|
803
|
+
work_data->options.origin_bundle_path = origin_bundle_path;
|
|
804
|
+
work_data->options.bundle_patch_path = bundle_patch_path;
|
|
805
|
+
work_data->options.bundle_output_path = bundle_output_path;
|
|
806
|
+
work_data->options.merge_source_subdir = merge_source_subdir;
|
|
807
|
+
work_data->options.enable_merge = enable_merge;
|
|
768
808
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
ThrowError(env,
|
|
809
|
+
napi_value promise = nullptr;
|
|
810
|
+
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
|
|
811
|
+
delete work_data;
|
|
812
|
+
ThrowError(env, "Unable to create promise");
|
|
773
813
|
return nullptr;
|
|
774
814
|
}
|
|
775
815
|
|
|
776
|
-
napi_value
|
|
777
|
-
|
|
778
|
-
|
|
816
|
+
napi_value resource_name = nullptr;
|
|
817
|
+
napi_create_string_utf8(
|
|
818
|
+
env, "applyPatchFromFileSource", NAPI_AUTO_LENGTH, &resource_name);
|
|
819
|
+
if (napi_create_async_work(
|
|
820
|
+
env,
|
|
821
|
+
nullptr,
|
|
822
|
+
resource_name,
|
|
823
|
+
[](napi_env, void* data) {
|
|
824
|
+
auto* w = static_cast<ApplyPatchWork*>(data);
|
|
825
|
+
w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
|
|
826
|
+
},
|
|
827
|
+
[](napi_env cb_env, napi_status, void* data) {
|
|
828
|
+
auto* w = static_cast<ApplyPatchWork*>(data);
|
|
829
|
+
if (w->status.ok) {
|
|
830
|
+
napi_value undefined_value = nullptr;
|
|
831
|
+
napi_get_undefined(cb_env, &undefined_value);
|
|
832
|
+
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
|
|
833
|
+
} else {
|
|
834
|
+
napi_value error = nullptr;
|
|
835
|
+
napi_value message = nullptr;
|
|
836
|
+
napi_create_string_utf8(
|
|
837
|
+
cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
|
|
838
|
+
napi_create_error(cb_env, nullptr, message, &error);
|
|
839
|
+
napi_reject_deferred(cb_env, w->deferred, error);
|
|
840
|
+
}
|
|
841
|
+
napi_delete_async_work(cb_env, w->work);
|
|
842
|
+
delete w;
|
|
843
|
+
},
|
|
844
|
+
work_data,
|
|
845
|
+
&work_data->work) != napi_ok) {
|
|
846
|
+
// Work was never created: settle the promise and free the data so it does
|
|
847
|
+
// not leak / hang pending forever.
|
|
848
|
+
RejectDeferredWithMessage(
|
|
849
|
+
env, work_data->deferred, "Unable to create async work");
|
|
850
|
+
delete work_data;
|
|
851
|
+
return promise;
|
|
852
|
+
}
|
|
853
|
+
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
|
|
854
|
+
// Queued failed: the complete callback will never run, so clean up here.
|
|
855
|
+
napi_delete_async_work(env, work_data->work);
|
|
856
|
+
RejectDeferredWithMessage(
|
|
857
|
+
env, work_data->deferred, "Unable to queue async work");
|
|
858
|
+
delete work_data;
|
|
859
|
+
return promise;
|
|
860
|
+
}
|
|
861
|
+
return promise;
|
|
779
862
|
}
|
|
780
863
|
|
|
781
864
|
napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
@@ -804,19 +887,63 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
|
804
887
|
return nullptr;
|
|
805
888
|
}
|
|
806
889
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
890
|
+
auto* work_data = new CleanupWork();
|
|
891
|
+
work_data->root_dir = root_dir;
|
|
892
|
+
work_data->keep_current = keep_current;
|
|
893
|
+
work_data->keep_previous = keep_previous;
|
|
894
|
+
work_data->max_age_days = max_age_days;
|
|
895
|
+
|
|
896
|
+
napi_value promise = nullptr;
|
|
897
|
+
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
|
|
898
|
+
delete work_data;
|
|
899
|
+
ThrowError(env, "Unable to create promise");
|
|
814
900
|
return nullptr;
|
|
815
901
|
}
|
|
816
902
|
|
|
817
|
-
napi_value
|
|
818
|
-
|
|
819
|
-
|
|
903
|
+
napi_value resource_name = nullptr;
|
|
904
|
+
napi_create_string_utf8(
|
|
905
|
+
env, "cleanupOldEntries", NAPI_AUTO_LENGTH, &resource_name);
|
|
906
|
+
if (napi_create_async_work(
|
|
907
|
+
env,
|
|
908
|
+
nullptr,
|
|
909
|
+
resource_name,
|
|
910
|
+
[](napi_env, void* data) {
|
|
911
|
+
auto* w = static_cast<CleanupWork*>(data);
|
|
912
|
+
w->status = pushy::patch::CleanupOldEntries(
|
|
913
|
+
w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
|
|
914
|
+
},
|
|
915
|
+
[](napi_env cb_env, napi_status, void* data) {
|
|
916
|
+
auto* w = static_cast<CleanupWork*>(data);
|
|
917
|
+
if (w->status.ok) {
|
|
918
|
+
napi_value undefined_value = nullptr;
|
|
919
|
+
napi_get_undefined(cb_env, &undefined_value);
|
|
920
|
+
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
|
|
921
|
+
} else {
|
|
922
|
+
napi_value error = nullptr;
|
|
923
|
+
napi_value message = nullptr;
|
|
924
|
+
napi_create_string_utf8(
|
|
925
|
+
cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
|
|
926
|
+
napi_create_error(cb_env, nullptr, message, &error);
|
|
927
|
+
napi_reject_deferred(cb_env, w->deferred, error);
|
|
928
|
+
}
|
|
929
|
+
napi_delete_async_work(cb_env, w->work);
|
|
930
|
+
delete w;
|
|
931
|
+
},
|
|
932
|
+
work_data,
|
|
933
|
+
&work_data->work) != napi_ok) {
|
|
934
|
+
RejectDeferredWithMessage(
|
|
935
|
+
env, work_data->deferred, "Unable to create async work");
|
|
936
|
+
delete work_data;
|
|
937
|
+
return promise;
|
|
938
|
+
}
|
|
939
|
+
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
|
|
940
|
+
napi_delete_async_work(env, work_data->work);
|
|
941
|
+
RejectDeferredWithMessage(
|
|
942
|
+
env, work_data->deferred, "Unable to queue async work");
|
|
943
|
+
delete work_data;
|
|
944
|
+
return promise;
|
|
945
|
+
}
|
|
946
|
+
return promise;
|
|
820
947
|
}
|
|
821
948
|
|
|
822
949
|
bool ExportFunction(
|
|
@@ -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: [],
|
|
@@ -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) {
|
|
@@ -363,6 +412,7 @@ export class DownloadTask {
|
|
|
363
412
|
});
|
|
364
413
|
|
|
365
414
|
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
|
|
415
|
+
refreshWatchdog();
|
|
366
416
|
if (writeError) {
|
|
367
417
|
return;
|
|
368
418
|
}
|
|
@@ -377,22 +427,21 @@ export class DownloadTask {
|
|
|
377
427
|
writeError = error as Error;
|
|
378
428
|
}
|
|
379
429
|
});
|
|
380
|
-
|
|
430
|
+
reportProgress();
|
|
381
431
|
});
|
|
382
432
|
|
|
383
433
|
httpRequest.on(
|
|
384
434
|
'dataReceiveProgress',
|
|
385
435
|
(data: http.DataReceiveProgressInfo) => {
|
|
436
|
+
// Only refine the known total here; progress events are emitted from
|
|
437
|
+
// the dataReceive handler to avoid double-firing.
|
|
386
438
|
if (data.totalSize > 0) {
|
|
387
439
|
contentLength = data.totalSize;
|
|
388
440
|
}
|
|
389
|
-
if (data.receiveSize > received) {
|
|
390
|
-
received = data.receiveSize;
|
|
391
|
-
}
|
|
392
|
-
this.onProgressUpdate(received, contentLength);
|
|
393
441
|
},
|
|
394
442
|
);
|
|
395
443
|
|
|
444
|
+
refreshWatchdog();
|
|
396
445
|
const responseCode = await httpRequest.requestInStream(params.url, {
|
|
397
446
|
method: http.RequestMethod.GET,
|
|
398
447
|
readTimeout: 60000,
|
|
@@ -405,7 +454,7 @@ export class DownloadTask {
|
|
|
405
454
|
throw Error(`Server error: ${responseCode}`);
|
|
406
455
|
}
|
|
407
456
|
|
|
408
|
-
await dataEndPromise;
|
|
457
|
+
await Promise.race([dataEndPromise, inactivityPromise]);
|
|
409
458
|
const stats = await fileIo.stat(params.targetFile);
|
|
410
459
|
const fileSize = stats.size;
|
|
411
460
|
if (contentLength > 0 && fileSize !== contentLength) {
|
|
@@ -417,6 +466,7 @@ export class DownloadTask {
|
|
|
417
466
|
console.error('Download failed:', error);
|
|
418
467
|
throw error;
|
|
419
468
|
} finally {
|
|
469
|
+
clearWatchdog();
|
|
420
470
|
try {
|
|
421
471
|
await closeWriter();
|
|
422
472
|
} catch (closeError) {
|
|
@@ -518,7 +568,7 @@ export class DownloadTask {
|
|
|
518
568
|
manifestArrays.deletes,
|
|
519
569
|
HARMONY_BUNDLE_PATCH_ENTRY,
|
|
520
570
|
);
|
|
521
|
-
NativePatchCore.applyPatchFromFileSource({
|
|
571
|
+
await NativePatchCore.applyPatchFromFileSource({
|
|
522
572
|
copyFroms: manifestArrays.copyFroms,
|
|
523
573
|
copyTos: manifestArrays.copyTos,
|
|
524
574
|
deletes: manifestArrays.deletes,
|
|
@@ -614,7 +664,7 @@ export class DownloadTask {
|
|
|
614
664
|
|
|
615
665
|
private async doCleanUp(params: DownloadTaskParams): Promise<void> {
|
|
616
666
|
try {
|
|
617
|
-
NativePatchCore.cleanupOldEntries(
|
|
667
|
+
await NativePatchCore.cleanupOldEntries(
|
|
618
668
|
params.unzipDirectory,
|
|
619
669
|
params.hash || '',
|
|
620
670
|
params.originHash || '',
|
|
@@ -75,13 +75,13 @@ interface NativePatchCoreBindings {
|
|
|
75
75
|
bundlePatchEntryName?: string,
|
|
76
76
|
): ArchivePatchPlanResult;
|
|
77
77
|
buildCopyGroups(copyFroms: string[], copyTos: string[]): CopyGroupResult[];
|
|
78
|
-
applyPatchFromFileSource(options: FileSourcePatchRequest): void
|
|
78
|
+
applyPatchFromFileSource(options: FileSourcePatchRequest): Promise<void>;
|
|
79
79
|
cleanupOldEntries(
|
|
80
80
|
rootDir: string,
|
|
81
81
|
keepCurrent: string,
|
|
82
82
|
keepPrevious: string,
|
|
83
83
|
maxAgeDays: number,
|
|
84
|
-
): void
|
|
84
|
+
): Promise<void>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
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;
|