Skip to content

Commit 1cb6d90

Browse files
committed
iOS: roll back stuck pending updates before JS/Fabric starts, fix crash
Moves the rollback decision for a stuck (isLoading==YES) pending update from -initializeUpdateAfterRestart (runs mid bundle-eval, after JS/Fabric already exist) into +bundleURLForResource:..., the bridge's source-URL provider, which runs before any JS or Fabric surface exists. This removes the in-process reload that raced RN 0.86's Fabric surface teardown/recreate internals and crashed CI intermittently on localPackage.install.revert.dorevert (EXC_BAD_ACCESS / SIGSEGV). The rollback itself (+[CodePushPackage rollbackPackage] + bookkeeping) is extracted into a new +[CodePush rollbackPendingUpdate] class method, callable with no bridge/instance around. The old -rollbackPackage instance method (which called -loadBundle to trigger the racy reload) is now dead and removed; -saveFailedUpdate: became a class method since it is shared by both the download-failure path and the new rollback path. Note: the IMMEDIATE-install/restartApp() reload path has the same underlying Fabric race and is not addressed here; that needs a separate follow-up.
1 parent 87fe9e8 commit 1cb6d90

1 file changed

Lines changed: 27 additions & 24 deletions

File tree

ios/CodePush/CodePush.m

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ + (NSURL *)bundleURLForResource:(NSString *)resourceName
157157
bundleResourceSubdirectory = resourceSubdirectory;
158158
bundleResourceBundle = resourceBundle;
159159

160+
// If a pending update never finished loading (i.e. it crashed before calling
161+
// notifyApplicationReady), roll it back here, before any JS/Fabric exists to
162+
// race against. See docs/IOS_FABRIC_RELOAD_RACE.md.
163+
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
164+
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
165+
if (pendingUpdate && [pendingUpdate[PendingUpdateIsLoadingKey] boolValue]) {
166+
CPLog(@"Update did not finish loading the last time, rolling back to a previous version.");
167+
[self rollbackPendingUpdate];
168+
}
169+
160170
[self ensureBinaryBundleExists];
161171

162172
NSString *logMessageFormat = @"Loading JS bundle from %@";
@@ -400,20 +410,14 @@ - (void)initializeUpdateAfterRestart
400410
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
401411
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
402412
if (pendingUpdate) {
413+
// A stuck (isLoading == YES) pending update is rolled back earlier, in
414+
// +bundleURLForResource:..., before this method ever runs. So by this point,
415+
// any pending update here is one that's ready to be tried out.
403416
_isFirstRunAfterUpdate = YES;
404-
BOOL updateIsLoading = [pendingUpdate[PendingUpdateIsLoadingKey] boolValue];
405-
if (updateIsLoading) {
406-
// Pending update was initialized, but notifyApplicationReady was not called.
407-
// Therefore, deduce that it is a broken update and rollback.
408-
CPLog(@"Update did not finish loading the last time, rolling back to a previous version.");
409-
needToReportRollback = YES;
410-
[self rollbackPackage];
411-
} else {
412-
// Mark that we tried to initialize the new update, so that if it crashes,
413-
// we will know that we need to rollback when the app next starts.
414-
[self savePendingUpdate:pendingUpdate[PendingUpdateHashKey]
415-
isLoading:YES];
416-
}
417+
// Mark that we tried to initialize the new update, so that if it crashes,
418+
// we will know that we need to rollback when the app next starts.
419+
[self savePendingUpdate:pendingUpdate[PendingUpdateHashKey]
420+
isLoading:YES];
417421
}
418422
}
419423

@@ -546,13 +550,12 @@ - (void)loadBundle
546550
}
547551

548552
/*
549-
* This method is used when an update has failed installation
550-
* and the app needs to be rolled back to the previous bundle.
551-
* This method is automatically called when the rollback timer
552-
* expires without the app indicating whether the update succeeded,
553-
* and therefore, it shouldn't be called directly.
553+
* This method is used when a pending update never finished loading (i.e. it
554+
* crashed before calling notifyApplicationReady) and needs to be rolled back
555+
* to the previous bundle. It's called from +bundleURLForResource:..., before
556+
* any JS/Fabric exists, so there's no live bridge to reload.
554557
*/
555-
- (void)rollbackPackage
558+
+ (void)rollbackPendingUpdate
556559
{
557560
NSError *error;
558561
NSDictionary *failedPackage = [CodePushPackage getCurrentPackage:&error];
@@ -569,18 +572,18 @@ - (void)rollbackPackage
569572

570573
// Rollback to the previous version and de-register the new update
571574
[CodePushPackage rollbackPackage];
572-
[CodePush removePendingUpdate];
573-
[self loadBundle];
575+
[self removePendingUpdate];
576+
needToReportRollback = YES;
574577
}
575578

576579
/*
577580
* When an update failed to apply, this method can be called
578581
* to store its hash so that it can be ignored on future
579582
* attempts to check the server for an update.
580583
*/
581-
- (void)saveFailedUpdate:(NSDictionary *)failedPackage
584+
+ (void)saveFailedUpdate:(NSDictionary *)failedPackage
582585
{
583-
if ([[self class] isFailedHash:[failedPackage objectForKey:PackageHashKey]]) {
586+
if ([self isFailedHash:[failedPackage objectForKey:PackageHashKey]]) {
584587
return;
585588
}
586589

@@ -760,7 +763,7 @@ -(void)loadBundleOnTick:(NSTimer *)timer {
760763
// The download failed
761764
failCallback:^(NSError *err) {
762765
if ([CodePushErrorUtils isCodePushError:err]) {
763-
[self saveFailedUpdate:mutableUpdatePackage];
766+
[[self class] saveFailedUpdate:mutableUpdatePackage];
764767
}
765768

766769
// Stop observing frame updates if the download fails.

0 commit comments

Comments
 (0)