@@ -754,7 +754,8 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation
754754 responseText : (NSString *)responseText
755755 request : (NSString *)requestBody
756756 config : (NSString *)configJson
757- responseAt : (long long )responseAtSeconds ;
757+ responseAt : (long long )responseAtSeconds
758+ activated : (BOOL *)activatedOut ;
758759@end
759760
760761// One round per process, whoever starts it first β the delayed cold-start
@@ -767,9 +768,15 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation
767768// it downloads (Β§11.3).
768769static std::atomic<bool > pushyCrashRescueActive{false };
769770static std::atomic<bool > pushyRescueAttempted{false };
770- // Set when a launch blocks on reinstalling a version tvOS purged: the round
771- // activates what it downloads, JS has not started to decide.
771+ // Set when this process's round is the tvOS purged-version restore.
772772static std::atomic<bool > pushyPurgeRestoreActive{false };
773+ // True while +bundleURL waits for that restore. Only then may the round
774+ // activate what it downloads on its own (JS has not started to decide), since
775+ // the launch resolves the bundle again afterwards. Closed once the wait ends,
776+ // timeout or not; guarded by the state lock that every activation commits
777+ // under, so a commit lands either before the close (and launches) or after it
778+ // (and is left to JS) β never selected behind the packaged bundle's back.
779+ static bool pushyPurgeRestoreWindowOpen = false ;
773780static dispatch_semaphore_t pushyRoundDone;
774781static NSString *pushyLaunchRolledBackForRescue = nil ;
775782// A version this process downloaded but left for JS to activate. If the
@@ -2374,12 +2381,13 @@ + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion {
23742381
23752382#if TARGET_OS_TV && !DEBUG
23762383// Runs this process's round before the first bundle loads, blocking the
2377- // caller for at most kPushyPurgeRestoreBudget, and activates whatever it
2378- // installs. The state no longer has a current version, so the server answers
2379- // with the latest version for the packaged bundle. Returns YES when the
2380- // round finished in time β the caller then resolves the launch bundle again.
2381- // The round stops at the same deadline, keeping a partial download for the
2382- // resumable retry of the next check; the launch then uses the packaged bundle.
2384+ // caller for at most kPushyPurgeRestoreBudget. The state no longer has a
2385+ // current version, so the server answers with the latest version for the
2386+ // packaged bundle, and the round activates it while the restore window is
2387+ // open. Returns NO when no round can run; otherwise the caller must resolve
2388+ // the launch bundle again: it launches whatever was activated before the
2389+ // window closed, or the packaged bundle. The round stops at the same
2390+ // deadline, keeping a partial download for the next check to resume.
23832391+ (BOOL )restorePurgedLaunch : (NSString *)purgedVersion
23842392 rolledBack : (NSString *)launchRolledBackVersion {
23852393 [self prepareProcess: launchRolledBackVersion];
@@ -2388,23 +2396,31 @@ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion
23882396 @" configured; launching the packaged bundle" , purgedVersion);
23892397 return NO ;
23902398 }
2399+ PushyWithStateLock (^{
2400+ pushyPurgeRestoreWindowOpen = true ;
2401+ });
23912402 pushyPurgeRestoreActive.store (true );
23922403 NSTimeInterval deadline = PushyMonotonicNow () + kPushyPurgeRestoreBudget ;
23932404 dispatch_semaphore_t done = dispatch_semaphore_create (0 );
23942405 dispatch_async (dispatch_get_global_queue (QOS_CLASS_USER_INITIATED , 0 ), ^{
23952406 [self startRoundWithDeadline: deadline];
23962407 dispatch_semaphore_signal (done);
23972408 });
2398- // The round honours the deadline itself; the extra second only covers
2399- // its last commit.
2400- if (dispatch_semaphore_wait (done, dispatch_time (DISPATCH_TIME_NOW ,
2401- (int64_t )((kPushyPurgeRestoreBudget + 1 ) * NSEC_PER_SEC ))) != 0 ) {
2402- RCTLogWarn (@" RCTPushy -- purged version restore timed out; launching the packaged bundle" );
2403- return NO ;
2409+ BOOL timedOut = dispatch_semaphore_wait (done, dispatch_time (DISPATCH_TIME_NOW ,
2410+ (int64_t )(kPushyPurgeRestoreBudget * NSEC_PER_SEC ))) != 0 ;
2411+ // The round's deadline does not bound a slow response (the request
2412+ // timeout is an idle timeout) nor a version already on disk, so the close
2413+ // is what stops a late activation β not the deadline.
2414+ PushyWithStateLock (^{
2415+ pushyPurgeRestoreWindowOpen = false ;
2416+ });
2417+ if (timedOut) {
2418+ RCTLogWarn (@" RCTPushy -- purged version %@ restore timed out" , purgedVersion);
2419+ } else {
2420+ NSDictionary *result = pushyHostRoundResult;
2421+ RCTLogInfo (@" RCTPushy -- purged version %@ restore: %@ %@ %@ " , purgedVersion,
2422+ result[@" status" ], result[@" reason" ], result[@" hash" ]);
24042423 }
2405- NSDictionary *result = pushyHostRoundResult;
2406- RCTLogInfo (@" RCTPushy -- purged version %@ restore: %@ %@ %@ " , purgedVersion,
2407- result[@" status" ], result[@" reason" ], result[@" hash" ]);
24082424 return YES ;
24092425}
24102426#endif
@@ -2555,7 +2571,8 @@ + (void)activatePendingVersion {
25552571 responseText: nil
25562572 request: nil
25572573 config: nil
2558- responseAt: 0 ];
2574+ responseAt: 0
2575+ activated: NULL ];
25592576 if (committed) {
25602577 @synchronized (self) {
25612578 pushyUnactivatedHash = nil ;
@@ -2705,7 +2722,8 @@ + (void)runConfiguredRound:(const flowjson::Value &)config
27052722 responseText: responseText
27062723 request: body
27072724 config: configJson
2708- responseAt: responseAtSeconds];
2725+ responseAt: responseAtSeconds
2726+ activated: NULL ];
27092727 pushyHostRoundResult = committed
27102728 ? PushyHostResult (@" noUpdate" , PushyFromStdString (decision.Get (" reason" ).AsString ()), nil , NO )
27112729 : PushyHostResult (@" cancelled" , @" reset" , nil , NO );
@@ -2734,7 +2752,8 @@ + (void)runConfiguredRound:(const flowjson::Value &)config
27342752 responseText: responseText
27352753 request: body
27362754 config: configJson
2737- responseAt: responseAtSeconds];
2755+ responseAt: responseAtSeconds
2756+ activated: NULL ];
27382757 pushyHostRoundResult = committed
27392758 ? PushyHostResult (@" failed" , @" download_failed" , nil , NO )
27402759 : PushyHostResult (@" cancelled" , @" reset" , nil , NO );
@@ -2760,24 +2779,25 @@ + (void)runConfiguredRound:(const flowjson::Value &)config
27602779 if (pushyCrashRescueActive.load ()) {
27612780 versionInfo[@" crashRescue" ] = @YES ;
27622781 }
2763- if (pushyPurgeRestoreActive.load ()) {
2764- versionInfo[@" purgeRestore" ] = @YES ;
2765- }
27662782 // Silent strategies or a server-marked forceBoot version (per-version
27672783 // remote override β the brick rescue) activate for the next launch;
27682784 // otherwise activation stays with the JS side (Β§6/Β§10.1). Unless a crash
27692785 // is being held: JS is dead, deferring to it would leave the fix on disk
27702786 // forever (Β§11.3). Or the launch is blocked on reinstalling a version
2771- // tvOS purged: JS has not started, and the whole point is to boot it.
2787+ // tvOS purged: JS has not started, and the whole point is to boot it β
2788+ // commitRoundWithGeneration drops that one once the restore window closed.
27722789 BOOL activate = decision.Get (" activate" ).Truthy () || pushyCrashRescueActive.load ()
27732790 || pushyPurgeRestoreActive.load ();
2791+ BOOL activated = NO ;
27742792 BOOL committed = [self commitRoundWithGeneration: resetGeneration
27752793 hashInfo: @{@" hash" : hash, @" info" : versionInfo}
27762794 activate: activate ? hash : nil
27772795 responseText: responseText
27782796 request: body
27792797 config: configJson
2780- responseAt: responseAtSeconds];
2798+ responseAt: responseAtSeconds
2799+ activated: &activated];
2800+ activate = activated;
27812801 if (!committed) {
27822802 RCTLogInfo (@" RCTPushy -- native check: reset during round, dropping result" );
27832803 } else if (activate) {
@@ -2810,7 +2830,8 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation
28102830 responseText : (NSString *)responseText
28112831 request : (NSString *)requestBody
28122832 config : (NSString *)configJson
2813- responseAt : (long long )responseAtSeconds {
2833+ responseAt : (long long )responseAtSeconds
2834+ activated : (BOOL *)activatedOut {
28142835 // responseText is nil for the crash handler's activation-only commit
28152836 // (activatePendingVersion): no round ran, so there is no cache to write.
28162837 NSData *cacheData = nil ;
@@ -2824,29 +2845,51 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation
28242845 cacheData = [NSJSONSerialization dataWithJSONObject: cacheEntry options: 0 error: nil ];
28252846 }
28262847 __block BOOL committed = NO ;
2848+ __block BOOL activated = NO ;
28272849 PushyWithStateLock (^{
28282850 if (pushyResetGeneration.load () != generation) {
28292851 return ;
28302852 }
2853+ NSString *activation = hashToActivate;
2854+ NSDictionary *info = hashInfoEntry[@" info" ];
2855+ // The tvOS purged-version restore round (a round commit, not the
2856+ // crash handler's activation-only one): it may take over this launch
2857+ // only while +bundleURL still waits for it; after that the version is
2858+ // left to JS unless a crash is being held (JS is dead then).
2859+ if (responseText != nil && pushyPurgeRestoreActive.load ()) {
2860+ if (pushyPurgeRestoreWindowOpen) {
2861+ if (activation != nil && info != nil ) {
2862+ NSMutableDictionary *marked = [info mutableCopy ];
2863+ marked[@" purgeRestore" ] = @YES ;
2864+ info = marked;
2865+ }
2866+ } else if (!pushyCrashRescueActive.load ()) {
2867+ activation = nil ;
2868+ }
2869+ }
28312870 NSUserDefaults *defaults = PushyDefaults ();
28322871 if (hashInfoEntry != nil ) {
2833- NSData *infoData = [NSJSONSerialization dataWithJSONObject: hashInfoEntry[ @" info" ]
2872+ NSData *infoData = [NSJSONSerialization dataWithJSONObject: info
28342873 options: 0
28352874 error: nil ];
28362875 if (infoData != nil ) {
28372876 [defaults setObject: [[NSString alloc ] initWithData: infoData encoding: NSUTF8StringEncoding]
28382877 forKey: PushyHashInfoKey (hashInfoEntry[@" hash" ])];
28392878 }
28402879 }
2841- if (hashToActivate != nil ) {
2842- PushySwitchVersionLocked (hashToActivate);
2880+ if (activation != nil ) {
2881+ PushySwitchVersionLocked (activation);
2882+ activated = YES ;
28432883 }
28442884 if (cacheData != nil ) {
28452885 [defaults setObject: [[NSString alloc ] initWithData: cacheData encoding: NSUTF8StringEncoding]
28462886 forKey: keyNativeCheckCache];
28472887 }
28482888 committed = YES ;
28492889 });
2890+ if (activatedOut != NULL ) {
2891+ *activatedOut = activated;
2892+ }
28502893 return committed;
28512894}
28522895
0 commit comments