Skip to content

Commit 0cc712c

Browse files
committed
iOS: gate CodePush-triggered reloads on a Fabric readiness signal
1 parent b9d0351 commit 0cc712c

1 file changed

Lines changed: 121 additions & 10 deletions

File tree

ios/CodePush/CodePush.m

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#if __has_include(<React/RCTAssert.h>)
22
#import <React/RCTAssert.h>
33
#import <React/RCTBridgeModule.h>
4+
#import <React/RCTConstants.h>
45
#import <React/RCTConvert.h>
56
#import <React/RCTEventDispatcher.h>
67
#import <React/RCTRootView.h>
@@ -36,6 +37,11 @@ @implementation CodePush {
3637
BOOL _allowed;
3738
BOOL _restartInProgress;
3839
NSMutableArray *_restartQueue;
40+
41+
// Reload readiness tracking. See -registerSettleObservers for details.
42+
BOOL _instanceSettled;
43+
// YES exactly while a reload is parked waiting for the instance to settle.
44+
BOOL _reloadPending;
3945
}
4046

4147
RCT_EXPORT_MODULE()
@@ -45,6 +51,11 @@ @implementation CodePush {
4551
// These constants represent emitted events
4652
static NSString *const DownloadProgressEvent = @"CodePushDownloadProgress";
4753

54+
// How long a parked reload waits for a settle signal before reloading anyway.
55+
// See -registerSettleObservers for why this exists and why the exact value is
56+
// not critical.
57+
static const NSTimeInterval SettleTimeout = 5.0;
58+
4859
// These constants represent valid deployment statuses
4960
static NSString *const DeploymentFailed = @"DeploymentFailed";
5061
static NSString *const DeploymentSucceeded = @"DeploymentSucceeded";
@@ -395,15 +406,88 @@ - (instancetype)init
395406
_allowed = YES;
396407
_restartInProgress = NO;
397408
_restartQueue = [NSMutableArray arrayWithCapacity:1];
398-
409+
399410
self = [super init];
400411
if (self) {
412+
[self registerSettleObservers];
401413
[self initializeUpdateAfterRestart];
402414
}
403415

404416
return self;
405417
}
406418

