react-native-update 10.55.1 → 10.56.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/README-CN.md +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +399 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +36 -3
- package/src/client.ts +236 -82
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
|
@@ -16,6 +16,7 @@ import java.util.Enumeration;
|
|
|
16
16
|
import java.util.HashMap;
|
|
17
17
|
import java.util.Iterator;
|
|
18
18
|
import java.util.zip.ZipEntry;
|
|
19
|
+
import okhttp3.Call;
|
|
19
20
|
import okhttp3.OkHttpClient;
|
|
20
21
|
import okhttp3.Request;
|
|
21
22
|
import okhttp3.Response;
|
|
@@ -180,31 +181,6 @@ class DownloadTask implements Runnable {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
// "bytes <start>-<end>/<total>". Returns the total (0 when "*"), or -1
|
|
184
|
-
// when the header is missing/malformed or the start does not match the
|
|
185
|
-
// local partial — either way the appended bytes could not be trusted.
|
|
186
|
-
private static long parseContentRange(String header, long expectedStart) {
|
|
187
|
-
if (header == null || !header.startsWith("bytes ")) {
|
|
188
|
-
return -1;
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
String range = header.substring("bytes ".length()).trim();
|
|
192
|
-
int slash = range.indexOf('/');
|
|
193
|
-
int dash = range.indexOf('-');
|
|
194
|
-
if (slash < 0 || dash < 0 || dash > slash) {
|
|
195
|
-
return -1;
|
|
196
|
-
}
|
|
197
|
-
long start = Long.parseLong(range.substring(0, dash).trim());
|
|
198
|
-
if (start != expectedStart) {
|
|
199
|
-
return -1;
|
|
200
|
-
}
|
|
201
|
-
String totalPart = range.substring(slash + 1).trim();
|
|
202
|
-
return totalPart.equals("*") ? 0 : Long.parseLong(totalPart);
|
|
203
|
-
} catch (NumberFormatException e) {
|
|
204
|
-
return -1;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
184
|
private void downloadFile() throws IOException {
|
|
209
185
|
this.hash = params.hash;
|
|
210
186
|
String url = params.url;
|
|
@@ -298,7 +274,11 @@ class DownloadTask implements Runnable {
|
|
|
298
274
|
.build();
|
|
299
275
|
}
|
|
300
276
|
|
|
301
|
-
|
|
277
|
+
Call call = requestClient.newCall(builder.build());
|
|
278
|
+
// Registered so an orchestrated phase timeout can cancel it
|
|
279
|
+
// (CODE_AUDIT 2.12); cleared once the transfer is over.
|
|
280
|
+
params.attachCall(call);
|
|
281
|
+
try (Response response = call.execute()) {
|
|
302
282
|
rejectProtocolDowngrade(url, response);
|
|
303
283
|
if (response.code() == 416) {
|
|
304
284
|
long total = resumeMeta == null ? 0 : resumeMeta.optLong("total", 0);
|
|
@@ -335,7 +315,8 @@ class DownloadTask implements Runnable {
|
|
|
335
315
|
long contentLength = body.contentLength();
|
|
336
316
|
long totalAll;
|
|
337
317
|
if (append) {
|
|
338
|
-
totalAll = parseContentRange(
|
|
318
|
+
totalAll = HttpUtils.parseContentRange(
|
|
319
|
+
response.header("Content-Range"), resumeOffset);
|
|
339
320
|
if (totalAll < 0) {
|
|
340
321
|
// Malformed or mismatched Content-Range: treat like a
|
|
341
322
|
// stale partial (one clean retry from zero) instead of
|
|
@@ -371,6 +352,13 @@ class DownloadTask implements Runnable {
|
|
|
371
352
|
long received = 0;
|
|
372
353
|
int currentPercentage = 0;
|
|
373
354
|
long lastPostedBytes = baseOffset;
|
|
355
|
+
// Unknown length: the response-time check could only reserve the
|
|
356
|
+
// margin, so the disk is probed before any write that would run
|
|
357
|
+
// past the bytes reserved so far, each probe reserving at least
|
|
358
|
+
// the next PROBE bytes (more when one chunk is larger) — no write
|
|
359
|
+
// can ever eat into the margin (the archive cap alone is far more
|
|
360
|
+
// than the margin). A throw keeps the partial for a later attempt.
|
|
361
|
+
long freeSpaceReservedUntil = 0;
|
|
374
362
|
|
|
375
363
|
try (
|
|
376
364
|
BufferedSource source = body.source();
|
|
@@ -379,6 +367,20 @@ class DownloadTask implements Runnable {
|
|
|
379
367
|
) {
|
|
380
368
|
while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
|
|
381
369
|
received += bytesRead;
|
|
370
|
+
if (totalAll <= 0 && received > freeSpaceReservedUntil) {
|
|
371
|
+
long reserve = Math.max(
|
|
372
|
+
ArchiveLimits.UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, bytesRead);
|
|
373
|
+
freeSpaceReservedUntil = received - bytesRead + reserve;
|
|
374
|
+
try {
|
|
375
|
+
ArchiveLimits.ensureFreeSpace(writePath, reserve);
|
|
376
|
+
} catch (IOException e) {
|
|
377
|
+
// The chunk is still only buffered; closing the
|
|
378
|
+
// sink would flush it onto the disk that just
|
|
379
|
+
// failed the probe.
|
|
380
|
+
sink.buffer().clear();
|
|
381
|
+
throw e;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
382
384
|
sink.emit();
|
|
383
385
|
|
|
384
386
|
long overall = baseOffset + received;
|
|
@@ -414,6 +416,8 @@ class DownloadTask implements Runnable {
|
|
|
414
416
|
if (baseOffset + received != lastPostedBytes) {
|
|
415
417
|
postProgress(baseOffset + received, totalAll);
|
|
416
418
|
}
|
|
419
|
+
} finally {
|
|
420
|
+
params.attachCall(null);
|
|
417
421
|
}
|
|
418
422
|
return true;
|
|
419
423
|
}
|
|
@@ -576,6 +580,14 @@ class DownloadTask implements Runnable {
|
|
|
576
580
|
*/
|
|
577
581
|
private void promoteStaging() throws IOException, JSONException {
|
|
578
582
|
File work = stagingDirectory();
|
|
583
|
+
if (targetsRunningVersion()) {
|
|
584
|
+
// The process switched to this very version while the task ran
|
|
585
|
+
// (a restartApp racing the download): never rename over the
|
|
586
|
+
// live directory. Its verified install stands; staging goes.
|
|
587
|
+
ensureNotReinstallingRunningVersion();
|
|
588
|
+
UpdateFileUtils.removeDirectory(work);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
579
591
|
File bundle = new File(work, "index.bundlejs");
|
|
580
592
|
if (!bundle.isFile()) {
|
|
581
593
|
throw new IOException("bundle missing after install: " + bundle);
|
|
@@ -589,6 +601,12 @@ class DownloadTask implements Runnable {
|
|
|
589
601
|
if (!work.renameTo(params.unzipDirectory)) {
|
|
590
602
|
throw new IOException("failed to promote staging directory to " + params.unzipDirectory);
|
|
591
603
|
}
|
|
604
|
+
// The rename rewrote the versions root's entries; the record's own
|
|
605
|
+
// directory was synced by InstallRecord.write, the root was not.
|
|
606
|
+
File versionsRoot = params.unzipDirectory.getParentFile();
|
|
607
|
+
if (versionsRoot != null) {
|
|
608
|
+
InstallRecord.syncDirectory(versionsRoot);
|
|
609
|
+
}
|
|
592
610
|
}
|
|
593
611
|
|
|
594
612
|
/**
|
|
@@ -695,11 +713,18 @@ class DownloadTask implements Runnable {
|
|
|
695
713
|
}
|
|
696
714
|
|
|
697
715
|
private void doCleanUp() {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
716
|
+
UpdateContext.getInstance(context).runCleanupWithLatestState(
|
|
717
|
+
new UpdateContext.CleanupAction() {
|
|
718
|
+
@Override
|
|
719
|
+
public void run(String keepCurrent, String keepPrevious) {
|
|
720
|
+
cleanupOldEntries(
|
|
721
|
+
params.unzipDirectory.getAbsolutePath(),
|
|
722
|
+
keepCurrent,
|
|
723
|
+
keepPrevious,
|
|
724
|
+
params.maxAgeDays
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
703
728
|
);
|
|
704
729
|
}
|
|
705
730
|
|
|
@@ -768,13 +793,51 @@ class DownloadTask implements Runnable {
|
|
|
768
793
|
&& InstallRecord.isComplete(params.unzipDirectory, params.hash);
|
|
769
794
|
}
|
|
770
795
|
|
|
796
|
+
// True when this task targets the version the process is running from.
|
|
797
|
+
private boolean targetsRunningVersion() {
|
|
798
|
+
String running = UpdateContext.runningVersion();
|
|
799
|
+
return running != null && params.hash != null && running.equals(params.hash);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* The running version's directory is never replaced in place
|
|
804
|
+
* (CODE_AUDIT 2.10): images and fonts are read from it on demand, and an
|
|
805
|
+
* install from before completion records existed has no marker, so a
|
|
806
|
+
* re-download of the same hash would wipe the live directory. A running
|
|
807
|
+
* version whose record verifies against the bundle on disk needs no
|
|
808
|
+
* install and passes; anything else is refused as a file-operation
|
|
809
|
+
* failure rather than overwritten.
|
|
810
|
+
*/
|
|
811
|
+
private void ensureNotReinstallingRunningVersion() throws IOException {
|
|
812
|
+
File bundle = new File(params.unzipDirectory, "index.bundlejs");
|
|
813
|
+
if (bundle.isFile() && InstallRecord.isComplete(params.unzipDirectory, params.hash)) {
|
|
814
|
+
try {
|
|
815
|
+
InstallRecord.verifyForActivation(params.unzipDirectory, params.hash, bundle);
|
|
816
|
+
return;
|
|
817
|
+
} catch (IOException e) {
|
|
818
|
+
throw new FileOperationException("Running version " + params.hash
|
|
819
|
+
+ " cannot be reinstalled in place: " + e.getMessage());
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
throw new FileOperationException("Running version " + params.hash
|
|
823
|
+
+ " has no verifiable install record and cannot be reinstalled in place");
|
|
824
|
+
}
|
|
825
|
+
|
|
771
826
|
@Override
|
|
772
827
|
public void run() {
|
|
773
828
|
int taskType = params.type;
|
|
829
|
+
final boolean runningVersion = isPatchTask(taskType) && targetsRunningVersion();
|
|
774
830
|
final boolean alreadyCompleted = isPatchTask(taskType)
|
|
775
831
|
&& hasCompletedPatchDirectory();
|
|
776
832
|
try {
|
|
777
|
-
if (
|
|
833
|
+
if (params.isCancelled()) {
|
|
834
|
+
throw new IOException("download task cancelled before it started");
|
|
835
|
+
}
|
|
836
|
+
if (runningVersion) {
|
|
837
|
+
ensureNotReinstallingRunningVersion();
|
|
838
|
+
Log.i(UpdateContext.TAG, "download task: version " + params.hash
|
|
839
|
+
+ " is running in this process and already installed");
|
|
840
|
+
} else if (alreadyCompleted) {
|
|
778
841
|
Log.i(UpdateContext.TAG,
|
|
779
842
|
"download task: version " + params.hash + " already completed");
|
|
780
843
|
} else {
|
|
@@ -818,7 +881,8 @@ class DownloadTask implements Runnable {
|
|
|
818
881
|
Throwable classified = error;
|
|
819
882
|
if (downloadPhaseCompleted
|
|
820
883
|
&& isPatchTask(taskType)
|
|
821
|
-
&& !(error instanceof PatchFailedException)
|
|
884
|
+
&& !(error instanceof PatchFailedException)
|
|
885
|
+
&& !(error instanceof FileOperationException)) {
|
|
822
886
|
classified = new PatchFailedException(
|
|
823
887
|
String.valueOf(error.getMessage()), error);
|
|
824
888
|
}
|
|
@@ -27,4 +27,33 @@ class DownloadTaskParams {
|
|
|
27
27
|
File unzipDirectory;
|
|
28
28
|
File originDirectory;
|
|
29
29
|
UpdateContext.DownloadFileListener listener;
|
|
30
|
+
|
|
31
|
+
// Cooperative cancellation for orchestrated downloads (CODE_AUDIT 2.12):
|
|
32
|
+
// the native check cancels a task whose phase budget expired, so it
|
|
33
|
+
// neither starts late on the single download thread nor keeps
|
|
34
|
+
// transferring behind the next attempt. Guarded by this.
|
|
35
|
+
private boolean cancelled;
|
|
36
|
+
private okhttp3.Call activeCall;
|
|
37
|
+
|
|
38
|
+
synchronized void cancel() {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
if (activeCall != null) {
|
|
41
|
+
activeCall.cancel();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
synchronized boolean isCancelled() {
|
|
46
|
+
return cancelled;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Registers the transfer in flight (null clears it); a task cancelled
|
|
51
|
+
* before its transfer started cancels the call right here.
|
|
52
|
+
*/
|
|
53
|
+
synchronized void attachCall(okhttp3.Call call) {
|
|
54
|
+
activeCall = call;
|
|
55
|
+
if (cancelled && call != null) {
|
|
56
|
+
call.cancel();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
30
59
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
import java.io.IOException;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A download task refused by a local file-system rule rather than by the
|
|
7
|
+
* network or the patch itself — today: reinstalling the version this process
|
|
8
|
+
* is running from in place (DownloadTask.ensureNotReinstallingRunningVersion).
|
|
9
|
+
* UpdateModuleImpl rejects these with FILE_OPERATION_FAILED.
|
|
10
|
+
*/
|
|
11
|
+
class FileOperationException extends IOException {
|
|
12
|
+
FileOperationException(String message) {
|
|
13
|
+
super(message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure string helpers for the HTTP paths (no Android or OkHttp types), kept
|
|
5
|
+
* apart from DownloadTask / NativeCheckOrchestrator so they stay testable on
|
|
6
|
+
* a plain JVM: DownloadTask loads librnupdate.so in its static initializer
|
|
7
|
+
* and both pull in OkHttp.
|
|
8
|
+
*/
|
|
9
|
+
final class HttpUtils {
|
|
10
|
+
private HttpUtils() {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** True for an "https:" URL, case-insensitively; false for anything else (incl. null). */
|
|
14
|
+
static boolean isHttpsUrl(String url) {
|
|
15
|
+
return url != null && url.regionMatches(true, 0, "https://", 0, 8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Strips every trailing slash so "<base>/checkUpdate/<appKey>" never doubles one. */
|
|
19
|
+
static String normalizeEndpointBase(String base) {
|
|
20
|
+
while (base.endsWith("/")) {
|
|
21
|
+
base = base.substring(0, base.length() - 1);
|
|
22
|
+
}
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// "bytes <start>-<end>/<total>". Returns the total (0 when "*"), or -1
|
|
27
|
+
// when the header is missing/malformed, the start does not match the
|
|
28
|
+
// local partial, or the range and total contradict each other (end
|
|
29
|
+
// before start, or a numeric total not beyond the end) — either way the
|
|
30
|
+
// appended bytes could not be trusted. A total of 0 would otherwise pass
|
|
31
|
+
// as "unknown" and skip the final size check (RFC 9110 §14.4: the
|
|
32
|
+
// complete length must exceed the last position).
|
|
33
|
+
static long parseContentRange(String header, long expectedStart) {
|
|
34
|
+
if (header == null || !header.startsWith("bytes ")) {
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
String range = header.substring("bytes ".length()).trim();
|
|
39
|
+
int slash = range.indexOf('/');
|
|
40
|
+
int dash = range.indexOf('-');
|
|
41
|
+
if (slash < 0 || dash < 0 || dash > slash) {
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
long start = Long.parseLong(range.substring(0, dash).trim());
|
|
45
|
+
long end = Long.parseLong(range.substring(dash + 1, slash).trim());
|
|
46
|
+
if (start != expectedStart || end < start) {
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
String totalPart = range.substring(slash + 1).trim();
|
|
50
|
+
if (totalPart.equals("*")) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
long total = Long.parseLong(totalPart);
|
|
54
|
+
return total > end ? total : -1;
|
|
55
|
+
} catch (NumberFormatException e) {
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package cn.reactnative.modules.update;
|
|
2
2
|
|
|
3
|
+
import android.os.Build;
|
|
4
|
+
import android.util.Log;
|
|
3
5
|
import java.io.File;
|
|
4
6
|
import java.io.FileInputStream;
|
|
5
7
|
import java.io.FileOutputStream;
|
|
@@ -39,7 +41,11 @@ final class InstallRecord {
|
|
|
39
41
|
return record.toString();
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Writes the record and fsyncs it, then the directory holding it: the
|
|
46
|
+
* rename that follows must find the record on disk, and the new
|
|
47
|
+
* directory entry is only durable once the directory itself is synced.
|
|
48
|
+
*/
|
|
43
49
|
static void write(File versionDir, String recordJson) throws IOException {
|
|
44
50
|
File file = new File(versionDir, FILE_NAME);
|
|
45
51
|
try (FileOutputStream out = new FileOutputStream(file)) {
|
|
@@ -47,6 +53,26 @@ final class InstallRecord {
|
|
|
47
53
|
out.flush();
|
|
48
54
|
out.getFD().sync();
|
|
49
55
|
}
|
|
56
|
+
syncDirectory(versionDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// java.nio.file (the only way to open a directory for fsync from Java)
|
|
60
|
+
// is API 26+ on Android; below that the file fsync above is all there
|
|
61
|
+
// is. Best-effort by design: a file system that refuses to open a
|
|
62
|
+
// directory for reading must not fail the install. Also called by the
|
|
63
|
+
// installer on the versions root after the staging -> version rename:
|
|
64
|
+
// that rename changes the parent's entries, which only the parent's own
|
|
65
|
+
// sync makes durable.
|
|
66
|
+
static void syncDirectory(File dir) {
|
|
67
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try (java.nio.channels.FileChannel channel = java.nio.channels.FileChannel.open(
|
|
71
|
+
dir.toPath(), java.nio.file.StandardOpenOption.READ)) {
|
|
72
|
+
channel.force(true);
|
|
73
|
+
} catch (Throwable e) {
|
|
74
|
+
Log.w("pushy", "Directory fsync skipped for " + dir + ": " + e);
|
|
75
|
+
}
|
|
50
76
|
}
|
|
51
77
|
|
|
52
78
|
/**
|
|
@@ -2,6 +2,10 @@ package cn.reactnative.modules.update;
|
|
|
2
2
|
|
|
3
3
|
import android.os.Build;
|
|
4
4
|
import android.util.Log;
|
|
5
|
+
import androidx.annotation.Nullable;
|
|
6
|
+
import java.io.IOException;
|
|
7
|
+
import java.nio.charset.Charset;
|
|
8
|
+
import java.nio.charset.StandardCharsets;
|
|
5
9
|
import java.util.HashSet;
|
|
6
10
|
import java.util.concurrent.CountDownLatch;
|
|
7
11
|
import java.util.concurrent.TimeUnit;
|
|
@@ -11,6 +15,8 @@ import okhttp3.OkHttpClient;
|
|
|
11
15
|
import okhttp3.Request;
|
|
12
16
|
import okhttp3.RequestBody;
|
|
13
17
|
import okhttp3.Response;
|
|
18
|
+
import okhttp3.ResponseBody;
|
|
19
|
+
import okio.BufferedSource;
|
|
14
20
|
import org.json.JSONArray;
|
|
15
21
|
import org.json.JSONException;
|
|
16
22
|
import org.json.JSONObject;
|
|
@@ -35,6 +41,9 @@ final class NativeCheckOrchestrator {
|
|
|
35
41
|
static final String KEY_ROUND_INCOMPLETE = "nativeCheckIncomplete";
|
|
36
42
|
private static final int MAX_CHECK_HTTP_ATTEMPTS = 8;
|
|
37
43
|
private static final long DOWNLOAD_PHASE_TIMEOUT_SECONDS = 600;
|
|
44
|
+
// The check response (and a remote queryUrls list) is a small JSON
|
|
45
|
+
// document; anything bigger is a broken or hijacked endpoint.
|
|
46
|
+
private static final long MAX_CHECK_RESPONSE_BYTES = 1024 * 1024;
|
|
38
47
|
|
|
39
48
|
private static final AtomicBoolean scheduled = new AtomicBoolean(false);
|
|
40
49
|
// One round per process, whoever starts it first — the delayed cold-start
|
|
@@ -177,7 +186,8 @@ final class NativeCheckOrchestrator {
|
|
|
177
186
|
|
|
178
187
|
// The alert-strategy variant of the §10.7 hole: the round downloaded a
|
|
179
188
|
// fix but deferred activation to JS, and JS is now dead. Activation is
|
|
180
|
-
// local and bounded (a state switch under the
|
|
189
|
+
// local and bounded (the bundle digest, then a state switch under the
|
|
190
|
+
// commit lock).
|
|
181
191
|
private static void activatePendingVersion(UpdateContext context) {
|
|
182
192
|
String hash = unactivatedHash;
|
|
183
193
|
if (hash == null) {
|
|
@@ -341,7 +351,7 @@ final class NativeCheckOrchestrator {
|
|
|
341
351
|
return;
|
|
342
352
|
}
|
|
343
353
|
String hash = decision.optString("hash", "");
|
|
344
|
-
if (!
|
|
354
|
+
if (!UpdateFileUtils.isSafePathComponent(hash)) {
|
|
345
355
|
return;
|
|
346
356
|
}
|
|
347
357
|
|
|
@@ -467,16 +477,43 @@ final class NativeCheckOrchestrator {
|
|
|
467
477
|
// Same rule as the artifact download: an https endpoint that
|
|
468
478
|
// redirects to plaintext http is a failed endpoint.
|
|
469
479
|
DownloadTask.rejectProtocolDowngrade(url, response);
|
|
470
|
-
|
|
480
|
+
ResponseBody body = response.body();
|
|
481
|
+
if (!response.isSuccessful() || body == null) {
|
|
471
482
|
return null;
|
|
472
483
|
}
|
|
473
|
-
return
|
|
484
|
+
return readBoundedBody(body);
|
|
474
485
|
}
|
|
475
486
|
} catch (Exception e) {
|
|
487
|
+
// One line per failed endpoint; the fallback chain is otherwise
|
|
488
|
+
// invisible in the field.
|
|
489
|
+
Log.w(UpdateContext.TAG, "native check: request failed with "
|
|
490
|
+
+ e.getClass().getName() + ": " + e.getMessage());
|
|
476
491
|
return null;
|
|
477
492
|
}
|
|
478
493
|
}
|
|
479
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Reads the body with a hard cap (CODE_AUDIT 2.7): a hijacked endpoint
|
|
497
|
+
* must not be able to OOM this thread with an unbounded string. Over the
|
|
498
|
+
* cap counts as a failed endpoint (null), like any other bad response.
|
|
499
|
+
*/
|
|
500
|
+
@Nullable
|
|
501
|
+
private static String readBoundedBody(ResponseBody body) throws IOException {
|
|
502
|
+
if (body.contentLength() > MAX_CHECK_RESPONSE_BYTES) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
BufferedSource source = body.source();
|
|
506
|
+
if (source.request(MAX_CHECK_RESPONSE_BYTES + 1)) {
|
|
507
|
+
Log.w(UpdateContext.TAG, "native check: response exceeds "
|
|
508
|
+
+ MAX_CHECK_RESPONSE_BYTES + " bytes, ignoring endpoint");
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
MediaType contentType = body.contentType();
|
|
512
|
+
Charset charset = contentType == null
|
|
513
|
+
? null : contentType.charset(StandardCharsets.UTF_8);
|
|
514
|
+
return source.readString(charset == null ? StandardCharsets.UTF_8 : charset);
|
|
515
|
+
}
|
|
516
|
+
|
|
480
517
|
// Shared schema rule (update_flow_core::IsValidCheckResponse): a 200 with
|
|
481
518
|
// `{"error": ...}` is a failed endpoint, not a verdict, and must not stop
|
|
482
519
|
// the endpoint fallback.
|
|
@@ -484,13 +521,6 @@ final class NativeCheckOrchestrator {
|
|
|
484
521
|
return responseText != null && NativeUpdateFlow.isValidCheckResponse(responseText);
|
|
485
522
|
}
|
|
486
523
|
|
|
487
|
-
private static String normalizeEndpointBase(String base) {
|
|
488
|
-
while (base.endsWith("/")) {
|
|
489
|
-
base = base.substring(0, base.length() - 1);
|
|
490
|
-
}
|
|
491
|
-
return base;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
524
|
/**
|
|
495
525
|
* Sequential fallback over the ordered candidates (§5.1): one request at
|
|
496
526
|
* a time with its own timeout; after the configured round fails,
|
|
@@ -513,7 +543,7 @@ final class NativeCheckOrchestrator {
|
|
|
513
543
|
HashSet<String> tried = new HashSet<>();
|
|
514
544
|
int httpAttempts = 0;
|
|
515
545
|
for (int i = 0; i < ordered.length(); i++) {
|
|
516
|
-
String base = normalizeEndpointBase(ordered.optString(i, ""));
|
|
546
|
+
String base = HttpUtils.normalizeEndpointBase(ordered.optString(i, ""));
|
|
517
547
|
if (base.isEmpty() || !tried.add(base)) {
|
|
518
548
|
continue;
|
|
519
549
|
}
|
|
@@ -549,7 +579,7 @@ final class NativeCheckOrchestrator {
|
|
|
549
579
|
continue;
|
|
550
580
|
}
|
|
551
581
|
for (int j = 0; j < remote.length(); j++) {
|
|
552
|
-
String base = normalizeEndpointBase(remote.optString(j, ""));
|
|
582
|
+
String base = HttpUtils.normalizeEndpointBase(remote.optString(j, ""));
|
|
553
583
|
if (base.isEmpty() || tried.contains(base)) {
|
|
554
584
|
continue;
|
|
555
585
|
}
|
|
@@ -644,20 +674,28 @@ final class NativeCheckOrchestrator {
|
|
|
644
674
|
latch.countDown();
|
|
645
675
|
}
|
|
646
676
|
};
|
|
677
|
+
DownloadTaskParams task;
|
|
647
678
|
if ("diff".equals(type)) {
|
|
648
|
-
context.downloadPatchFromPpk(
|
|
679
|
+
task = context.downloadPatchFromPpk(
|
|
649
680
|
url, hash, originHash, listener, deadlineNanos);
|
|
650
681
|
} else if ("pdiff".equals(type)) {
|
|
651
|
-
context.downloadPatchFromApk(
|
|
682
|
+
task = context.downloadPatchFromApk(
|
|
652
683
|
url, hash, listener, deadlineNanos);
|
|
653
684
|
} else {
|
|
654
|
-
context.downloadFullUpdate(
|
|
685
|
+
task = context.downloadFullUpdate(
|
|
655
686
|
url, hash, listener, deadlineNanos);
|
|
656
687
|
}
|
|
657
688
|
try {
|
|
658
689
|
if (!latch.await(remainingNanos, TimeUnit.NANOSECONDS)) {
|
|
659
690
|
Log.w(UpdateContext.TAG,
|
|
660
691
|
"native check: download phase timed out during " + type);
|
|
692
|
+
// The task shares one download thread with the next
|
|
693
|
+
// attempt: cancel its transfer (or keep it from
|
|
694
|
+
// starting) instead of queueing behind it
|
|
695
|
+
// (CODE_AUDIT 2.12).
|
|
696
|
+
if (task != null) {
|
|
697
|
+
task.cancel();
|
|
698
|
+
}
|
|
661
699
|
if (isFullAttempt) {
|
|
662
700
|
return false;
|
|
663
701
|
}
|
|
@@ -665,6 +703,9 @@ final class NativeCheckOrchestrator {
|
|
|
665
703
|
}
|
|
666
704
|
} catch (InterruptedException e) {
|
|
667
705
|
Thread.currentThread().interrupt();
|
|
706
|
+
if (task != null) {
|
|
707
|
+
task.cancel();
|
|
708
|
+
}
|
|
668
709
|
return false;
|
|
669
710
|
}
|
|
670
711
|
if (succeeded.get()) {
|
|
@@ -18,18 +18,35 @@ final class ReactReloadManager {
|
|
|
18
18
|
private ReactReloadManager() {
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
/**
|
|
22
|
+
* First half of a restart, for the state serial thread: activate the
|
|
23
|
+
* requested version (bundle digest + commit) and resolve the bundle to
|
|
24
|
+
* load. Nothing here touches React, so nothing here needs the UI thread
|
|
25
|
+
* (CODE_AUDIT 2.2). Returns the bundle path, or null for the packaged
|
|
26
|
+
* bundle.
|
|
27
|
+
*/
|
|
28
|
+
@Nullable
|
|
29
|
+
static String prepareRestart(UpdateContext updateContext, @Nullable String hash) {
|
|
26
30
|
if (hash != null) {
|
|
27
31
|
updateContext.switchVersion(hash);
|
|
28
32
|
}
|
|
33
|
+
return updateContext.getBundleUrl();
|
|
34
|
+
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Second half, for the UI thread: point the running React instance at
|
|
38
|
+
* {@code updateBundlePath} and reload it. Throws when no supported
|
|
39
|
+
* injection worked — the caller rejects RESTART_FAILED. There is
|
|
40
|
+
* deliberately no Activity.recreate() fallback: it would reload the
|
|
41
|
+
* previous bundle while the promise reported success (CODE_AUDIT 2.9).
|
|
42
|
+
*/
|
|
43
|
+
static void reload(
|
|
44
|
+
UpdateContext updateContext,
|
|
45
|
+
ReactApplicationContext reactContext,
|
|
46
|
+
@Nullable String updateBundlePath
|
|
47
|
+
) throws Throwable {
|
|
30
48
|
Context application = reactContext.getApplicationContext();
|
|
31
49
|
Activity currentActivity = reactContext.getCurrentActivity();
|
|
32
|
-
String updateBundlePath = updateContext.getBundleUrl();
|
|
33
50
|
|
|
34
51
|
boolean newArchitectureEnabled = isNewArchitectureEnabled(application);
|
|
35
52
|
Object reactHost = newArchitectureEnabled ? getReactHost(currentActivity, application) : null;
|
|
@@ -38,53 +55,28 @@ final class ReactReloadManager {
|
|
|
38
55
|
reloadReactHost(reactHost, createBundleLoader(application, updateBundlePath, true));
|
|
39
56
|
return;
|
|
40
57
|
} catch (Throwable err) {
|
|
41
|
-
Log.e(UpdateContext.TAG,
|
|
58
|
+
Log.e(UpdateContext.TAG,
|
|
59
|
+
"Failed to reload via ReactHost, trying ReactInstanceManager", err);
|
|
42
60
|
}
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
JSBundleLoader loader = createBundleLoader(application, updateBundlePath, false);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
resolveReactInstanceManager(updateContext, application);
|
|
64
|
+
ReactInstanceManager instanceManager =
|
|
65
|
+
resolveReactInstanceManager(updateContext, application);
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
jsBundleField.setAccessible(true);
|
|
57
|
-
jsBundleField.set(instanceManager, updateContext.getBundleUrl());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
instanceManager.recreateReactContextInBackground();
|
|
67
|
+
// Java (mBundleLoader / mJSBundleFile) and Kotlin (bundleLoader /
|
|
68
|
+
// jsBundleFile) spellings of the private fields are both tried.
|
|
69
|
+
try {
|
|
70
|
+
Field loadField = getCompatibleField(instanceManager.getClass(), "bundleLoader");
|
|
71
|
+
loadField.setAccessible(true);
|
|
72
|
+
loadField.set(instanceManager, loader);
|
|
61
73
|
} catch (Throwable err) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
if (!newArchitectureEnabled) {
|
|
68
|
-
throw err;
|
|
69
|
-
}
|
|
70
|
-
Object currentReactHost = getReactHost(currentActivity, application);
|
|
71
|
-
if (currentReactHost == null) {
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
reloadReactHost(
|
|
75
|
-
currentReactHost,
|
|
76
|
-
createBundleLoader(application, updateBundlePath, true)
|
|
77
|
-
);
|
|
78
|
-
} catch (Throwable reloadError) {
|
|
79
|
-
Log.e(
|
|
80
|
-
UpdateContext.TAG,
|
|
81
|
-
"Failed to reload via ReactHost, falling back to Activity.recreate() "
|
|
82
|
-
+ "(this may load the previous bundle until next cold start)",
|
|
83
|
-
reloadError
|
|
84
|
-
);
|
|
85
|
-
currentActivity.recreate();
|
|
86
|
-
}
|
|
74
|
+
Field jsBundleField = getCompatibleField(instanceManager.getClass(), "jsBundleFile");
|
|
75
|
+
jsBundleField.setAccessible(true);
|
|
76
|
+
jsBundleField.set(instanceManager, updateBundlePath);
|
|
87
77
|
}
|
|
78
|
+
|
|
79
|
+
instanceManager.recreateReactContextInBackground();
|
|
88
80
|
}
|
|
89
81
|
|
|
90
82
|
private static Field getCompatibleField(Class<?> clazz, String fieldName)
|