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.
- package/README-CN.md +7 -7
- package/README.md +7 -6
- package/android/build.gradle +25 -33
- package/android/jni/Android.mk +16 -24
- package/android/lib/arm64-v8a/librnpushy.so +0 -0
- package/android/lib/armeabi-v7a/librnpushy.so +0 -0
- package/android/lib/x86/librnpushy.so +0 -0
- package/android/lib/x86_64/librnpushy.so +0 -0
- package/android/proguard.pro +9 -7
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
- package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
- package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
- package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
- package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
- package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
- package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
- package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
- package/cpp/android_jni/archive_limits_jni.cpp +83 -0
- package/cpp/android_jni/install_record_jni.cpp +89 -0
- package/cpp/android_jni/jni_log.h +31 -0
- package/cpp/android_jni/jni_natives.h +40 -0
- package/cpp/android_jni/jni_registration.cpp +133 -0
- package/cpp/android_jni/jni_util.h +160 -0
- package/cpp/android_jni/update_core_android.cpp +450 -0
- package/cpp/android_jni/update_flow_jni.cpp +133 -0
- package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
- package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
- package/cpp/patch_core/archive_limits.cpp +194 -0
- package/cpp/patch_core/archive_limits.h +99 -10
- package/cpp/patch_core/archive_patch_core.cpp +96 -12
- package/cpp/patch_core/archive_patch_core.h +49 -0
- package/cpp/patch_core/digest.h +5 -4
- package/cpp/patch_core/error_codes.h +5 -3
- package/cpp/patch_core/install_record.cpp +430 -0
- package/cpp/patch_core/install_record.h +163 -9
- package/cpp/patch_core/patch_core.cpp +62 -16
- package/cpp/patch_core/patch_core.h +27 -7
- package/cpp/patch_core/patch_manifest.cpp +201 -0
- package/cpp/patch_core/patch_manifest.h +74 -0
- package/cpp/patch_core/state_ops.h +3 -3
- package/cpp/update_flow_core/flow_json.cpp +82 -8
- package/cpp/update_flow_core/flow_json.h +21 -0
- package/cpp/update_flow_core/native_config.cpp +456 -0
- package/cpp/update_flow_core/native_config.h +84 -0
- package/cpp/update_flow_core/native_round.cpp +326 -0
- package/cpp/update_flow_core/native_round.h +90 -0
- package/cpp/update_flow_core/update_flow_core.h +2 -2
- package/harmony/hvigor-plugin.ts +11 -19
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
- package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
- package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
- package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
- package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
- package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
- package/harmony/pushy/src/main/ets/Logger.ts +20 -34
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
- package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
- package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
- package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
- package/harmony/pushy/src/main/ets/Texts.ts +0 -4
- package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
- package/ios/RCTPushy/RCTPushy.h +12 -0
- package/ios/RCTPushy/RCTPushy.mm +87 -2628
- package/ios/RCTPushy/RCTPushyCommon.h +34 -0
- package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
- package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
- package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
- package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
- package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
- package/ios/RCTPushy/RCTPushyInternal.h +144 -0
- package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
- package/ios/RCTPushy/RCTPushyState.mm +314 -0
- package/package.json +25 -23
- package/react-native-update.podspec +74 -83
- package/src/client.ts +361 -451
- package/src/context.ts +1 -1
- package/src/core.ts +35 -8
- package/src/endpoint.ts +0 -2
- package/src/error.ts +13 -32
- package/src/i18n.ts +18 -83
- package/src/locales/en.ts +0 -4
- package/src/locales/zh.ts +0 -4
- package/src/provider.tsx +60 -53
- package/src/telemetry.ts +0 -13
- package/src/type.ts +0 -15
- package/src/updateFlowCore.ts +116 -10
- package/src/utils.ts +49 -42
- package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
- package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
- package/cpp/patch_core/jni_natives.h +0 -27
- package/cpp/patch_core/jni_registration.cpp +0 -103
- package/cpp/patch_core/jni_util.h +0 -60
- package/cpp/patch_core/patch_core_android.cpp +0 -97
- package/cpp/patch_core/update_core_android.cpp +0 -485
- 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 =
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
27
|
-
return new File(versionDir
|
|
24
|
+
static File bundleFile(File versionDir) {
|
|
25
|
+
return new File(versionDir, BUNDLE_FILE_NAME);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
static
|
|
31
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
|
50
|
-
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
*
|
|
111
|
-
*
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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).
|
|
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 =
|
|
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 =
|
|
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(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|