419+
#pragma mark - Immediate update + reload readiness tracking
420+
421+
/*
422+
* In case of an immediate bundle update, we need to tear down the current
423+
* RCTInstance at the right time.
424+
*
425+
* Right after bundle evaluation, RN enqueues -[RCTFabricSurface start] on a
426+
* background queue for that same instance. Tearing the instance down while
427+
* that block is in flight crashes inside RN's mounting layer. This module is
428+
* initialized *during* bundle evaluation, so in case of immediate update
429+
* mode, -loadBundle needs to park the reload until we see the surface get
430+
* past its startup.
431+
*
432+
* The two signals below are the possible outcomes of an instance's startup:
433+
* - RCTContentDidAppearNotification: Fabric posts this from
434+
* RCTRootComponentView on the first child mount. That needs a JS render
435+
* and commit, so by that time we are good to tear things down.
436+
* - RCTJavaScriptDidFailToLoadNotification: the bundle never evaluated, so
437+
* no surface startup will follow.
438+
*
439+
* Note: neither signal is guaranteed, so we also need a fallback timeout. If
440+
* the first render returns nil and mounts no child, RCTContentDidAppear
441+
* never arrives. Think of a splash gate or a fonts/auth loader. That kind of
442+
* app could run codepush.sync() with an IMMEDIATE install before it shows
443+
* any UI.
444+
*
445+
* Running the teardown after the timeout fires is safe because the delay is
446+
* large enough that the teardown already happened by that time.
447+
*/
448+
- (void)registerSettleObservers
449+
{
450+
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
451+
[center addObserver:self
452+
selector:@selector(instanceDidSettle)
453+
name:RCTContentDidAppearNotification
454+
object:nil];
455+
[center addObserver:self
456+
selector:@selector(instanceDidSettle)
457+
name:RCTJavaScriptDidFailToLoadNotification
458+
object:nil];
459+
}
460+
461+
- (void)instanceDidSettle
462+
{
463+
// Note: A reload tears the instance down synchronously.
464+
// Calling -releasePendingReload directly would destroy the surface partway through
465+
// the mount that just notified us.
466+
dispatch_async(dispatch_get_main_queue(), ^{
467+
self->_instanceSettled = YES;
468+
469+
if (self->_reloadPending) {
470+
CPLog(@"Instance settled. Restarting app.");
471+
[self releasePendingReload];
472+
}
473+
});
474+
}
475+
476+
/*
477+
* Fires a parked reload, if one is still parked. Called both by the settle
478+
* signal and by the timeout in -loadBundle, so whichever arrives first wins and
479+
* the other becomes a no-op. Main queue only.
480+
*/
481+
- (void)releasePendingReload
482+
{
483+
if (!_reloadPending) {
484+
return;
485+
}
486+
487+
_reloadPending = NO;
488+
[self performBundleReload];
489+
}
490+
407491
/*
408492
* This method is used when the app is started to either
409493
* initialize a pending update or rollback a faulty update
@@ -547,18 +631,45 @@ - (void)loadBundle
547631
// This needs to be async dispatched because the bridge is not set on init
548632
// when the app first starts, therefore rollbacks will not take effect.
549633
dispatch_async(dispatch_get_main_queue(), ^{
550-
// If the current bundle URL is using http(s), then assume the dev
551-
// is debugging and therefore, shouldn't be redirected to a local
552-
// file (since Chrome wouldn't support it). Otherwise, update
553-
// the current bundle URL to point at the latest update
554-
if ([CodePush isUsingTestConfiguration] || ![super.bridge.bundleURL.scheme hasPrefix:@"http"]) {
555-
[super.bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"];
634+
if (!self->_instanceSettled) {
635+
CPLog(@"Restart deferred until the current instance has settled.");
636+
self->_reloadPending = YES;
637+
638+
// Weak, so that a module outliving the teardown of its own instance (because of the timer)
639+
// cannot reload against a bridge that no longer belongs to it.
640+
__weak __typeof(self) weakSelf = self;
641+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SettleTimeout * NSEC_PER_SEC)),
642+
dispatch_get_main_queue(), ^{
643+
__typeof(self) strongSelf = weakSelf;
644+
if (strongSelf && strongSelf->_reloadPending) {
645+
CPLog(@"Timed out waiting for the current instance to settle. Restarting app anyway.");
646+
[strongSelf releasePendingReload];
647+
}
648+
});
649+
return;
556650
}
557651

558-
RCTTriggerReloadCommandListeners(@"react-native-code-push: Restart");
652+
[self performBundleReload];
559653
});
560654
}
561655

656+
/*
657+
* Performs the actual reload. Must be called on the main queue, and only once the
658+
* current instance has settled or the wait for it timed out. See -loadBundle.
659+
*/
660+
- (void)performBundleReload
661+
{
662+
// If the current bundle URL is using http(s), then assume the dev
663+
// is debugging and therefore, shouldn't be redirected to a local
664+
// file (since Chrome wouldn't support it). Otherwise, update
665+
// the current bundle URL to point at the latest update
666+
if ([CodePush isUsingTestConfiguration] || ![super.bridge.bundleURL.scheme hasPrefix:@"http"]) {
667+
[super.bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"];
668+
}
669+
670+
RCTTriggerReloadCommandListeners(@"react-native-code-push: Restart");
671+
}
672+
562673
/*
563674
* This method is used when a pending update never finished loading (i.e. it
564675
* crashed before calling notifyApplicationReady) and needs to be rolled back
@@ -804,7 +915,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
804915

805916
_restartInProgress = NO;
806917
if ([_restartQueue count] > 0) {
807-
BOOL buf = [_restartQueue valueForKey: @"@firstObject"];
918+
BOOL buf = [[_restartQueue firstObject] boolValue];
808919
[_restartQueue removeObjectAtIndex:0];
809920
[self restartAppInternal:buf];
810921
}
@@ -1010,7 +1121,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
10101121

10111122
if ([_restartQueue count] > 0) {
10121123
CPLog(@"Executing pending restart.");
1013-
BOOL buf = [_restartQueue valueForKey: @"@firstObject"];
1124+
BOOL buf = [[_restartQueue firstObject] boolValue];
10141125
[_restartQueue removeObjectAtIndex:0];
10151126
[self restartAppInternal:buf];
10161127
}

0 commit comments

Comments
 (0)