react-native-update 10.51.0 → 10.52.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/NATIVE_CHECKUPDATE_DESIGN.md +108 -0
- package/NATIVE_CHECK_FOLLOWUPS.md +46 -0
- package/README-CN.md +2 -0
- package/README.md +2 -0
- package/android/build.gradle +10 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashRescue.java +115 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +259 -26
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +209 -19
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +6 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +381 -72
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +32 -2
- package/harmony/pushy/src/main/ets/UpdateContext.ts +5 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +308 -25
- package/ios/RCTPushy/RCTPushyDownloader.mm +361 -74
- package/package.json +1 -1
- package/src/client.ts +8 -0
- package/src/type.ts +6 -0
|
@@ -29,10 +29,35 @@ final class NativeCheckOrchestrator {
|
|
|
29
29
|
// Raw response cache for the JS side to reuse (§10.3), scoped to the
|
|
30
30
|
// exact logical request and native config that produced it.
|
|
31
31
|
static final String KEY_RESP_CACHE = "nativeCheckResp";
|
|
32
|
+
// Set when a round starts, cleared when it ends (§11.4). Residue on the
|
|
33
|
+
// next launch means the previous process died mid-round (a crash rescue
|
|
34
|
+
// was truncated): that launch resumes immediately instead of waiting 5s.
|
|
35
|
+
static final String KEY_ROUND_INCOMPLETE = "nativeCheckIncomplete";
|
|
32
36
|
private static final int MAX_CHECK_HTTP_ATTEMPTS = 8;
|
|
33
37
|
private static final long DOWNLOAD_PHASE_TIMEOUT_SECONDS = 600;
|
|
34
38
|
|
|
35
39
|
private static final AtomicBoolean scheduled = new AtomicBoolean(false);
|
|
40
|
+
// One round per process, whoever starts it first — the delayed cold-start
|
|
41
|
+
// thread or the crash-rescue thread (§11.3). roundDone lets the rescue
|
|
42
|
+
// wait for an in-flight round instead of racing it.
|
|
43
|
+
private static final AtomicBoolean roundStarted = new AtomicBoolean(false);
|
|
44
|
+
private static final CountDownLatch roundDone = new CountDownLatch(1);
|
|
45
|
+
private static volatile boolean roundCompleted = false;
|
|
46
|
+
// Flipped the moment a crash is being held. JS is dead from that point
|
|
47
|
+
// on, so there is no second decision maker: the round force-activates
|
|
48
|
+
// whatever it downloads (§11.3).
|
|
49
|
+
private static volatile boolean crashRescueActive = false;
|
|
50
|
+
// A version this process downloaded but left for JS to activate. If the
|
|
51
|
+
// process then crashes, JS will never activate it — the crash handler
|
|
52
|
+
// activates it directly (bounded local work, no network). The generation
|
|
53
|
+
// is the one the round committed under: a reset that lands afterwards
|
|
54
|
+
// bumps it, and the late activation must lose to that reset exactly like
|
|
55
|
+
// the round itself would. Written generation-first, read hash-first, so
|
|
56
|
+
// a torn read can only see a stricter (newer) generation.
|
|
57
|
+
private static volatile String unactivatedHash;
|
|
58
|
+
private static volatile long unactivatedGeneration;
|
|
59
|
+
private static volatile UpdateContext sContext;
|
|
60
|
+
private static volatile String sLaunchRolledBackVersion;
|
|
36
61
|
|
|
37
62
|
private NativeCheckOrchestrator() {
|
|
38
63
|
}
|
|
@@ -44,14 +69,24 @@ final class NativeCheckOrchestrator {
|
|
|
44
69
|
if (!scheduled.compareAndSet(false, true)) {
|
|
45
70
|
return;
|
|
46
71
|
}
|
|
72
|
+
sContext = context;
|
|
73
|
+
sLaunchRolledBackVersion = launchRolledBackVersion;
|
|
74
|
+
// The crash-hold rescue shares the orchestrator's rollout gate: no
|
|
75
|
+
// persisted config, no handler (§11.3).
|
|
76
|
+
if (context.getKv(KEY_CONFIG) != null) {
|
|
77
|
+
CrashRescue.install();
|
|
78
|
+
}
|
|
47
79
|
Thread thread = new Thread(new Runnable() {
|
|
48
80
|
@Override
|
|
49
81
|
public void run() {
|
|
50
82
|
try {
|
|
51
83
|
// Keep the check away from the cold-start critical path
|
|
52
|
-
// (§7 R5) —
|
|
53
|
-
|
|
54
|
-
|
|
84
|
+
// (§7 R5) — unless the previous process died mid-round,
|
|
85
|
+
// in which case every launch second counts (§11.4).
|
|
86
|
+
if (context.getKv(KEY_ROUND_INCOMPLETE) == null) {
|
|
87
|
+
Thread.sleep(5000);
|
|
88
|
+
}
|
|
89
|
+
startRound(0);
|
|
55
90
|
} catch (Throwable e) {
|
|
56
91
|
// The rescue path must never take the app down with it.
|
|
57
92
|
Log.w(UpdateContext.TAG, "native check failed: " + e);
|
|
@@ -63,9 +98,94 @@ final class NativeCheckOrchestrator {
|
|
|
63
98
|
thread.start();
|
|
64
99
|
}
|
|
65
100
|
|
|
101
|
+
static boolean isRoundInFlight() {
|
|
102
|
+
return roundStarted.get() && !roundCompleted;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Runs the process's single round on the calling thread if nobody has
|
|
107
|
+
* started it yet. deadlineNanos > 0 (crash rescue) caps every HTTP call
|
|
108
|
+
* and download phase to the remaining budget.
|
|
109
|
+
*/
|
|
110
|
+
private static void startRound(long deadlineNanos) {
|
|
111
|
+
if (!roundStarted.compareAndSet(false, true)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
runOnce(sContext, sLaunchRolledBackVersion, deadlineNanos);
|
|
116
|
+
} catch (Throwable e) {
|
|
117
|
+
Log.w(UpdateContext.TAG, "native check failed: " + e);
|
|
118
|
+
} finally {
|
|
119
|
+
roundCompleted = true;
|
|
120
|
+
roundDone.countDown();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Crash-rescue entry (§11.3), called from the handler's worker thread
|
|
126
|
+
* while the uncaught-exception handler holds the dying process. Ensures
|
|
127
|
+
* this process's round runs to completion within the budget, then
|
|
128
|
+
* activates a downloaded-but-unactivated version if one exists — the
|
|
129
|
+
* last chance before the process is gone.
|
|
130
|
+
*/
|
|
131
|
+
static void runRescue(long deadlineNanos) {
|
|
132
|
+
UpdateContext context = sContext;
|
|
133
|
+
if (context == null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
crashRescueActive = true;
|
|
137
|
+
startRound(deadlineNanos);
|
|
138
|
+
if (!roundCompleted) {
|
|
139
|
+
long remainingNanos = deadlineNanos - System.nanoTime();
|
|
140
|
+
if (remainingNanos > 0) {
|
|
141
|
+
try {
|
|
142
|
+
roundDone.await(remainingNanos, TimeUnit.NANOSECONDS);
|
|
143
|
+
} catch (InterruptedException e) {
|
|
144
|
+
Thread.currentThread().interrupt();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
activatePendingVersion(context);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The alert-strategy variant of the §10.7 hole: the round downloaded a
|
|
153
|
+
// fix but deferred activation to JS, and JS is now dead. Activation is
|
|
154
|
+
// local and bounded (a state switch under the commit lock).
|
|
155
|
+
private static void activatePendingVersion(UpdateContext context) {
|
|
156
|
+
String hash = unactivatedHash;
|
|
157
|
+
if (hash == null) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
String hashInfoJson = null;
|
|
161
|
+
String existingInfo = context.getKv("hash_" + hash);
|
|
162
|
+
if (existingInfo != null) {
|
|
163
|
+
try {
|
|
164
|
+
JSONObject info = new JSONObject(existingInfo);
|
|
165
|
+
info.put("crashRescue", true);
|
|
166
|
+
hashInfoJson = info.toString();
|
|
167
|
+
} catch (JSONException ignored) {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
if (context.commitNativeCheckResult(
|
|
172
|
+
unactivatedGeneration, hash, hashInfoJson, true, null)) {
|
|
173
|
+
unactivatedHash = null;
|
|
174
|
+
Log.i(UpdateContext.TAG,
|
|
175
|
+
"crash rescue: activated downloaded version " + hash);
|
|
176
|
+
} else {
|
|
177
|
+
Log.i(UpdateContext.TAG,
|
|
178
|
+
"crash rescue: reset since download, dropping activation");
|
|
179
|
+
}
|
|
180
|
+
} catch (Exception e) {
|
|
181
|
+
Log.w(UpdateContext.TAG, "crash rescue: activation failed: " + e);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
66
185
|
private static void runOnce(
|
|
67
186
|
UpdateContext context,
|
|
68
|
-
String launchRolledBackVersion
|
|
187
|
+
String launchRolledBackVersion,
|
|
188
|
+
long deadlineNanos
|
|
69
189
|
) throws JSONException {
|
|
70
190
|
// Sampled before any IO: a reset landing while this round runs must
|
|
71
191
|
// win over the round's decision.
|
|
@@ -89,6 +209,27 @@ final class NativeCheckOrchestrator {
|
|
|
89
209
|
if (appKey.isEmpty()) {
|
|
90
210
|
return;
|
|
91
211
|
}
|
|
212
|
+
// From here on the round does real work: leave the breadcrumb that
|
|
213
|
+
// the next launch reads to skip its 5s delay if we die mid-round.
|
|
214
|
+
context.setKv(KEY_ROUND_INCOMPLETE, "1");
|
|
215
|
+
try {
|
|
216
|
+
runConfiguredRound(
|
|
217
|
+
context, launchRolledBackVersion, deadlineNanos,
|
|
218
|
+
resetGeneration, configJson, config, appKey);
|
|
219
|
+
} finally {
|
|
220
|
+
context.removeKv(KEY_ROUND_INCOMPLETE);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private static void runConfiguredRound(
|
|
225
|
+
UpdateContext context,
|
|
226
|
+
String launchRolledBackVersion,
|
|
227
|
+
long deadlineNanos,
|
|
228
|
+
long resetGeneration,
|
|
229
|
+
String configJson,
|
|
230
|
+
JSONObject config,
|
|
231
|
+
String appKey
|
|
232
|
+
) throws JSONException {
|
|
92
233
|
String packageVersion = config.optString(
|
|
93
234
|
"packageVersion", context.getPackageVersion());
|
|
94
235
|
if (packageVersion.isEmpty()) {
|
|
@@ -141,7 +282,7 @@ final class NativeCheckOrchestrator {
|
|
|
141
282
|
return;
|
|
142
283
|
}
|
|
143
284
|
|
|
144
|
-
String responseText = runCheckRequest(config, appKey, body);
|
|
285
|
+
String responseText = runCheckRequest(config, appKey, body, deadlineNanos);
|
|
145
286
|
if (responseText == null) {
|
|
146
287
|
Log.i(UpdateContext.TAG,
|
|
147
288
|
"native check: no endpoint reachable, giving up until next launch");
|
|
@@ -173,7 +314,8 @@ final class NativeCheckOrchestrator {
|
|
|
173
314
|
boolean downloaded = context.hasCompletedVersion(hash);
|
|
174
315
|
if (!downloaded) {
|
|
175
316
|
downloaded = performAttempts(
|
|
176
|
-
context, decision.optJSONArray("attempts"), hash, currentVersion
|
|
317
|
+
context, decision.optJSONArray("attempts"), hash, currentVersion,
|
|
318
|
+
deadlineNanos);
|
|
177
319
|
}
|
|
178
320
|
if (!downloaded) {
|
|
179
321
|
// The native attempt has finished, so JS may safely reuse the
|
|
@@ -197,12 +339,25 @@ final class NativeCheckOrchestrator {
|
|
|
197
339
|
hashInfo.put(key, value);
|
|
198
340
|
}
|
|
199
341
|
}
|
|
342
|
+
// A forceBoot activation is the brick-rescue path: mark it in the
|
|
343
|
+
// persisted info so JS can report force_boot_rescue when this
|
|
344
|
+
// version survives to markSuccess. Only the server-sent directive
|
|
345
|
+
// counts — a silent-strategy activation is ordinary delivery.
|
|
346
|
+
JSONObject infoConfig = info.optJSONObject("config");
|
|
347
|
+
if (infoConfig != null && infoConfig.optBoolean("forceBoot", false)) {
|
|
348
|
+
hashInfo.put("forceBootRescue", true);
|
|
349
|
+
}
|
|
350
|
+
if (crashRescueActive) {
|
|
351
|
+
hashInfo.put("crashRescue", true);
|
|
352
|
+
}
|
|
200
353
|
hashInfoJson = hashInfo.toString();
|
|
201
354
|
}
|
|
202
355
|
// Silent strategies or a server-marked forceBoot version (per-version
|
|
203
356
|
// remote override — the brick rescue) activate for the next launch;
|
|
204
|
-
// otherwise activation stays with the JS side.
|
|
205
|
-
|
|
357
|
+
// otherwise activation stays with the JS side. Unless a crash is
|
|
358
|
+
// being held: JS is dead, deferring to it would leave the fix on
|
|
359
|
+
// disk forever (§11.3).
|
|
360
|
+
boolean activate = decision.optBoolean("activate", false) || crashRescueActive;
|
|
206
361
|
boolean committed;
|
|
207
362
|
try {
|
|
208
363
|
committed = context.commitNativeCheckResult(
|
|
@@ -218,9 +373,14 @@ final class NativeCheckOrchestrator {
|
|
|
218
373
|
if (!committed) {
|
|
219
374
|
Log.i(UpdateContext.TAG, "native check: reset during round, dropping result");
|
|
220
375
|
} else if (activate) {
|
|
376
|
+
unactivatedHash = null;
|
|
221
377
|
Log.i(UpdateContext.TAG,
|
|
222
378
|
"native check: downloaded " + hash + " and set for next launch");
|
|
223
379
|
} else {
|
|
380
|
+
// Remembered so a crash later in this process can still activate
|
|
381
|
+
// it (activatePendingVersion) — JS never will.
|
|
382
|
+
unactivatedGeneration = resetGeneration;
|
|
383
|
+
unactivatedHash = hash;
|
|
224
384
|
Log.i(UpdateContext.TAG,
|
|
225
385
|
"native check: downloaded " + hash + ", activation left to JS");
|
|
226
386
|
}
|
|
@@ -246,7 +406,7 @@ final class NativeCheckOrchestrator {
|
|
|
246
406
|
.callTimeout(15, TimeUnit.SECONDS)
|
|
247
407
|
.build();
|
|
248
408
|
|
|
249
|
-
private static String httpRequest(String url, String postBody) {
|
|
409
|
+
private static String httpRequest(String url, String postBody, long deadlineNanos) {
|
|
250
410
|
try {
|
|
251
411
|
Request.Builder builder =
|
|
252
412
|
new Request.Builder().url(url).header("Accept", "application/json");
|
|
@@ -254,7 +414,22 @@ final class NativeCheckOrchestrator {
|
|
|
254
414
|
builder.post(RequestBody.create(
|
|
255
415
|
postBody, MediaType.parse("application/json; charset=utf-8")));
|
|
256
416
|
}
|
|
257
|
-
|
|
417
|
+
OkHttpClient client = httpClient;
|
|
418
|
+
if (deadlineNanos > 0) {
|
|
419
|
+
// Crash-rescue budget: never let a single request outlive the
|
|
420
|
+
// handler's hold window.
|
|
421
|
+
long remainingMillis = TimeUnit.NANOSECONDS.toMillis(
|
|
422
|
+
deadlineNanos - System.nanoTime());
|
|
423
|
+
if (remainingMillis <= 0) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if (remainingMillis < 15000) {
|
|
427
|
+
client = httpClient.newBuilder()
|
|
428
|
+
.callTimeout(remainingMillis, TimeUnit.MILLISECONDS)
|
|
429
|
+
.build();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
try (Response response = client.newCall(builder.build()).execute()) {
|
|
258
433
|
if (!response.isSuccessful() || response.body() == null) {
|
|
259
434
|
return null;
|
|
260
435
|
}
|
|
@@ -291,7 +466,9 @@ final class NativeCheckOrchestrator {
|
|
|
291
466
|
* already-tried) for one more round. No hedged race on purpose — this
|
|
292
467
|
* path is latency-insensitive.
|
|
293
468
|
*/
|
|
294
|
-
private static String runCheckRequest(
|
|
469
|
+
private static String runCheckRequest(
|
|
470
|
+
JSONObject config, String appKey, String body, long deadlineNanos
|
|
471
|
+
) {
|
|
295
472
|
JSONArray endpoints = config.optJSONArray("endpoints");
|
|
296
473
|
String orderedJson = NativeUpdateFlow.orderEndpointCandidates(
|
|
297
474
|
endpoints == null ? "[]" : endpoints.toString(), Math.random());
|
|
@@ -311,7 +488,8 @@ final class NativeCheckOrchestrator {
|
|
|
311
488
|
if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
|
|
312
489
|
return null;
|
|
313
490
|
}
|
|
314
|
-
String response = httpRequest(
|
|
491
|
+
String response = httpRequest(
|
|
492
|
+
base + "/checkUpdate/" + appKey, body, deadlineNanos);
|
|
315
493
|
if (isValidCheckResponse(response)) {
|
|
316
494
|
return response;
|
|
317
495
|
}
|
|
@@ -328,7 +506,7 @@ final class NativeCheckOrchestrator {
|
|
|
328
506
|
if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
|
|
329
507
|
return null;
|
|
330
508
|
}
|
|
331
|
-
String listText = httpRequest(listUrl, null);
|
|
509
|
+
String listText = httpRequest(listUrl, null, deadlineNanos);
|
|
332
510
|
if (listText == null) {
|
|
333
511
|
continue;
|
|
334
512
|
}
|
|
@@ -347,7 +525,8 @@ final class NativeCheckOrchestrator {
|
|
|
347
525
|
return null;
|
|
348
526
|
}
|
|
349
527
|
tried.add(base);
|
|
350
|
-
String response = httpRequest(
|
|
528
|
+
String response = httpRequest(
|
|
529
|
+
base + "/checkUpdate/" + appKey, body, deadlineNanos);
|
|
351
530
|
if (isValidCheckResponse(response)) {
|
|
352
531
|
return response;
|
|
353
532
|
}
|
|
@@ -358,14 +537,23 @@ final class NativeCheckOrchestrator {
|
|
|
358
537
|
return null;
|
|
359
538
|
}
|
|
360
539
|
|
|
540
|
+
private static long capToRescueBudget(long phaseDeadlineNanos, long rescueDeadlineNanos) {
|
|
541
|
+
if (rescueDeadlineNanos <= 0) {
|
|
542
|
+
return phaseDeadlineNanos;
|
|
543
|
+
}
|
|
544
|
+
return Math.min(phaseDeadlineNanos, rescueDeadlineNanos);
|
|
545
|
+
}
|
|
546
|
+
|
|
361
547
|
private static boolean performAttempts(
|
|
362
|
-
UpdateContext context, JSONArray attempts, String hash, String originHash
|
|
548
|
+
UpdateContext context, JSONArray attempts, String hash, String originHash,
|
|
549
|
+
long rescueDeadlineNanos
|
|
363
550
|
) {
|
|
364
551
|
if (attempts == null) {
|
|
365
552
|
return false;
|
|
366
553
|
}
|
|
367
|
-
final long incrementalDeadlineNanos =
|
|
368
|
-
+ TimeUnit.SECONDS.toNanos(DOWNLOAD_PHASE_TIMEOUT_SECONDS)
|
|
554
|
+
final long incrementalDeadlineNanos = capToRescueBudget(
|
|
555
|
+
System.nanoTime() + TimeUnit.SECONDS.toNanos(DOWNLOAD_PHASE_TIMEOUT_SECONDS),
|
|
556
|
+
rescueDeadlineNanos);
|
|
369
557
|
long fullDeadlineNanos = 0;
|
|
370
558
|
for (int i = 0; i < attempts.length(); i++) {
|
|
371
559
|
JSONObject attempt = attempts.optJSONObject(i);
|
|
@@ -381,8 +569,10 @@ final class NativeCheckOrchestrator {
|
|
|
381
569
|
if (isFullAttempt && fullDeadlineNanos == 0) {
|
|
382
570
|
// Incremental failures must not consume the last-resort full
|
|
383
571
|
// download's budget. Each phase gets one bounded 10min window.
|
|
384
|
-
fullDeadlineNanos =
|
|
385
|
-
|
|
572
|
+
fullDeadlineNanos = capToRescueBudget(
|
|
573
|
+
System.nanoTime()
|
|
574
|
+
+ TimeUnit.SECONDS.toNanos(DOWNLOAD_PHASE_TIMEOUT_SECONDS),
|
|
575
|
+
rescueDeadlineNanos);
|
|
386
576
|
}
|
|
387
577
|
final long deadlineNanos = isFullAttempt
|
|
388
578
|
? fullDeadlineNanos : incrementalDeadlineNanos;
|
|
@@ -425,6 +425,12 @@ public class UpdateContext {
|
|
|
425
425
|
return sp.getString(key, null);
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
void removeKv(String key) {
|
|
429
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
430
|
+
editor.remove(key);
|
|
431
|
+
persistEditor(editor, "remove key " + key);
|
|
432
|
+
}
|
|
433
|
+
|
|
428
434
|
public String getCurrentVersion() {
|
|
429
435
|
return sp.getString("currentVersion", null);
|
|
430
436
|
}
|