react-native-update 10.51.1 → 10.52.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/NATIVE_CHECKUPDATE_DESIGN.md +117 -0
- package/NATIVE_CHECK_FOLLOWUPS.md +149 -0
- package/README-CN.md +2 -0
- package/README.md +2 -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/cpp/PushyTurboModule.cpp +14 -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
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -47,6 +47,10 @@ static NSString *const keyNativeConfig = @"REACTNATIVECN_PUSHY_NATIVE_CONFIG_KEY
|
|
|
47
47
|
// Raw response cache written by the native cold-start check for the JS side
|
|
48
48
|
// to reuse (§10.3), scoped to the request and config that produced it.
|
|
49
49
|
static NSString *const keyNativeCheckCache = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_RESP_KEY";
|
|
50
|
+
// Set when a native check round starts, cleared when it ends (§11.4). Residue
|
|
51
|
+
// on the next launch means the previous process died mid-round (a crash
|
|
52
|
+
// rescue was truncated): that launch resumes immediately instead of waiting.
|
|
53
|
+
static NSString *const keyNativeCheckIncomplete = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_INCOMPLETE_KEY";
|
|
50
54
|
static NSString *const PushyErrorDomain = @"cn.reactnative.pushy";
|
|
51
55
|
|
|
52
56
|
// file def
|
|
@@ -471,7 +475,9 @@ static void PushySwitchVersionLocked(NSString *hash) {
|
|
|
471
475
|
// launch. Decisions come from cpp/update_flow_core; this class is IO glue.
|
|
472
476
|
@interface RCTPushyOrchestrator : NSObject
|
|
473
477
|
+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion;
|
|
474
|
-
+ (void)
|
|
478
|
+
+ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime;
|
|
479
|
+
+ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime;
|
|
480
|
+
+ (void)runRescueWithDeadline:(NSTimeInterval)deadlineUptime;
|
|
475
481
|
+ (BOOL)commitRoundWithGeneration:(uint64_t)generation
|
|
476
482
|
hashInfo:(NSDictionary *)hashInfoEntry
|
|
477
483
|
activate:(NSString *)hashToActivate
|
|
@@ -481,6 +487,102 @@ static void PushySwitchVersionLocked(NSString *hash) {
|
|
|
481
487
|
responseAt:(long long)responseAtSeconds;
|
|
482
488
|
@end
|
|
483
489
|
|
|
490
|
+
// One round per process, whoever starts it first — the delayed cold-start
|
|
491
|
+
// path or the crash-rescue thread (§11.3). The semaphore lets the rescue
|
|
492
|
+
// wait out an in-flight round instead of racing it.
|
|
493
|
+
static std::atomic<bool> pushyRoundStarted{false};
|
|
494
|
+
static std::atomic<bool> pushyRoundCompleted{false};
|
|
495
|
+
// Flipped the moment a crash is being held. JS is dead from that point on,
|
|
496
|
+
// so there is no second decision maker: the round force-activates whatever
|
|
497
|
+
// it downloads (§11.3).
|
|
498
|
+
static std::atomic<bool> pushyCrashRescueActive{false};
|
|
499
|
+
static std::atomic<bool> pushyRescueAttempted{false};
|
|
500
|
+
static dispatch_semaphore_t pushyRoundDone;
|
|
501
|
+
static NSString *pushyLaunchRolledBackForRescue = nil;
|
|
502
|
+
// A version this process downloaded but left for JS to activate. If the
|
|
503
|
+
// process then crashes, JS will never activate it — the crash handler
|
|
504
|
+
// activates it directly (bounded local work, no network). The generation is
|
|
505
|
+
// the one the round committed under: a reset that lands afterwards bumps it,
|
|
506
|
+
// and the late activation must lose to that reset exactly like the round
|
|
507
|
+
// itself would. Guarded by @synchronized (RCTPushyOrchestrator class).
|
|
508
|
+
static NSString *pushyUnactivatedHash = nil;
|
|
509
|
+
static uint64_t pushyUnactivatedGeneration = 0;
|
|
510
|
+
static NSUncaughtExceptionHandler *pushyPreviousExceptionHandler = NULL;
|
|
511
|
+
// ≈ process start: scheduleFromColdStart runs during the first bundleURL.
|
|
512
|
+
static NSTimeInterval pushyProcessAnchorUptime = 0;
|
|
513
|
+
|
|
514
|
+
static const NSTimeInterval kPushyRescueTriggerUptime = 60;
|
|
515
|
+
static const NSTimeInterval kPushyRescueBudgetBackgroundThread = 10;
|
|
516
|
+
// A held main thread freezes UI teardown; stay well under the watchdog.
|
|
517
|
+
static const NSTimeInterval kPushyRescueBudgetMainThread = 3.5;
|
|
518
|
+
|
|
519
|
+
static void PushyMaybeHoldForRescue(void) {
|
|
520
|
+
// Once per process; a second crashing thread passes straight through
|
|
521
|
+
// instead of waiting behind the first (§11.3: prefer under-rescuing over
|
|
522
|
+
// wedging the teardown).
|
|
523
|
+
bool expected = false;
|
|
524
|
+
if (!pushyRescueAttempted.compare_exchange_strong(expected, true)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
NSTimeInterval uptime = PushyMonotonicNow() - pushyProcessAnchorUptime;
|
|
528
|
+
bool roundInFlight = pushyRoundStarted.load() && !pushyRoundCompleted.load();
|
|
529
|
+
// Early crashes are the brick signature; a crash with an in-flight round
|
|
530
|
+
// is worth finishing regardless of uptime. Everything else is an ordinary
|
|
531
|
+
// crash whose UX must not be delayed.
|
|
532
|
+
if (uptime >= kPushyRescueTriggerUptime && !roundInFlight) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
NSTimeInterval budget = [NSThread isMainThread]
|
|
536
|
+
? kPushyRescueBudgetMainThread
|
|
537
|
+
: kPushyRescueBudgetBackgroundThread;
|
|
538
|
+
NSTimeInterval deadline = PushyMonotonicNow() + budget;
|
|
539
|
+
NSLog(@"RCTPushy -- crash rescue: holding process for up to %.1fs "
|
|
540
|
+
@"(uptime %.1fs)", budget, uptime);
|
|
541
|
+
|
|
542
|
+
// The rescue runs on its own queue and the dying thread only waits with a
|
|
543
|
+
// hard timeout: even a deadlocked rescue (a thread that died holding a
|
|
544
|
+
// lock — uncaught ObjC exceptions skip @finally) can only delay the
|
|
545
|
+
// process death, never prevent it.
|
|
546
|
+
dispatch_semaphore_t done = dispatch_semaphore_create(0);
|
|
547
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
548
|
+
@try {
|
|
549
|
+
[RCTPushyOrchestrator runRescueWithDeadline:deadline];
|
|
550
|
+
} @catch (NSException *exception) {
|
|
551
|
+
NSLog(@"RCTPushy -- crash rescue failed: %@", exception.reason);
|
|
552
|
+
}
|
|
553
|
+
dispatch_semaphore_signal(done);
|
|
554
|
+
});
|
|
555
|
+
if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW,
|
|
556
|
+
(int64_t)(budget * NSEC_PER_SEC))) != 0) {
|
|
557
|
+
NSLog(@"RCTPushy -- crash rescue: budget exhausted, letting go");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Crash-moment brick rescue (§11): when the app is dying of an uncaught
|
|
562
|
+
// exception during startup (RCTFatal raises NSException in release), the
|
|
563
|
+
// process is still alive and JS will never run again — a natural,
|
|
564
|
+
// false-positive-free window to finish the cold-start check. The previous
|
|
565
|
+
// handler (crash reporters chain the same way) always runs afterwards.
|
|
566
|
+
static void PushyCrashRescueExceptionHandler(NSException *exception) {
|
|
567
|
+
@try {
|
|
568
|
+
PushyMaybeHoldForRescue();
|
|
569
|
+
} @catch (NSException *inner) {
|
|
570
|
+
// The dying process owes the previous handler its turn no matter
|
|
571
|
+
// what the rescue did.
|
|
572
|
+
}
|
|
573
|
+
if (pushyPreviousExceptionHandler != NULL) {
|
|
574
|
+
pushyPreviousExceptionHandler(exception);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
static void PushyInstallCrashRescueHandler(void) {
|
|
579
|
+
static dispatch_once_t onceToken;
|
|
580
|
+
dispatch_once(&onceToken, ^{
|
|
581
|
+
pushyPreviousExceptionHandler = NSGetUncaughtExceptionHandler();
|
|
582
|
+
NSSetUncaughtExceptionHandler(&PushyCrashRescueExceptionHandler);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
484
586
|
// Shared by the getBundleHash RCT method and the native cold-start check.
|
|
485
587
|
// Returns @"" when unknown; blocking (sha256 of the embedded bundle on first
|
|
486
588
|
// call per install, cached afterwards) — call off the main thread.
|
|
@@ -1300,6 +1402,11 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1300
1402
|
progressHandler:nil
|
|
1301
1403
|
completionHandler:^(NSString *archivePath, BOOL succeeded, NSError *error) {
|
|
1302
1404
|
[fileManager removeItemAtPath:archivePath error:nil];
|
|
1405
|
+
// The resume sidecar dies with its archive (§11.4): whether the
|
|
1406
|
+
// unzip consumed it or classified it as poisoned, nothing must
|
|
1407
|
+
// survive to vouch for bytes that are gone.
|
|
1408
|
+
[fileManager removeItemAtPath:[archivePath stringByAppendingString:@".resume"]
|
|
1409
|
+
error:nil];
|
|
1303
1410
|
if (completionHandler == nil) {
|
|
1304
1411
|
return;
|
|
1305
1412
|
}
|
|
@@ -1471,21 +1578,115 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1471
1578
|
#if !DEBUG
|
|
1472
1579
|
// Once per process; a few seconds of delay keeps the check away from the
|
|
1473
1580
|
// cold-start critical path (§7 R5) — its result targets the NEXT launch.
|
|
1581
|
+
// Unless the previous process died mid-round (residual incomplete
|
|
1582
|
+
// marker), in which case every launch second counts (§11.4).
|
|
1474
1583
|
static dispatch_once_t onceToken;
|
|
1475
1584
|
dispatch_once(&onceToken, ^{
|
|
1476
|
-
|
|
1585
|
+
pushyRoundDone = dispatch_semaphore_create(0);
|
|
1586
|
+
pushyProcessAnchorUptime = PushyMonotonicNow();
|
|
1587
|
+
pushyLaunchRolledBackForRescue = [launchRolledBackVersion copy];
|
|
1588
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
1589
|
+
// The crash-hold rescue shares the orchestrator's rollout gate: no
|
|
1590
|
+
// persisted config, no handler (§11.3).
|
|
1591
|
+
if ([defaults stringForKey:keyNativeConfig].length > 0) {
|
|
1592
|
+
PushyInstallCrashRescueHandler();
|
|
1593
|
+
}
|
|
1594
|
+
int64_t delaySeconds =
|
|
1595
|
+
[defaults objectForKey:keyNativeCheckIncomplete] != nil ? 0 : 5;
|
|
1596
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delaySeconds * NSEC_PER_SEC),
|
|
1477
1597
|
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
1478
|
-
|
|
1479
|
-
[self runOnce:launchRolledBackVersion];
|
|
1480
|
-
} @catch (NSException *exception) {
|
|
1481
|
-
// The rescue path must never take the app down with it.
|
|
1482
|
-
RCTLogWarn(@"RCTPushy -- native check crashed: %@", exception.reason);
|
|
1483
|
-
}
|
|
1598
|
+
[self startRoundWithDeadline:0];
|
|
1484
1599
|
});
|
|
1485
1600
|
});
|
|
1486
1601
|
#endif
|
|
1487
1602
|
}
|
|
1488
1603
|
|
|
1604
|
+
// Runs the process's single round on the calling thread if nobody has
|
|
1605
|
+
// started it yet. deadlineUptime > 0 (crash rescue) caps every HTTP call and
|
|
1606
|
+
// download phase to the remaining budget.
|
|
1607
|
+
+ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime {
|
|
1608
|
+
bool expected = false;
|
|
1609
|
+
if (!pushyRoundStarted.compare_exchange_strong(expected, true)) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
@try {
|
|
1613
|
+
[self runOnce:pushyLaunchRolledBackForRescue deadline:deadlineUptime];
|
|
1614
|
+
} @catch (NSException *exception) {
|
|
1615
|
+
// The rescue path must never take the app down with it.
|
|
1616
|
+
RCTLogWarn(@"RCTPushy -- native check crashed: %@", exception.reason);
|
|
1617
|
+
} @finally {
|
|
1618
|
+
pushyRoundCompleted.store(true);
|
|
1619
|
+
dispatch_semaphore_signal(pushyRoundDone);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Crash-rescue entry (§11.3), called from the handler's worker queue while
|
|
1624
|
+
// the uncaught-exception handler holds the dying process. Ensures this
|
|
1625
|
+
// process's round runs to completion within the budget, then activates a
|
|
1626
|
+
// downloaded-but-unactivated version if one exists — the last chance before
|
|
1627
|
+
// the process is gone.
|
|
1628
|
+
+ (void)runRescueWithDeadline:(NSTimeInterval)deadlineUptime {
|
|
1629
|
+
pushyCrashRescueActive.store(true);
|
|
1630
|
+
[self startRoundWithDeadline:deadlineUptime];
|
|
1631
|
+
if (!pushyRoundCompleted.load()) {
|
|
1632
|
+
NSTimeInterval remaining = deadlineUptime - PushyMonotonicNow();
|
|
1633
|
+
if (remaining > 0) {
|
|
1634
|
+
dispatch_semaphore_wait(pushyRoundDone, dispatch_time(DISPATCH_TIME_NOW,
|
|
1635
|
+
(int64_t)(remaining * NSEC_PER_SEC)));
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
[self activatePendingVersion];
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// The alert-strategy variant of the §10.7 hole: the round downloaded a fix
|
|
1642
|
+
// but deferred activation to JS, and JS is now dead. Activation is local and
|
|
1643
|
+
// bounded (a state switch under the commit lock).
|
|
1644
|
+
+ (void)activatePendingVersion {
|
|
1645
|
+
NSString *hash = nil;
|
|
1646
|
+
uint64_t generation = 0;
|
|
1647
|
+
@synchronized (self) {
|
|
1648
|
+
hash = pushyUnactivatedHash;
|
|
1649
|
+
generation = pushyUnactivatedGeneration;
|
|
1650
|
+
}
|
|
1651
|
+
if (hash == nil) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
// Unlike Android's switchVersion, PushySwitchVersionLocked does not
|
|
1655
|
+
// validate the version directory; never point the next launch at bytes
|
|
1656
|
+
// that are gone (e.g. wiped by a reset whose generation bump we would
|
|
1657
|
+
// catch below anyway, or by cleanup).
|
|
1658
|
+
NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash];
|
|
1659
|
+
if (!PushyHasCompletedVersionAtPath(versionDir)) {
|
|
1660
|
+
NSLog(@"RCTPushy -- crash rescue: version %@ no longer on disk, dropping activation", hash);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
NSDictionary *hashInfoEntry = nil;
|
|
1664
|
+
NSString *existingInfo = [PushyDefaults() stringForKey:PushyHashInfoKey(hash)];
|
|
1665
|
+
NSData *existingData = [existingInfo dataUsingEncoding:NSUTF8StringEncoding];
|
|
1666
|
+
id parsed = existingData == nil ? nil :
|
|
1667
|
+
[NSJSONSerialization JSONObjectWithData:existingData options:0 error:nil];
|
|
1668
|
+
if ([parsed isKindOfClass:[NSDictionary class]]) {
|
|
1669
|
+
NSMutableDictionary *merged = [parsed mutableCopy];
|
|
1670
|
+
merged[@"crashRescue"] = @YES;
|
|
1671
|
+
hashInfoEntry = @{@"hash": hash, @"info": merged};
|
|
1672
|
+
}
|
|
1673
|
+
BOOL committed = [self commitRoundWithGeneration:generation
|
|
1674
|
+
hashInfo:hashInfoEntry
|
|
1675
|
+
activate:hash
|
|
1676
|
+
responseText:nil
|
|
1677
|
+
request:nil
|
|
1678
|
+
config:nil
|
|
1679
|
+
responseAt:0];
|
|
1680
|
+
if (committed) {
|
|
1681
|
+
@synchronized (self) {
|
|
1682
|
+
pushyUnactivatedHash = nil;
|
|
1683
|
+
}
|
|
1684
|
+
NSLog(@"RCTPushy -- crash rescue: activated downloaded version %@", hash);
|
|
1685
|
+
} else {
|
|
1686
|
+
NSLog(@"RCTPushy -- crash rescue: reset since download, dropping activation");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1489
1690
|
// A bare module instance drives the existing download/patch pipeline without
|
|
1490
1691
|
// a bridge: the file queue is process-global and progress events are gated on
|
|
1491
1692
|
// hasListeners (never set without a bridge).
|
|
@@ -1498,7 +1699,7 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1498
1699
|
return engine;
|
|
1499
1700
|
}
|
|
1500
1701
|
|
|
1501
|
-
+ (void)runOnce:(NSString *)launchRolledBackVersion {
|
|
1702
|
+
+ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime {
|
|
1502
1703
|
// Sampled before any IO: resetToPackagedBundle bumps it, and a reset that
|
|
1503
1704
|
// lands while this round is running must win over the round's decision.
|
|
1504
1705
|
const uint64_t resetGeneration = pushyResetGeneration.load();
|
|
@@ -1518,6 +1719,28 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1518
1719
|
if (appKey.length == 0) {
|
|
1519
1720
|
return;
|
|
1520
1721
|
}
|
|
1722
|
+
// From here on the round does real work: leave the breadcrumb that the
|
|
1723
|
+
// next launch reads to skip its 5s delay if we die mid-round (§11.4).
|
|
1724
|
+
[defaults setObject:@YES forKey:keyNativeCheckIncomplete];
|
|
1725
|
+
@try {
|
|
1726
|
+
[self runConfiguredRound:config
|
|
1727
|
+
configJson:configJson
|
|
1728
|
+
appKey:appKey
|
|
1729
|
+
launchRolledBackVersion:launchRolledBackVersion
|
|
1730
|
+
resetGeneration:resetGeneration
|
|
1731
|
+
deadline:deadlineUptime];
|
|
1732
|
+
} @finally {
|
|
1733
|
+
[defaults removeObjectForKey:keyNativeCheckIncomplete];
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
+ (void)runConfiguredRound:(const flowjson::Value &)config
|
|
1738
|
+
configJson:(NSString *)configJson
|
|
1739
|
+
appKey:(NSString *)appKey
|
|
1740
|
+
launchRolledBackVersion:(NSString *)launchRolledBackVersion
|
|
1741
|
+
resetGeneration:(uint64_t)resetGeneration
|
|
1742
|
+
deadline:(NSTimeInterval)deadlineUptime {
|
|
1743
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
1521
1744
|
NSString *packageVersion =
|
|
1522
1745
|
PushyFromStdString(config.Get("packageVersion").AsString());
|
|
1523
1746
|
if (packageVersion.length == 0) {
|
|
@@ -1573,7 +1796,10 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1573
1796
|
return;
|
|
1574
1797
|
}
|
|
1575
1798
|
|
|
1576
|
-
NSString *responseText = [self runCheckRequest:config
|
|
1799
|
+
NSString *responseText = [self runCheckRequest:config
|
|
1800
|
+
appKey:appKey
|
|
1801
|
+
body:body
|
|
1802
|
+
deadline:deadlineUptime];
|
|
1577
1803
|
if (responseText == nil) {
|
|
1578
1804
|
RCTLogInfo(@"RCTPushy -- native check: no endpoint reachable, giving up until next launch");
|
|
1579
1805
|
return;
|
|
@@ -1605,7 +1831,8 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1605
1831
|
if (!downloaded) {
|
|
1606
1832
|
downloaded = [self performAttempts:decision.Get("attempts")
|
|
1607
1833
|
hash:hash
|
|
1608
|
-
originHash:currentVersion
|
|
1834
|
+
originHash:currentVersion
|
|
1835
|
+
deadline:deadlineUptime];
|
|
1609
1836
|
}
|
|
1610
1837
|
if (!downloaded) {
|
|
1611
1838
|
[self commitRoundWithGeneration:resetGeneration
|
|
@@ -1627,10 +1854,22 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1627
1854
|
versionInfo[@(key)] = PushyFromStdString(info.Get(key).AsString()) ?: @"";
|
|
1628
1855
|
}
|
|
1629
1856
|
}
|
|
1857
|
+
// A forceBoot activation is the brick-rescue path: mark it in the
|
|
1858
|
+
// persisted info so JS can report force_boot_rescue when this version
|
|
1859
|
+
// survives to markSuccess. Only the server-sent directive counts — a
|
|
1860
|
+
// silent-strategy activation is ordinary delivery.
|
|
1861
|
+
if (info.Get("config").Get("forceBoot").Truthy()) {
|
|
1862
|
+
versionInfo[@"forceBootRescue"] = @YES;
|
|
1863
|
+
}
|
|
1864
|
+
if (pushyCrashRescueActive.load()) {
|
|
1865
|
+
versionInfo[@"crashRescue"] = @YES;
|
|
1866
|
+
}
|
|
1630
1867
|
// Silent strategies or a server-marked forceBoot version (per-version
|
|
1631
1868
|
// remote override — the brick rescue) activate for the next launch;
|
|
1632
|
-
// otherwise activation stays with the JS side (§6/§10.1).
|
|
1633
|
-
|
|
1869
|
+
// otherwise activation stays with the JS side (§6/§10.1). Unless a crash
|
|
1870
|
+
// is being held: JS is dead, deferring to it would leave the fix on disk
|
|
1871
|
+
// forever (§11.3).
|
|
1872
|
+
BOOL activate = decision.Get("activate").Truthy() || pushyCrashRescueActive.load();
|
|
1634
1873
|
BOOL committed = [self commitRoundWithGeneration:resetGeneration
|
|
1635
1874
|
hashInfo:@{@"hash": hash, @"info": versionInfo}
|
|
1636
1875
|
activate:activate ? hash : nil
|
|
@@ -1641,8 +1880,17 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1641
1880
|
if (!committed) {
|
|
1642
1881
|
RCTLogInfo(@"RCTPushy -- native check: reset during round, dropping result");
|
|
1643
1882
|
} else if (activate) {
|
|
1883
|
+
@synchronized (self) {
|
|
1884
|
+
pushyUnactivatedHash = nil;
|
|
1885
|
+
}
|
|
1644
1886
|
RCTLogInfo(@"RCTPushy -- native check: downloaded %@ and set for next launch", hash);
|
|
1645
1887
|
} else {
|
|
1888
|
+
// Remembered so a crash later in this process can still activate it
|
|
1889
|
+
// (activatePendingVersion) — JS never will.
|
|
1890
|
+
@synchronized (self) {
|
|
1891
|
+
pushyUnactivatedHash = [hash copy];
|
|
1892
|
+
pushyUnactivatedGeneration = resetGeneration;
|
|
1893
|
+
}
|
|
1646
1894
|
RCTLogInfo(@"RCTPushy -- native check: downloaded %@, activation left to JS", hash);
|
|
1647
1895
|
}
|
|
1648
1896
|
}
|
|
@@ -1659,13 +1907,18 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1659
1907
|
request:(NSString *)requestBody
|
|
1660
1908
|
config:(NSString *)configJson
|
|
1661
1909
|
responseAt:(long long)responseAtSeconds {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
@
|
|
1667
|
-
|
|
1668
|
-
|
|
1910
|
+
// responseText is nil for the crash handler's activation-only commit
|
|
1911
|
+
// (activatePendingVersion): no round ran, so there is no cache to write.
|
|
1912
|
+
NSData *cacheData = nil;
|
|
1913
|
+
if (responseText != nil) {
|
|
1914
|
+
NSDictionary *cacheEntry = @{
|
|
1915
|
+
@"ts": @(responseAtSeconds),
|
|
1916
|
+
@"body": responseText,
|
|
1917
|
+
@"request": requestBody,
|
|
1918
|
+
@"config": configJson,
|
|
1919
|
+
};
|
|
1920
|
+
cacheData = [NSJSONSerialization dataWithJSONObject:cacheEntry options:0 error:nil];
|
|
1921
|
+
}
|
|
1669
1922
|
__block BOOL committed = NO;
|
|
1670
1923
|
PushyWithStateLock(^{
|
|
1671
1924
|
if (pushyResetGeneration.load() != generation) {
|
|
@@ -1697,9 +1950,19 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1697
1950
|
// time with its own timeout; after the configured round fails, queryUrls
|
|
1698
1951
|
// discovery merges remote candidates (excluding the already-tried) for one
|
|
1699
1952
|
// more round. No hedged race on purpose — this path is latency-insensitive.
|
|
1953
|
+
// Per-request timeout, capped to the crash-rescue budget when one is active.
|
|
1954
|
+
// <= 0 means the budget is gone and the round must stop issuing requests.
|
|
1955
|
+
static NSTimeInterval PushyCheckRequestTimeout(NSTimeInterval deadlineUptime) {
|
|
1956
|
+
if (deadlineUptime <= 0) {
|
|
1957
|
+
return 10;
|
|
1958
|
+
}
|
|
1959
|
+
return MIN(10, deadlineUptime - PushyMonotonicNow());
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1700
1962
|
+ (NSString *)runCheckRequest:(const flowjson::Value &)config
|
|
1701
1963
|
appKey:(NSString *)appKey
|
|
1702
|
-
body:(NSString *)body
|
|
1964
|
+
body:(NSString *)body
|
|
1965
|
+
deadline:(NSTimeInterval)deadlineUptime {
|
|
1703
1966
|
double sample = arc4random() / 4294967296.0;
|
|
1704
1967
|
flowjson::Value ordered =
|
|
1705
1968
|
updateflow::OrderEndpointCandidates(config.Get("endpoints"), sample);
|
|
@@ -1716,9 +1979,13 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1716
1979
|
return nil;
|
|
1717
1980
|
}
|
|
1718
1981
|
[tried addObject:base];
|
|
1982
|
+
NSTimeInterval timeout = PushyCheckRequestTimeout(deadlineUptime);
|
|
1983
|
+
if (timeout <= 0) {
|
|
1984
|
+
return nil;
|
|
1985
|
+
}
|
|
1719
1986
|
NSString *response = PushyHttpRequest(
|
|
1720
1987
|
[NSString stringWithFormat:@"%@/checkUpdate/%@", base, appKey],
|
|
1721
|
-
@"POST", body,
|
|
1988
|
+
@"POST", body, timeout);
|
|
1722
1989
|
if (PushyIsValidCheckResponse(response)) {
|
|
1723
1990
|
return response;
|
|
1724
1991
|
}
|
|
@@ -1731,7 +1998,11 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1731
1998
|
if (httpAttempts++ >= maxHttpAttempts) {
|
|
1732
1999
|
return nil;
|
|
1733
2000
|
}
|
|
1734
|
-
|
|
2001
|
+
NSTimeInterval listTimeout = PushyCheckRequestTimeout(deadlineUptime);
|
|
2002
|
+
if (listTimeout <= 0) {
|
|
2003
|
+
return nil;
|
|
2004
|
+
}
|
|
2005
|
+
NSString *listText = PushyHttpRequest(listUrl, @"GET", nil, listTimeout);
|
|
1735
2006
|
if (listText == nil) {
|
|
1736
2007
|
continue;
|
|
1737
2008
|
}
|
|
@@ -1750,9 +2021,13 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1750
2021
|
return nil;
|
|
1751
2022
|
}
|
|
1752
2023
|
[tried addObject:base];
|
|
2024
|
+
NSTimeInterval timeout = PushyCheckRequestTimeout(deadlineUptime);
|
|
2025
|
+
if (timeout <= 0) {
|
|
2026
|
+
return nil;
|
|
2027
|
+
}
|
|
1753
2028
|
NSString *response = PushyHttpRequest(
|
|
1754
2029
|
[NSString stringWithFormat:@"%@/checkUpdate/%@", base, appKey],
|
|
1755
|
-
@"POST", body,
|
|
2030
|
+
@"POST", body, timeout);
|
|
1756
2031
|
if (PushyIsValidCheckResponse(response)) {
|
|
1757
2032
|
return response;
|
|
1758
2033
|
}
|
|
@@ -1764,9 +2039,14 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1764
2039
|
|
|
1765
2040
|
+ (BOOL)performAttempts:(const flowjson::Value &)attempts
|
|
1766
2041
|
hash:(NSString *)hash
|
|
1767
|
-
originHash:(NSString *)originHash
|
|
2042
|
+
originHash:(NSString *)originHash
|
|
2043
|
+
deadline:(NSTimeInterval)rescueDeadline {
|
|
1768
2044
|
RCTPushy *engine = [self engine];
|
|
2045
|
+
// Crash-rescue budget caps every phase; 0 keeps the normal 10min windows.
|
|
1769
2046
|
NSTimeInterval incrementalDeadline = PushyMonotonicNow() + 600;
|
|
2047
|
+
if (rescueDeadline > 0) {
|
|
2048
|
+
incrementalDeadline = MIN(incrementalDeadline, rescueDeadline);
|
|
2049
|
+
}
|
|
1770
2050
|
NSTimeInterval fullDeadline = 0;
|
|
1771
2051
|
for (const auto &attempt : attempts.elements()) {
|
|
1772
2052
|
const std::string &type = attempt.Get("type").AsString();
|
|
@@ -1780,6 +2060,9 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
1780
2060
|
if (isFullAttempt && fullDeadline == 0) {
|
|
1781
2061
|
// diff/pdiff cannot starve the last-resort full download.
|
|
1782
2062
|
fullDeadline = PushyMonotonicNow() + 600;
|
|
2063
|
+
if (rescueDeadline > 0) {
|
|
2064
|
+
fullDeadline = MIN(fullDeadline, rescueDeadline);
|
|
2065
|
+
}
|
|
1783
2066
|
}
|
|
1784
2067
|
NSTimeInterval deadline = isFullAttempt ? fullDeadline : incrementalDeadline;
|
|
1785
2068
|
for (const auto &urlValue : attempt.Get("urls").elements()) {
|