react-native-update 10.59.1 → 10.60.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.
Files changed (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2628
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +74 -83
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -13,13 +13,15 @@ class DownloadTaskParams {
13
13
  static final int TASK_TYPE_PATCH_FROM_PPK = 3;
14
14
  static final int TASK_TYPE_PLAIN_DOWNLOAD = 4;
15
15
 
16
+ static final int DEFAULT_MAX_AGE_DAYS = 3;
17
+
16
18
 
17
19
  int type;
18
20
  String url;
19
21
  String hash;
20
22
  String originHash;
21
23
  // TASK_TYPE_CLEANUP only: entries younger than this survive; 0 = delete all
22
- int maxAgeDays = 3;
24
+ int maxAgeDays = DEFAULT_MAX_AGE_DAYS;
23
25
  // Absolute System.nanoTime deadline for orchestrated cold-start downloads;
24
26
  // 0 keeps the normal public API's 10-minute per-call timeout.
25
27
  long deadlineNanos;
@@ -25,4 +25,31 @@ final class ErrorCodes {
25
25
 
26
26
  private ErrorCodes() {
27
27
  }
28
+
29
+ /**
30
+ * Rejection code of a failed download task, from DownloadTask's
31
+ * classification: post-download failures (unzip / hdiff / resource copy,
32
+ * incl. copiesCrc verification) are PATCH_FAILED so telemetry can
33
+ * separate patch health from network health; a local refusal
34
+ * (reinstalling the running version in place) is FILE_OPERATION_FAILED.
35
+ */
36
+ static String forDownloadFailure(Throwable error) {
37
+ if (error instanceof PatchFailedException) {
38
+ return DELTA_FAILED;
39
+ }
40
+ if (error instanceof FileOperationException) {
41
+ return FILE_OPERATION_FAILED;
42
+ }
43
+ return DOWNLOAD_FAILED;
44
+ }
45
+
46
+ /**
47
+ * Whether another mirror of the same artifact can change the outcome
48
+ * (mirrors update_flow_core::IsMirrorRetryableCode and the JS
49
+ * isMirrorRetryableCode). A patch failure cannot: the bytes arrived and
50
+ * every mirror serves the same artifact.
51
+ */
52
+ static boolean isMirrorRetryable(String code) {
53
+ return !DELTA_FAILED.equals(code);
54
+ }
28
55
  }
@@ -6,7 +6,7 @@ import java.io.IOException;
6
6
  * A download task refused by a local file-system rule rather than by the
7
7
  * network or the patch itself — today: reinstalling the version this process
8
8
  * is running from in place (DownloadTask.ensureNotReinstallingRunningVersion).
9
- * UpdateModuleImpl rejects these with FILE_OPERATION_FAILED.
9
+ * UpdateModule rejects these with FILE_OPERATION_FAILED.
10
10
  */
11
11
  class FileOperationException extends IOException {
12
12
  FileOperationException(String message) {
@@ -7,19 +7,43 @@ package cn.reactnative.modules.update;
7
7
  * A null return means the input did not parse; callers skip the check round.
8
8
  */
9
9
  final class FlowBridge {
10
- static {
11
- NativeCore.ensureLoaded();
12
- }
13
-
14
10
  private FlowBridge() {
15
11
  }
16
12
 
17
- static native String buildRequestBody(String inputJson);
13
+ // The cold-start round (cpp/update_flow_core/native_round.h), shared with
14
+ // iOS and Harmony. envJson describes this launch (see native_round.h).
18
15
 
19
- static native String orderEndpointCandidates(String endpointsJson, double randomSample);
16
+ /** {"body": request text, "query": first query state}; null when not runnable. */
17
+ static native String prepareRound(String configJson, String envJson, double randomSample);
20
18
 
21
- static native boolean isValidResponse(String responseText);
19
+ /**
20
+ * The next endpoint-query state: "url" (+ "post") to request next,
21
+ * "found" when the text passed in is the response, neither when the
22
+ * query is over. responseText is null for a failed (or no) request.
23
+ */
24
+ static native String nextQueryStep(String stateJson, String responseText);
22
25
 
26
+ /** The decision with planned attempts, hashInfo and the response-cache text. */
23
27
  static native String handleResponse(
24
- String responseText, String identityJson, String afterDownload);
28
+ String responseText, String configJson, String envJson, String requestBody,
29
+ long responseAtSeconds);
30
+
31
+ // Persisted-config layer (cpp/update_flow_core/native_config.h), shared
32
+ // with iOS and Harmony.
33
+
34
+ /** Contract C1: same config in another serialization. Null is never equivalent. */
35
+ static native boolean configsEquivalent(String a, String b);
36
+
37
+ /**
38
+ * The run gate: {"appKey": trimmed} when the config may run a round,
39
+ * otherwise {"status", "reason"}. Null config text = not configured.
40
+ */
41
+ static native String runnableConfig(String configJson);
42
+
43
+ /**
44
+ * Validates host configure() options (a JSON object) into the config to
45
+ * persist. Throws IllegalArgumentException("Invalid native configuration:
46
+ * ...") for options the host must fix.
47
+ */
48
+ static native String normalizeConfiguration(String optionsJson);
25
49
  }
@@ -0,0 +1,63 @@
1
+ package cn.reactnative.modules.update;
2
+
3
+ import java.io.IOException;
4
+ import java.util.concurrent.TimeUnit;
5
+ import okhttp3.OkHttpClient;
6
+ import okhttp3.Response;
7
+
8
+ /**
9
+ * The one OkHttpClient the SDK owns. Callers derive their timeout variants
10
+ * with newBuilder(), which shares the connection pool and dispatcher threads
11
+ * instead of each client growing its own.
12
+ */
13
+ final class HttpClients {
14
+ private HttpClients() {
15
+ }
16
+
17
+ private static final class Holder {
18
+ static final OkHttpClient BASE = new OkHttpClient();
19
+ }
20
+
21
+ static OkHttpClient base() {
22
+ return Holder.BASE;
23
+ }
24
+
25
+ /**
26
+ * {@code client} with its call timeout capped to the time left until the
27
+ * absolute System.nanoTime {@code deadlineNanos} (0: no deadline, the
28
+ * client as is), so an orchestrated request never outlives its budget.
29
+ * Throws when the deadline already passed.
30
+ */
31
+ static OkHttpClient withDeadline(OkHttpClient client, long deadlineNanos)
32
+ throws IOException {
33
+ if (deadlineNanos <= 0) {
34
+ return client;
35
+ }
36
+ long remainingNanos = deadlineNanos - System.nanoTime();
37
+ if (remainingNanos <= 0) {
38
+ throw new IOException("Request deadline expired before start");
39
+ }
40
+ long remainingMillis = Math.max(1L, TimeUnit.NANOSECONDS.toMillis(remainingNanos));
41
+ int callTimeoutMillis = client.callTimeoutMillis();
42
+ if (callTimeoutMillis > 0 && callTimeoutMillis <= remainingMillis) {
43
+ return client;
44
+ }
45
+ return client.newBuilder()
46
+ .callTimeout(remainingMillis, TimeUnit.MILLISECONDS)
47
+ .build();
48
+ }
49
+
50
+ /**
51
+ * An https URL must stay on https through every redirect: TLS is what
52
+ * authenticates the package and the check response. OkHttp follows
53
+ * cross-scheme redirects by default, so the final request is checked
54
+ * here (the redirected bytes are discarded).
55
+ */
56
+ static void rejectProtocolDowngrade(String requestedUrl, Response response)
57
+ throws IOException {
58
+ if (HttpUtils.isHttpsUrl(requestedUrl) && !response.request().url().isHttps()) {
59
+ throw new IOException(
60
+ "https request redirected to plaintext http: " + response.request().url());
61
+ }
62
+ }
63
+ }
@@ -15,17 +15,6 @@ final class HttpUtils {
15
15
  return url != null && url.regionMatches(true, 0, "https://", 0, 8);
16
16
  }
17
17
 
18
- /** Request path appended to an endpoint base, kept out of the plain string table. */
19
- static final String QUERY_PATH = Texts.reveal("7514fcd4ad805d55263e08fc99");
20
-
21
- /** Strips every trailing slash so "<base><QUERY_PATH><appKey>" never doubles one. */
22
- static String normalizeEndpointBase(String base) {
23
- while (base.endsWith("/")) {
24
- base = base.substring(0, base.length() - 1);
25
- }
26
- return base;
27
- }
28
-
29
18
  // "bytes <start>-<end>/<total>". Returns the total (0 when "*"), or -1
30
19
  // when the header is missing/malformed, the start does not match the
31
20
  // local partial, or the range and total contradict each other (end
@@ -1,151 +1,101 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
- import android.os.Build;
4
- import android.util.Log;
5
3
  import java.io.File;
6
- import java.io.FileInputStream;
7
- import java.io.FileOutputStream;
8
4
  import java.io.IOException;
9
- import java.nio.charset.StandardCharsets;
10
- import org.json.JSONException;
11
- import org.json.JSONObject;
12
5
 
13
6
  /**
14
- * Java mirror of cpp/patch_core/install_record.h — keep in sync by hand.
15
7
  * The completion record written as the last step of a two-phase install and
16
- * read back before a version directory is trusted.
8
+ * read back before a version directory is trusted. The logic is the shared
9
+ * core's (cpp/patch_core/install_record.h, over JNI); only the paths are
10
+ * built here. Every call does file IO and checkActivation hashes the whole
11
+ * bundle: callers run on worker threads (download tasks, the state serial
12
+ * thread, the sync coordinator), except the launch-time reads of
13
+ * isInstalled / recordedBundleSha256, which only stat and read the record.
17
14
  */
18
15
  final class InstallRecord {
19
- static final int SCHEMA = 1;
20
- static final String FILE_NAME = ".pushy-complete";
16
+ // Mirrors install_record.h kStagingSuffix (scripts/check-constant-parity.js).
21
17
  static final String STAGING_SUFFIX = ".staging";
18
+ /** The JS bundle inside a version directory. */
19
+ static final String BUNDLE_FILE_NAME = "index.bundlejs";
22
20
 
23
21
  private InstallRecord() {
24
22
  }
25
23
 
26
- static File stagingDirectoryFor(File versionDir) {
27
- return new File(versionDir.getPath() + STAGING_SUFFIX);
24
+ static File bundleFile(File versionDir) {
25
+ return new File(versionDir, BUNDLE_FILE_NAME);
28
26
  }
29
27
 
30
- static String build(String versionHash, String bundleSha256, String artifactSha256)
31
- throws JSONException {
32
- JSONObject record = new JSONObject();
33
- record.put("schema", SCHEMA);
34
- record.put("versionHash", versionHash);
35
- if (bundleSha256 != null && !bundleSha256.isEmpty()) {
36
- record.put("bundleSha256", bundleSha256);
37
- }
38
- if (artifactSha256 != null && !artifactSha256.isEmpty()) {
39
- record.put("artifactSha256", artifactSha256);
40
- }
41
- return record.toString();
28
+ static File stagingDirectoryFor(File versionDir) {
29
+ return new File(versionDir.getPath() + STAGING_SUFFIX);
42
30
  }
43
31
 
44
32
  /**
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.
33
+ * Launch / dedup presence check: the bundle exists and the record names
34
+ * this version (or is the legacy empty marker). No digest work.
48
35
  */
49
- static void write(File versionDir, String recordJson) throws IOException {
50
- File file = new File(versionDir, FILE_NAME);
51
- try (FileOutputStream out = new FileOutputStream(file)) {
52
- out.write(recordJson.getBytes(StandardCharsets.UTF_8));
53
- out.flush();
54
- out.getFD().sync();
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
- }
36
+ static boolean isInstalled(File versionDir, String versionHash) {
37
+ return isComplete(versionDir.getPath(), versionHash, BUNDLE_FILE_NAME);
76
38
  }
77
39
 
78
40
  /**
79
- * Parsed record, or null when the file is absent or malformed. An empty
80
- * file (legacy marker) yields an empty JSONObject.
41
+ * Activation check: the bundle exists and its bytes match the record's
42
+ * mandatory digest (the legacy empty marker has none). A directory
43
+ * without a record passes only when {@code grandfathered} (it is the
44
+ * persisted current or last version). Throws with the reason otherwise.
81
45
  */
82
- static JSONObject read(File versionDir) {
83
- File file = new File(versionDir, FILE_NAME);
84
- if (!file.isFile()) {
85
- return null;
86
- }
87
- if (file.length() == 0) {
88
- return new JSONObject();
89
- }
90
- if (file.length() > 64 * 1024) {
91
- return null;
92
- }
93
- try (FileInputStream in = new FileInputStream(file)) {
94
- byte[] bytes = new byte[(int) file.length()];
95
- int offset = 0;
96
- while (offset < bytes.length) {
97
- int read = in.read(bytes, offset, bytes.length - offset);
98
- if (read < 0) {
99
- break;
100
- }
101
- offset += read;
102
- }
103
- return new JSONObject(new String(bytes, 0, offset, StandardCharsets.UTF_8));
104
- } catch (IOException | JSONException e) {
105
- return null;
46
+ static void verifyInstalled(File versionDir, String versionHash, boolean grandfathered)
47
+ throws IOException {
48
+ String reason = checkActivation(
49
+ versionDir.getPath(), versionHash, BUNDLE_FILE_NAME, grandfathered);
50
+ if (reason != null) {
51
+ throw new IOException(reason);
106
52
  }
107
53
  }
108
54
 
109
55
  /**
110
- * Presence check: the record exists and, unless legacy-empty, names this
111
- * version. No digest work — this runs on launch / dedup paths.
56
+ * Writes the record (with the staged bundle's digest: knownBundleSha256
57
+ * when the bundle was hashed while extracted, else hashed now) into the
58
+ * staging directory, atomically and durably.
112
59
  */
113
- static boolean isComplete(File versionDir, String versionHash) {
114
- JSONObject record = read(versionDir);
115
- if (record == null) {
116
- return false;
60
+ static void seal(
61
+ File stagingDir, String versionHash, String artifactSha256, String knownBundleSha256
62
+ ) throws IOException {
63
+ String reason = sealStaging(stagingDir.getPath(), versionHash, BUNDLE_FILE_NAME,
64
+ artifactSha256, knownBundleSha256);
65
+ if (reason != null) {
66
+ throw new IOException(reason);
117
67
  }
118
- if (record.length() == 0) {
119
- return true;
120
- }
121
- return record.optInt("schema", 0) == SCHEMA
122
- && versionHash.equals(record.optString("versionHash", ""));
123
68
  }
124
69
 
125
- /**
126
- * Activation check: re-hashes the bundle when the record carries a digest.
127
- * Throws with the reason when the directory must not be activated.
128
- */
129
- static void verifyForActivation(File versionDir, String versionHash, File bundleFile)
130
- throws IOException {
131
- JSONObject record = read(versionDir);
132
- if (record == null) {
133
- throw new IOException("Bundle version " + versionHash + " has no valid completion record.");
134
- }
135
- if (record.length() == 0) {
136
- return;
137
- }
138
- if (record.optInt("schema", 0) != SCHEMA
139
- || !versionHash.equals(record.optString("versionHash", ""))) {
140
- throw new IOException("Bundle version " + versionHash + " completion record mismatch.");
141
- }
142
- String expected = record.optString("bundleSha256", "");
143
- if (expected.isEmpty()) {
144
- return;
145
- }
146
- String actual = UpdateFileUtils.sha256Hex(bundleFile);
147
- if (!expected.equalsIgnoreCase(actual)) {
148
- throw new IOException("Bundle version " + versionHash + " bundle digest mismatch.");
70
+ /** Renames the sealed staging directory to the (absent) version directory. */
71
+ static void promote(File stagingDir, File versionDir) throws IOException {
72
+ String reason = promoteStaging(stagingDir.getPath(), versionDir.getPath());
73
+ if (reason != null) {
74
+ throw new IOException(reason);
149
75
  }
150
76
  }
77
+
78
+ /** The record's bundleSha256 (crash attribution); "" when there is none. */
79
+ static String recordedBundleSha256(File versionDir) {
80
+ String digest = bundleSha256Of(versionDir.getPath());
81
+ return digest == null ? "" : digest;
82
+ }
83
+
84
+ private static native boolean isComplete(
85
+ String versionDir, String versionHash, String bundleFileName);
86
+
87
+ // These return null on success, the reason otherwise.
88
+ private static native String checkActivation(
89
+ String versionDir, String versionHash, String bundleFileName, boolean grandfathered);
90
+
91
+ private static native String sealStaging(
92
+ String stagingDir,
93
+ String versionHash,
94
+ String bundleFileName,
95
+ String artifactSha256,
96
+ String knownBundleSha256);
97
+
98
+ private static native String promoteStaging(String stagingDir, String versionDir);
99
+
100
+ private static native String bundleSha256Of(String versionDir);
151
101
  }
@@ -1,5 +1,14 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
+ /**
4
+ * Loads librnpushy.so. Its JNI_OnLoad registers the natives of every class
5
+ * (cpp/android_jni/jni_registration.cpp), so the library only has to be
6
+ * loaded before the first native call: UpdateContext's static initializer
7
+ * does that, and DownloadTask, FlowBridge, InstallRecord and
8
+ * supportedDiffVersion are only ever reached through an UpdateContext
9
+ * instance — except PushyRuntime.configure, which validates options before
10
+ * touching UpdateContext and so loads the library itself.
11
+ */
3
12
  final class NativeCore {
4
13
  private static boolean loaded = false;
5
14
 
@@ -30,7 +39,6 @@ final class NativeCore {
30
39
  * 服务端按能力门控下发。
31
40
  */
32
41
  static int supportedDiffVersion() {
33
- ensureLoaded();
34
42
  return getSupportedDiffVersion();
35
43
  }
36
44
 
@@ -5,7 +5,7 @@ import java.io.IOException;
5
5
  /**
6
6
  * A download task failure that happened after the artifact was fully
7
7
  * downloaded — unzip, hdiff apply, or bundled-resource copy (including the
8
- * copiesCrc content verification). UpdateModuleImpl rejects these with
8
+ * copiesCrc content verification). UpdateModule rejects these with
9
9
  * PATCH_FAILED instead of DOWNLOAD_FAILED so the JS layer and server-side
10
10
  * telemetry can separate patch health from network health.
11
11
  */
@@ -7,8 +7,6 @@ import android.util.Log;
7
7
  import androidx.annotation.Nullable;
8
8
  import org.json.JSONObject;
9
9
  import java.util.concurrent.Executor;
10
- import java.util.concurrent.Executors;
11
- import java.util.concurrent.ThreadFactory;
12
10
 
13
11
  /** Bridge-free native configuration and update APIs. */
14
12
  public final class PushyRuntime {
@@ -19,14 +17,7 @@ public final class PushyRuntime {
19
17
 
20
18
  // A single waiting worker, not one thread per caller. The actual round is
21
19
  // shared with cold start and crash rescue by SyncCoordinator.
22
- private static final Executor WORKER = Executors.newSingleThreadExecutor(new ThreadFactory() {
23
- @Override
24
- public Thread newThread(Runnable runnable) {
25
- Thread thread = new Thread(runnable, "pushy-host-sync");
26
- thread.setDaemon(true);
27
- return thread;
28
- }
29
- });
20
+ private static final Executor WORKER = Threads.single("pushy-host-sync", true);
30
21
 
31
22
  public interface ConfigurationCallback {
32
23
  /** Main thread; null means configuration was persisted successfully. */
@@ -34,14 +25,7 @@ public final class PushyRuntime {
34
25
  }
35
26
 
36
27
  // Configuration must not wait behind a network round that it invalidates.
37
- private static final Executor CONFIG_WORKER = Executors.newSingleThreadExecutor(new ThreadFactory() {
38
- @Override
39
- public Thread newThread(Runnable runnable) {
40
- Thread thread = new Thread(runnable, "pushy-host-config");
41
- thread.setDaemon(true);
42
- return thread;
43
- }
44
- });
28
+ private static final Executor CONFIG_WORKER = Threads.single("pushy-host-config", true);
45
29
 
46
30
  /**
47
31
  * Validate and persist a complete configuration, even before JS or bundle
@@ -56,26 +40,22 @@ public final class PushyRuntime {
56
40
  final Context applicationContext = context.getApplicationContext();
57
41
  // Snapshot caller-owned JSON before dispatch, not minutes later on a worker.
58
42
  final String snapshot = options.toString();
59
- CONFIG_WORKER.execute(new Runnable() {
60
- @Override
61
- public void run() {
62
- Exception failure = null;
63
- try {
64
- String config = PushyConfiguration.normalize(snapshot);
65
- UpdateContext.getInstance(applicationContext).setNativeConfig(config);
66
- } catch (Exception e) {
67
- failure = e;
68
- } catch (LinkageError e) {
69
- failure = new IllegalStateException("Native configuration failed", e);
43
+ CONFIG_WORKER.execute(() -> {
44
+ Exception failure = null;
45
+ try {
46
+ NativeCore.ensureLoaded();
47
+ String config = FlowBridge.normalizeConfiguration(snapshot);
48
+ if (config == null) {
49
+ throw new IllegalStateException("Native configuration failed");
70
50
  }
71
- final Exception error = failure;
72
- new Handler(Looper.getMainLooper()).post(new Runnable() {
73
- @Override
74
- public void run() {
75
- callback.onComplete(error);
76
- }
77
- });
51
+ UpdateContext.getInstance(applicationContext).setNativeConfig(config);
52
+ } catch (Exception e) {
53
+ failure = e;
54
+ } catch (LinkageError e) {
55
+ failure = new IllegalStateException("Native configuration failed", e);
78
56
  }
57
+ final Exception error = failure;
58
+ new Handler(Looper.getMainLooper()).post(() -> callback.onComplete(error));
79
59
  });
80
60
  }
81
61
 
@@ -96,32 +76,24 @@ public final class PushyRuntime {
96
76
  throw new IllegalArgumentException("context and callback are required");
97
77
  }
98
78
  final Context applicationContext = context.getApplicationContext();
99
- WORKER.execute(new Runnable() {
100
- @Override
101
- public void run() {
102
- BundlePreparationResult outcome;
103
- try {
104
- if (BuildConfig.DEBUG) {
105
- outcome = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug");
106
- } else {
107
- outcome = SyncCoordinator.prepareBundle(
108
- UpdateContext.getInstance(applicationContext));
109
- }
110
- } catch (InterruptedException e) {
111
- Thread.currentThread().interrupt();
112
- outcome = BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "interrupted");
113
- } catch (Exception | LinkageError e) {
114
- Log.w(Texts.LOG_TAG, "native host sync failed", e);
115
- outcome = BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error");
79
+ WORKER.execute(() -> {
80
+ BundlePreparationResult outcome;
81
+ try {
82
+ if (BuildConfig.DEBUG) {
83
+ outcome = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug");
84
+ } else {
85
+ outcome = SyncCoordinator.prepareBundle(
86
+ UpdateContext.getInstance(applicationContext));
116
87
  }
117
- final BundlePreparationResult result = outcome;
118
- new Handler(Looper.getMainLooper()).post(new Runnable() {
119
- @Override
120
- public void run() {
121
- callback.onComplete(result);
122
- }
123
- });
88
+ } catch (InterruptedException e) {
89
+ Thread.currentThread().interrupt();
90
+ outcome = BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "interrupted");
91
+ } catch (Exception | LinkageError e) {
92
+ Log.w(Texts.LOG_TAG, "native host sync failed", e);
93
+ outcome = BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error");
124
94
  }
95
+ final BundlePreparationResult result = outcome;
96
+ new Handler(Looper.getMainLooper()).post(() -> callback.onComplete(result));
125
97
  });
126
98
  }
127
99
  }
@@ -19,22 +19,7 @@ final class ReactReloadManager {
19
19
  }
20
20
 
21
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) {
30
- if (hash != null) {
31
- updateContext.switchVersion(hash);
32
- }
33
- return updateContext.getBundleUrl();
34
- }
35
-
36
- /**
37
- * Second half, for the UI thread: point the running React instance at
22
+ * For the UI thread: point the running React instance at
38
23
  * {@code updateBundlePath} and reload it. Throws when no supported
39
24
  * injection worked — the caller rejects RESTART_FAILED. There is
40
25
  * deliberately no Activity.recreate() fallback: it would reload the
@@ -241,14 +226,14 @@ final class ReactReloadManager {
241
226
  reactHostDelegateField.setAccessible(true);
242
227
  Object reactHostDelegate = reactHostDelegateField.get(reactHost);
243
228
 
244
- String bundleFieldName = "jsBundleLoader";
245
- if ("expo.modules.ExpoReactHostFactory.ExpoReactHostDelegate".equals(
246
- reactHostDelegate.getClass().getCanonicalName()
247
- )) {
248
- bundleFieldName = "_jsBundleLoader";
229
+ // DefaultReactHostDelegate stores the loader in jsBundleLoader; Expo's
230
+ // ExpoReactHostDelegate computes that property from _jsBundleLoader.
231
+ Field jsBundleLoaderField;
232
+ try {
233
+ jsBundleLoaderField = reactHostDelegate.getClass().getDeclaredField("jsBundleLoader");
234
+ } catch (NoSuchFieldException e) {
235
+ jsBundleLoaderField = reactHostDelegate.getClass().getDeclaredField("_jsBundleLoader");
249
236
  }
250
-
251
- Field jsBundleLoaderField = reactHostDelegate.getClass().getDeclaredField(bundleFieldName);
252
237
  jsBundleLoaderField.setAccessible(true);
253
238
  jsBundleLoaderField.set(reactHostDelegate, loader);
254
239