react-native-update 10.55.1 → 10.56.1
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 +43 -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
|
@@ -4,8 +4,8 @@ import android.content.Context;
|
|
|
4
4
|
import android.content.SharedPreferences;
|
|
5
5
|
import android.content.pm.PackageInfo;
|
|
6
6
|
import android.content.pm.PackageManager;
|
|
7
|
-
import android.os.Build;
|
|
8
7
|
import android.util.Log;
|
|
8
|
+
import androidx.annotation.Nullable;
|
|
9
9
|
import com.facebook.react.ReactInstanceManager;
|
|
10
10
|
import java.io.File;
|
|
11
11
|
import org.json.JSONObject;
|
|
@@ -15,6 +15,7 @@ import java.security.MessageDigest;
|
|
|
15
15
|
import java.security.NoSuchAlgorithmException;
|
|
16
16
|
import java.util.concurrent.Executor;
|
|
17
17
|
import java.util.concurrent.Executors;
|
|
18
|
+
import java.util.concurrent.ThreadFactory;
|
|
18
19
|
|
|
19
20
|
public class UpdateContext {
|
|
20
21
|
static {
|
|
@@ -28,8 +29,15 @@ public class UpdateContext {
|
|
|
28
29
|
private final File rootDir;
|
|
29
30
|
private final Executor executor;
|
|
30
31
|
private final SharedPreferences sp;
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// Our own PackageInfo, looked up once: every getPackageInfo call is a
|
|
33
|
+
// Binder IPC, and the constructor, the bundle-hash cache and the native
|
|
34
|
+
// check all need the version name / install time.
|
|
35
|
+
@Nullable
|
|
36
|
+
private final PackageInfo packageInfo;
|
|
37
|
+
|
|
38
|
+
// Written under sLock (setCustomInstanceManager), read on the restart
|
|
39
|
+
// path without it.
|
|
40
|
+
private volatile ReactInstanceManager reactInstanceManager;
|
|
33
41
|
// Written on the launch path, read from executor/JS threads.
|
|
34
42
|
private volatile boolean isUsingBundleUrl;
|
|
35
43
|
private volatile boolean ignoreRollback;
|
|
@@ -38,12 +46,12 @@ public class UpdateContext {
|
|
|
38
46
|
// update assets (images/fonts) are read from it on demand at runtime, so
|
|
39
47
|
// wiping it under a silent (no-restart) reset would break every image the
|
|
40
48
|
// running app has not loaded yet. Volatile: written during launch, read
|
|
41
|
-
// from the state serial
|
|
49
|
+
// from the state serial and download executors.
|
|
42
50
|
private volatile String launchVersion;
|
|
43
51
|
private static final int STATE_OP_SWITCH_VERSION = 1;
|
|
44
52
|
private static final int STATE_OP_MARK_SUCCESS = 2;
|
|
45
53
|
private static final int STATE_OP_ROLLBACK = 3;
|
|
46
|
-
|
|
54
|
+
// 4 (clear first time) exists in the state core but has no Android caller.
|
|
47
55
|
private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
|
|
48
56
|
private static final int STATE_OP_RESOLVE_LAUNCH = 6;
|
|
49
57
|
private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
|
|
@@ -55,9 +63,21 @@ public class UpdateContext {
|
|
|
55
63
|
// can never resurrect the version the app just reset away from.
|
|
56
64
|
private static final java.util.concurrent.atomic.AtomicLong resetGeneration =
|
|
57
65
|
new java.util.concurrent.atomic.AtomicLong(0);
|
|
58
|
-
//
|
|
59
|
-
//
|
|
66
|
+
// The one mutex for every snapshot -> state core -> commit sequence
|
|
67
|
+
// (switchVersion, markSuccess, the launch resolution and its rollback,
|
|
68
|
+
// the launch markers, the cold-start round's commit,
|
|
69
|
+
// resetToPackagedBundle). They are read-modify-write cycles on the same
|
|
70
|
+
// SharedPreferences from different threads (state serial executor,
|
|
71
|
+
// native check thread, JS thread); without a common lock a commit built
|
|
72
|
+
// on an older snapshot silently undoes an earlier one — e.g. a JS
|
|
73
|
+
// markSuccess erasing the activation a crash-rescue round just committed
|
|
74
|
+
// (CODE_AUDIT 2.1). Intrinsic locks are reentrant, so nested takers such
|
|
75
|
+
// as getBundleUrl -> rollBack are fine. Nothing slow runs inside it: the
|
|
76
|
+
// bundle digest of a switch is verified before the lock is taken.
|
|
60
77
|
private static final Object commitLock = new Object();
|
|
78
|
+
// Cleanup and activation share this lock from directory verification through
|
|
79
|
+
// deletion/state commit. Slow filesystem work still stays outside commitLock.
|
|
80
|
+
private static final Object versionFilesLock = new Object();
|
|
61
81
|
|
|
62
82
|
// Singleton instance
|
|
63
83
|
private static volatile UpdateContext sInstance;
|
|
@@ -80,7 +100,12 @@ public class UpdateContext {
|
|
|
80
100
|
|
|
81
101
|
private UpdateContext(Context context) {
|
|
82
102
|
this.context = context.getApplicationContext();
|
|
83
|
-
this.executor = Executors.newSingleThreadExecutor()
|
|
103
|
+
this.executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
|
104
|
+
@Override
|
|
105
|
+
public Thread newThread(Runnable r) {
|
|
106
|
+
return new Thread(r, "pushy-download");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
84
109
|
|
|
85
110
|
this.rootDir = new File(this.context.getFilesDir(), "_update");
|
|
86
111
|
|
|
@@ -89,6 +114,7 @@ public class UpdateContext {
|
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
this.sp = this.context.getSharedPreferences("update", Context.MODE_PRIVATE);
|
|
117
|
+
this.packageInfo = lookupPackageInfo(this.context);
|
|
92
118
|
this.reactInstanceManager = pendingReactInstanceManager;
|
|
93
119
|
|
|
94
120
|
String packageVersion = getPackageVersion();
|
|
@@ -113,16 +139,20 @@ public class UpdateContext {
|
|
|
113
139
|
return rootDir.toString();
|
|
114
140
|
}
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
PackageInfo pi = null;
|
|
142
|
+
@Nullable
|
|
143
|
+
private static PackageInfo lookupPackageInfo(Context context) {
|
|
119
144
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
|
146
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
147
|
+
// Our own package is always installed; a failure here is a
|
|
148
|
+
// platform bug worth a log line, not a crash on the launch path.
|
|
149
|
+
Log.e(TAG, "Unable to read own package info", e);
|
|
150
|
+
return null;
|
|
124
151
|
}
|
|
125
|
-
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public String getPackageVersion() {
|
|
155
|
+
return packageInfo == null ? null : packageInfo.versionName;
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
public String getBuildTime() {
|
|
@@ -188,13 +218,7 @@ public class UpdateContext {
|
|
|
188
218
|
}
|
|
189
219
|
|
|
190
220
|
private long getPackageLastUpdateTime() {
|
|
191
|
-
|
|
192
|
-
PackageInfo pi = context.getPackageManager()
|
|
193
|
-
.getPackageInfo(context.getPackageName(), 0);
|
|
194
|
-
return pi.lastUpdateTime;
|
|
195
|
-
} catch (PackageManager.NameNotFoundException e) {
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
221
|
+
return packageInfo == null ? 0 : packageInfo.lastUpdateTime;
|
|
198
222
|
}
|
|
199
223
|
|
|
200
224
|
private String sha256OfBundledAsset(String assetName) {
|
|
@@ -224,21 +248,8 @@ public class UpdateContext {
|
|
|
224
248
|
executor.execute(new DownloadTask(context, params));
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
// Server-provided identifiers (hash/originHash/fileName) become child
|
|
228
|
-
// names under rootDir; anything that could resolve outside of it (path
|
|
229
|
-
// separators, "..", ".") must be rejected before touching the filesystem.
|
|
230
|
-
static boolean isSafePathComponent(String name) {
|
|
231
|
-
return name != null
|
|
232
|
-
&& !name.isEmpty()
|
|
233
|
-
&& !name.equals(".")
|
|
234
|
-
&& !name.equals("..")
|
|
235
|
-
&& !name.contains("/")
|
|
236
|
-
&& !name.contains("\\")
|
|
237
|
-
&& name.indexOf('\0') < 0;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
251
|
private static boolean rejectUnsafeComponent(String name, DownloadFileListener listener) {
|
|
241
|
-
if (isSafePathComponent(name)) {
|
|
252
|
+
if (UpdateFileUtils.isSafePathComponent(name)) {
|
|
242
253
|
return false;
|
|
243
254
|
}
|
|
244
255
|
listener.onDownloadFailed(new IllegalArgumentException("Invalid path component: " + name));
|
|
@@ -254,11 +265,18 @@ public class UpdateContext {
|
|
|
254
265
|
downloadFullUpdate(url, hash, listener, 0);
|
|
255
266
|
}
|
|
256
267
|
|
|
257
|
-
|
|
268
|
+
/**
|
|
269
|
+
* The deadline-taking overloads (native cold-start check) return the
|
|
270
|
+
* queued task's params so the orchestrator can cancel the task when its
|
|
271
|
+
* phase budget expires (CODE_AUDIT 2.12); null when the request was
|
|
272
|
+
* rejected up front (the listener has already been told).
|
|
273
|
+
*/
|
|
274
|
+
@Nullable
|
|
275
|
+
DownloadTaskParams downloadFullUpdate(
|
|
258
276
|
String url, String hash, DownloadFileListener listener, long deadlineNanos
|
|
259
277
|
) {
|
|
260
278
|
if (rejectUnsafeComponent(hash, listener)) {
|
|
261
|
-
return;
|
|
279
|
+
return null;
|
|
262
280
|
}
|
|
263
281
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
264
282
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
|
|
@@ -269,6 +287,7 @@ public class UpdateContext {
|
|
|
269
287
|
params.targetFile = new File(rootDir, hash + ".ppk");
|
|
270
288
|
params.unzipDirectory = new File(rootDir, hash);
|
|
271
289
|
enqueue(params);
|
|
290
|
+
return params;
|
|
272
291
|
}
|
|
273
292
|
|
|
274
293
|
public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
|
|
@@ -290,11 +309,12 @@ public class UpdateContext {
|
|
|
290
309
|
downloadPatchFromApk(url, hash, listener, 0);
|
|
291
310
|
}
|
|
292
311
|
|
|
293
|
-
|
|
312
|
+
@Nullable
|
|
313
|
+
DownloadTaskParams downloadPatchFromApk(
|
|
294
314
|
String url, String hash, DownloadFileListener listener, long deadlineNanos
|
|
295
315
|
) {
|
|
296
316
|
if (rejectUnsafeComponent(hash, listener)) {
|
|
297
|
-
return;
|
|
317
|
+
return null;
|
|
298
318
|
}
|
|
299
319
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
300
320
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
|
|
@@ -305,13 +325,15 @@ public class UpdateContext {
|
|
|
305
325
|
params.targetFile = new File(rootDir, hash + ".apk.patch");
|
|
306
326
|
params.unzipDirectory = new File(rootDir, hash);
|
|
307
327
|
enqueue(params);
|
|
328
|
+
return params;
|
|
308
329
|
}
|
|
309
330
|
|
|
310
331
|
public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
|
|
311
332
|
downloadPatchFromPpk(url, hash, originHash, listener, 0);
|
|
312
333
|
}
|
|
313
334
|
|
|
314
|
-
|
|
335
|
+
@Nullable
|
|
336
|
+
DownloadTaskParams downloadPatchFromPpk(
|
|
315
337
|
String url,
|
|
316
338
|
String hash,
|
|
317
339
|
String originHash,
|
|
@@ -319,7 +341,7 @@ public class UpdateContext {
|
|
|
319
341
|
long deadlineNanos
|
|
320
342
|
) {
|
|
321
343
|
if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
|
|
322
|
-
return;
|
|
344
|
+
return null;
|
|
323
345
|
}
|
|
324
346
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
325
347
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
|
|
@@ -332,6 +354,7 @@ public class UpdateContext {
|
|
|
332
354
|
params.unzipDirectory = new File(rootDir, hash);
|
|
333
355
|
params.originDirectory = new File(rootDir, originHash);
|
|
334
356
|
enqueue(params);
|
|
357
|
+
return params;
|
|
335
358
|
}
|
|
336
359
|
|
|
337
360
|
private StateCoreResult getStateSnapshot() {
|
|
@@ -396,12 +419,14 @@ public class UpdateContext {
|
|
|
396
419
|
}
|
|
397
420
|
|
|
398
421
|
/**
|
|
399
|
-
* Validates that {@code hash} is a switchable version
|
|
400
|
-
*
|
|
401
|
-
*
|
|
422
|
+
* Validates that {@code hash} is a switchable version: the slow half of
|
|
423
|
+
* a switch (file checks and the bundle digest of a multi-MB bundle),
|
|
424
|
+
* deliberately run without commitLock. The state snapshot read here only
|
|
425
|
+
* decides whether a markerless directory is grandfathered; a concurrent
|
|
426
|
+
* commit cannot make an unverified directory pass.
|
|
402
427
|
*/
|
|
403
|
-
private
|
|
404
|
-
if (!isSafePathComponent(hash)) {
|
|
428
|
+
private void verifySwitchTarget(String hash) {
|
|
429
|
+
if (!UpdateFileUtils.isSafePathComponent(hash)) {
|
|
405
430
|
throw new IllegalArgumentException("Invalid hash: " + hash);
|
|
406
431
|
}
|
|
407
432
|
File versionDir = new File(rootDir, hash);
|
|
@@ -412,24 +437,34 @@ public class UpdateContext {
|
|
|
412
437
|
StateCoreResult currentState = getStateSnapshot();
|
|
413
438
|
boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
|
|
414
439
|
|| hash.equals(currentState.lastVersion);
|
|
415
|
-
if (
|
|
440
|
+
if (isLegacyActivatedVersion) {
|
|
416
441
|
// Versions activated before completion markers were introduced are
|
|
417
|
-
// explicitly grandfathered through current/last state.
|
|
418
|
-
|
|
442
|
+
// explicitly grandfathered through current/last state.
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!InstallRecord.isComplete(versionDir, hash)) {
|
|
446
|
+
// An arbitrary markerless directory may be a crash-left partial
|
|
447
|
+
// install.
|
|
419
448
|
throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
|
|
420
449
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
throw new IllegalStateException(e.getMessage(), e);
|
|
428
|
-
}
|
|
450
|
+
// The record's bundle digest must match the bytes on disk before
|
|
451
|
+
// the next launch is pointed at them.
|
|
452
|
+
try {
|
|
453
|
+
InstallRecord.verifyForActivation(versionDir, hash, bundleFile);
|
|
454
|
+
} catch (IOException e) {
|
|
455
|
+
throw new IllegalStateException(e.getMessage(), e);
|
|
429
456
|
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The fast half of a switch: snapshot -> state core, without persisting,
|
|
461
|
+
* so callers can fold it into a larger single-commit transaction. Caller
|
|
462
|
+
* holds commitLock and has run verifySwitchTarget.
|
|
463
|
+
*/
|
|
464
|
+
private StateCoreResult computeSwitchState(String hash) {
|
|
430
465
|
return runStateCore(
|
|
431
466
|
STATE_OP_SWITCH_VERSION,
|
|
432
|
-
|
|
467
|
+
getStateSnapshot(),
|
|
433
468
|
hash,
|
|
434
469
|
false,
|
|
435
470
|
false
|
|
@@ -437,11 +472,16 @@ public class UpdateContext {
|
|
|
437
472
|
}
|
|
438
473
|
|
|
439
474
|
public void switchVersion(String hash) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
475
|
+
synchronized (versionFilesLock) {
|
|
476
|
+
verifySwitchTarget(hash);
|
|
477
|
+
synchronized (commitLock) {
|
|
478
|
+
StateCoreResult nextState = computeSwitchState(hash);
|
|
479
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
480
|
+
applyState(editor, nextState);
|
|
481
|
+
persistEditorOrThrow(editor, "switch version");
|
|
482
|
+
ignoreRollback = false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
445
485
|
}
|
|
446
486
|
|
|
447
487
|
public void setKv(String key, String value) {
|
|
@@ -464,18 +504,53 @@ public class UpdateContext {
|
|
|
464
504
|
return sp.getString("currentVersion", null);
|
|
465
505
|
}
|
|
466
506
|
|
|
467
|
-
|
|
468
|
-
|
|
507
|
+
/** The one-shot launch markers getConstants reports, consumed together. */
|
|
508
|
+
static final class LaunchMarkers {
|
|
509
|
+
final boolean isFirstTime;
|
|
510
|
+
@Nullable
|
|
511
|
+
final String rolledBackVersion;
|
|
512
|
+
|
|
513
|
+
LaunchMarkers(boolean isFirstTime, @Nullable String rolledBackVersion) {
|
|
514
|
+
this.isFirstTime = isFirstTime;
|
|
515
|
+
this.rolledBackVersion = rolledBackVersion;
|
|
516
|
+
}
|
|
469
517
|
}
|
|
470
518
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Reads and clears the first-load marker and the rollback mark in one
|
|
521
|
+
* commit. getConstants runs this synchronously on the JS thread (a
|
|
522
|
+
* TurboModule constant read), so the two writes are folded into a single
|
|
523
|
+
* editor; persistence is best-effort on this launch path.
|
|
524
|
+
*/
|
|
525
|
+
LaunchMarkers consumeLaunchMarkers() {
|
|
526
|
+
LaunchMarkers markers;
|
|
527
|
+
synchronized (commitLock) {
|
|
528
|
+
boolean isFirstLoadMarked = sp.getBoolean(KEY_FIRST_LOAD_MARKED, false);
|
|
529
|
+
String rolledBackVersion = rolledBackVersion();
|
|
530
|
+
markers = new LaunchMarkers(isFirstLoadMarked, rolledBackVersion);
|
|
531
|
+
if (isFirstLoadMarked || rolledBackVersion != null) {
|
|
532
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
533
|
+
if (isFirstLoadMarked) {
|
|
534
|
+
editor.remove(KEY_FIRST_LOAD_MARKED);
|
|
535
|
+
}
|
|
536
|
+
if (rolledBackVersion != null) {
|
|
537
|
+
StateCoreResult nextState = runStateCore(
|
|
538
|
+
STATE_OP_CLEAR_ROLLBACK_MARK,
|
|
539
|
+
getStateSnapshot(),
|
|
540
|
+
null,
|
|
541
|
+
false,
|
|
542
|
+
false
|
|
543
|
+
);
|
|
544
|
+
applyState(editor, nextState);
|
|
545
|
+
}
|
|
546
|
+
persistEditor(editor, "consume launch markers");
|
|
547
|
+
}
|
|
477
548
|
}
|
|
478
|
-
|
|
549
|
+
if (markers.rolledBackVersion != null) {
|
|
550
|
+
// The rolled-back version's directory is now eligible for cleanup.
|
|
551
|
+
this.cleanUp();
|
|
552
|
+
}
|
|
553
|
+
return markers;
|
|
479
554
|
}
|
|
480
555
|
|
|
481
556
|
public String rolledBackVersion() {
|
|
@@ -483,7 +558,10 @@ public class UpdateContext {
|
|
|
483
558
|
}
|
|
484
559
|
|
|
485
560
|
public void markSuccess() {
|
|
486
|
-
if (
|
|
561
|
+
if (BuildConfig.DEBUG) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
synchronized (commitLock) {
|
|
487
565
|
StateCoreResult currentState = getStateSnapshot();
|
|
488
566
|
StateCoreResult nextState = runStateCore(
|
|
489
567
|
STATE_OP_MARK_SUCCESS,
|
|
@@ -498,24 +576,7 @@ public class UpdateContext {
|
|
|
498
576
|
editor.remove("hash_" + nextState.staleVersionToDelete);
|
|
499
577
|
}
|
|
500
578
|
persistEditorOrThrow(editor, "mark success");
|
|
501
|
-
|
|
502
|
-
this.cleanUp();
|
|
503
579
|
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
public void clearFirstTime() {
|
|
507
|
-
StateCoreResult currentState = getStateSnapshot();
|
|
508
|
-
StateCoreResult nextState = runStateCore(
|
|
509
|
-
STATE_OP_CLEAR_FIRST_TIME,
|
|
510
|
-
currentState,
|
|
511
|
-
null,
|
|
512
|
-
false,
|
|
513
|
-
false
|
|
514
|
-
);
|
|
515
|
-
SharedPreferences.Editor editor = sp.edit();
|
|
516
|
-
applyState(editor, nextState);
|
|
517
|
-
editor.remove(KEY_FIRST_LOAD_MARKED);
|
|
518
|
-
persistEditorOrThrow(editor, "clear first time");
|
|
519
580
|
|
|
520
581
|
this.cleanUp();
|
|
521
582
|
}
|
|
@@ -560,32 +621,12 @@ public class UpdateContext {
|
|
|
560
621
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
561
622
|
params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
|
|
562
623
|
params.maxAgeDays = 0;
|
|
563
|
-
// Keep
|
|
564
|
-
//
|
|
565
|
-
// orphaned directory is removed by the next regular cleanup.
|
|
566
|
-
params.hash = launchVersion;
|
|
624
|
+
// Keep names are resolved by the queued task at execution time, so a
|
|
625
|
+
// later activation is protected too.
|
|
567
626
|
params.unzipDirectory = rootDir;
|
|
568
627
|
enqueue(params);
|
|
569
628
|
}
|
|
570
629
|
|
|
571
|
-
public void clearRollbackMark() {
|
|
572
|
-
StateCoreResult currentState = getStateSnapshot();
|
|
573
|
-
StateCoreResult nextState = runStateCore(
|
|
574
|
-
STATE_OP_CLEAR_ROLLBACK_MARK,
|
|
575
|
-
currentState,
|
|
576
|
-
null,
|
|
577
|
-
false,
|
|
578
|
-
false
|
|
579
|
-
);
|
|
580
|
-
SharedPreferences.Editor editor = sp.edit();
|
|
581
|
-
applyState(editor, nextState);
|
|
582
|
-
// Runs from getConstants on the launch path: best-effort only.
|
|
583
|
-
persistEditor(editor, "clear rollback mark");
|
|
584
|
-
|
|
585
|
-
this.cleanUp();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
630
|
public static void setCustomInstanceManager(ReactInstanceManager instanceManager) {
|
|
590
631
|
synchronized (sLock) {
|
|
591
632
|
pendingReactInstanceManager = instanceManager;
|
|
@@ -629,59 +670,64 @@ public class UpdateContext {
|
|
|
629
670
|
isUsingBundleUrl = true;
|
|
630
671
|
String nativeCheckRolledBackVersion = null;
|
|
631
672
|
try {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
673
|
+
// The whole resolution is one read-modify-write on the state
|
|
674
|
+
// (resolve, then possibly roll back missing bundles); see
|
|
675
|
+
// commitLock.
|
|
676
|
+
synchronized (commitLock) {
|
|
677
|
+
StateCoreResult currentState = getStateSnapshot();
|
|
678
|
+
StateCoreResult launchState = runStateCore(
|
|
679
|
+
STATE_OP_RESOLVE_LAUNCH,
|
|
680
|
+
currentState,
|
|
681
|
+
null,
|
|
682
|
+
ignoreRollback,
|
|
683
|
+
true
|
|
684
|
+
);
|
|
685
|
+
nativeCheckRolledBackVersion = launchState.rolledBackVersion;
|
|
686
|
+
if (launchState.didRollback) {
|
|
687
|
+
// The crash-protection rollback: the new version never called
|
|
688
|
+
// markSuccess. Keep this visible in release logs.
|
|
689
|
+
Log.e(TAG, "Version " + currentState.currentVersion
|
|
690
|
+
+ " was not marked as successful, rolling back to "
|
|
691
|
+
+ launchState.currentVersion);
|
|
692
|
+
}
|
|
693
|
+
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
694
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
695
|
+
applyState(editor, launchState);
|
|
696
|
+
if (launchState.consumedFirstTime) {
|
|
697
|
+
editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
|
|
698
|
+
}
|
|
699
|
+
persistEditor(editor, "resolve launch");
|
|
700
|
+
}
|
|
651
701
|
if (launchState.consumedFirstTime) {
|
|
652
|
-
|
|
702
|
+
// bundleURL may be resolved multiple times in one process.
|
|
703
|
+
ignoreRollback = true;
|
|
653
704
|
}
|
|
654
|
-
persistEditor(editor, "resolve launch");
|
|
655
|
-
}
|
|
656
|
-
if (launchState.consumedFirstTime) {
|
|
657
|
-
// bundleURL may be resolved multiple times in one process.
|
|
658
|
-
ignoreRollback = true;
|
|
659
|
-
}
|
|
660
705
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
706
|
+
String currentVersion = launchState.loadVersion;
|
|
707
|
+
if (currentVersion == null) {
|
|
708
|
+
return defaultAssetsUrl;
|
|
709
|
+
}
|
|
665
710
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
711
|
+
// Guard the rollback chain against cycles: a corrupted state returning
|
|
712
|
+
// an already-visited version would otherwise spin this loop forever on
|
|
713
|
+
// the main thread.
|
|
714
|
+
java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
|
|
715
|
+
while (currentVersion != null && visitedVersions.add(currentVersion)) {
|
|
716
|
+
File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
|
|
717
|
+
if (!bundleFile.exists()) {
|
|
718
|
+
Log.e(TAG, "Bundle version " + currentVersion + " not found.");
|
|
719
|
+
currentVersion = this.rollBack();
|
|
720
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
launchVersion = currentVersion;
|
|
675
724
|
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
676
|
-
|
|
725
|
+
return bundleFile.toString();
|
|
677
726
|
}
|
|
678
|
-
|
|
727
|
+
|
|
679
728
|
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
680
|
-
return
|
|
729
|
+
return defaultAssetsUrl;
|
|
681
730
|
}
|
|
682
|
-
|
|
683
|
-
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
684
|
-
return defaultAssetsUrl;
|
|
685
731
|
} finally {
|
|
686
732
|
// Even corrupted state or a state-core exception must not disable
|
|
687
733
|
// the next-launch rescue check. A null snapshot simply omits the
|
|
@@ -708,22 +754,36 @@ public class UpdateContext {
|
|
|
708
754
|
String hashInfoJson,
|
|
709
755
|
boolean activate,
|
|
710
756
|
String responseCacheJson
|
|
757
|
+
) {
|
|
758
|
+
boolean switching = activate && hash != null;
|
|
759
|
+
if (!switching) {
|
|
760
|
+
return commitNativeCheckResultState(
|
|
761
|
+
expectedGeneration, hash, hashInfoJson, false, responseCacheJson);
|
|
762
|
+
}
|
|
763
|
+
synchronized (versionFilesLock) {
|
|
764
|
+
verifySwitchTarget(hash);
|
|
765
|
+
return commitNativeCheckResultState(
|
|
766
|
+
expectedGeneration, hash, hashInfoJson, true, responseCacheJson);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private boolean commitNativeCheckResultState(
|
|
771
|
+
long expectedGeneration,
|
|
772
|
+
String hash,
|
|
773
|
+
String hashInfoJson,
|
|
774
|
+
boolean switching,
|
|
775
|
+
String responseCacheJson
|
|
711
776
|
) {
|
|
712
777
|
synchronized (commitLock) {
|
|
713
778
|
if (resetGeneration.get() != expectedGeneration) {
|
|
714
779
|
return false;
|
|
715
780
|
}
|
|
716
|
-
// One editor, one commit: the version info, the activation and
|
|
717
|
-
// the response cache land together or not at all. Three separate
|
|
718
|
-
// commits under the same lock only prevented interleaving, not a
|
|
719
|
-
// half-written round after a failed or interrupted write.
|
|
720
781
|
SharedPreferences.Editor editor = sp.edit();
|
|
721
782
|
if (hash != null && hashInfoJson != null) {
|
|
722
783
|
editor.putString("hash_" + hash, hashInfoJson);
|
|
723
784
|
}
|
|
724
|
-
boolean switching = activate && hash != null;
|
|
725
785
|
if (switching) {
|
|
726
|
-
applyState(editor,
|
|
786
|
+
applyState(editor, computeSwitchState(hash));
|
|
727
787
|
}
|
|
728
788
|
if (responseCacheJson != null) {
|
|
729
789
|
editor.putString(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
|
|
@@ -742,7 +802,7 @@ public class UpdateContext {
|
|
|
742
802
|
* unreadable record. Exposed to JS for crash-report attribution.
|
|
743
803
|
*/
|
|
744
804
|
String currentBundleSha256(String hash) {
|
|
745
|
-
if (hash == null || hash.isEmpty() || !isSafePathComponent(hash)) {
|
|
805
|
+
if (hash == null || hash.isEmpty() || !UpdateFileUtils.isSafePathComponent(hash)) {
|
|
746
806
|
return "";
|
|
747
807
|
}
|
|
748
808
|
JSONObject record = InstallRecord.read(new File(rootDir, hash));
|
|
@@ -750,7 +810,7 @@ public class UpdateContext {
|
|
|
750
810
|
}
|
|
751
811
|
|
|
752
812
|
boolean hasCompletedVersion(String hash) {
|
|
753
|
-
if (!isSafePathComponent(hash)) {
|
|
813
|
+
if (!UpdateFileUtils.isSafePathComponent(hash)) {
|
|
754
814
|
return false;
|
|
755
815
|
}
|
|
756
816
|
File versionDir = new File(rootDir, hash);
|
|
@@ -759,27 +819,63 @@ public class UpdateContext {
|
|
|
759
819
|
}
|
|
760
820
|
|
|
761
821
|
private String rollBack() {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
822
|
+
synchronized (commitLock) {
|
|
823
|
+
StateCoreResult currentState = getStateSnapshot();
|
|
824
|
+
StateCoreResult nextState = runStateCore(
|
|
825
|
+
STATE_OP_ROLLBACK,
|
|
826
|
+
currentState,
|
|
827
|
+
null,
|
|
828
|
+
false,
|
|
829
|
+
false
|
|
830
|
+
);
|
|
831
|
+
Log.e(TAG, "Rolling back version " + currentState.currentVersion
|
|
832
|
+
+ " to " + nextState.currentVersion);
|
|
833
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
834
|
+
applyState(editor, nextState);
|
|
835
|
+
persistEditor(editor, "rollback");
|
|
836
|
+
return nextState.currentVersion;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* The version this process is running from (null before getBundleUrl
|
|
842
|
+
* resolved one, or when the packaged bundle runs). Read by DownloadTask
|
|
843
|
+
* to refuse reinstalling that directory in place.
|
|
844
|
+
*/
|
|
845
|
+
@Nullable
|
|
846
|
+
static String runningVersion() {
|
|
847
|
+
UpdateContext instance = sInstance;
|
|
848
|
+
return instance == null ? null : instance.launchVersion;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
interface CleanupAction {
|
|
852
|
+
void run(@Nullable String keepCurrent, @Nullable String keepPrevious);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Resolve keep names immediately before deletion, never when enqueued. */
|
|
856
|
+
void runCleanupWithLatestState(CleanupAction action) {
|
|
857
|
+
synchronized (versionFilesLock) {
|
|
858
|
+
final String[] keepNames;
|
|
859
|
+
synchronized (commitLock) {
|
|
860
|
+
keepNames = CleanupKeepNames.select(
|
|
861
|
+
sp.getString("currentVersion", null),
|
|
862
|
+
sp.getString("lastVersion", null),
|
|
863
|
+
launchVersion
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
if (keepNames == null) {
|
|
867
|
+
// JNI has only two keep slots; three distinct live versions are
|
|
868
|
+
// safer to defer until the next process than to delete one.
|
|
869
|
+
Log.i(TAG, "Skipping cleanup: three live versions need protection");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
action.run(keepNames[0], keepNames[1]);
|
|
873
|
+
}
|
|
776
874
|
}
|
|
777
875
|
|
|
778
876
|
private void cleanUp() {
|
|
779
877
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
780
878
|
params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
|
|
781
|
-
params.hash = sp.getString("currentVersion", null);
|
|
782
|
-
params.originHash = sp.getString("lastVersion", null);
|
|
783
879
|
params.unzipDirectory = rootDir;
|
|
784
880
|
enqueue(params);
|
|
785
881
|
}
|