Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit c4b96ed

Browse files
committed
Merge pull request #71 from Microsoft/fix-rollback
Fix rollback
2 parents 75fa7dc + 20fd5be commit c4b96ed

File tree

2 files changed

+128
-155
lines changed

2 files changed

+128
-155
lines changed

CodePush.m

Lines changed: 50 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#import "CodePush.h"
88

99
@implementation CodePush {
10-
BOOL _resumablePendingUpdateAvailable;
10+
BOOL _hasResumeListener;
1111
}
1212

1313
RCT_EXPORT_MODULE()
@@ -84,55 +84,6 @@ - (void)cancelRollbackTimer
8484
});
8585
}
8686

87-
/*
88-
* This method checks to see whether a "pending update" has been applied
89-
* (e.g. install was called with a non-immediate mode), but the app hasn't
90-
* yet been restarted (either naturally or programmatically). If there is one,
91-
* it will restart the app (if specified), and start the rollback timer.
92-
*
93-
* Note: This method is safe to call from any thread.
94-
*/
95-
- (void)checkForPendingUpdate:(BOOL)needsRestart
96-
{
97-
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
98-
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
99-
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
100-
101-
if (pendingUpdate) {
102-
NSError *error;
103-
NSString *pendingHash = pendingUpdate[PendingUpdateHashKey];
104-
NSString *currentHash = [CodePushPackage getCurrentPackageHash:&error];
105-
106-
NSAssert([pendingHash isEqualToString:currentHash], @"There is a pending update but it's hash doesn't match that of the current package.");
107-
108-
// Kick off the rollback timer and ensure that the necessary state is setup for the pending update.
109-
int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue];
110-
[self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:needsRestart];
111-
112-
// Clear the pending update and sync
113-
[preferences removeObjectForKey:PendingUpdateKey];
114-
[preferences synchronize];
115-
}
116-
});
117-
}
118-
119-
/*
120-
* This method is meant as a handler for the global app
121-
* resume notification, and therefore, should not be called
122-
* directly. It simply checks to see whether there is a pending
123-
* update that is meant to be installed on resume, and if so
124-
* it applies it and restarts the app.
125-
*/
126-
- (void)checkForPendingUpdateDuringResume
127-
{
128-
// In order to ensure that CodePush doesn't impact the app's
129-
// resume experience, we're using a simple boolean check to
130-
// check whether we need to restart, before reading the defaults store
131-
if (_resumablePendingUpdateAvailable) {
132-
[self checkForPendingUpdate:YES];
133-
}
134-
}
135-
13687
/*
13788
* This method is used by the React Native bridge to allow
13889
* our plugin to expose constants to the JS-side. In our case
@@ -162,43 +113,33 @@ - (instancetype)init
162113
self = [super init];
163114

164115
if (self) {
165-
// Do an async check to see whether
166-
// we need to start the rollback timer
167-
// due to a pending update being installed at start
168-
[self checkForPendingUpdate:NO];
169-
170-
// Register for app resume notifications so that we
171-
// can check for pending updates which support "restart on resume"
172-
[[NSNotificationCenter defaultCenter] addObserver:self
173-
selector:@selector(checkForPendingUpdateDuringResume)
174-
name:UIApplicationWillEnterForegroundNotification
175-
object:[UIApplication sharedApplication]];
116+
[self initializeUpdateAfterRestart];
176117
}
177118

178119
return self;
179120
}
180121

181122
/*
182-
* This method performs the actual initialization work for an update
183-
* to ensure that the necessary state is setup, including:
184-
* --------------------------------------------------------
185-
* 1. Updating the current bundle URL to point at the latest update on disk
186-
* 2. Optionally restarting the app to load the new bundle
187-
* 3. Optionally starting the rollback protection timer
123+
* This method starts the rollback protection timer
124+
* and is used when a new update is initialized.
188125
*/
189-
- (void)initializeUpdateWithRollbackTimeout:(int)rollbackTimeout
190-
needsRestart:(BOOL)needsRestart
126+
- (void)initializeUpdateAfterRestart
191127
{
192-
didUpdate = YES;
193-
194-
if (needsRestart) {
195-
[self loadBundle];
196-
}
128+
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
129+
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
197130

198-
if (0 != rollbackTimeout) {
199-
dispatch_async(dispatch_get_main_queue(), ^{
200-
[self startRollbackTimer:rollbackTimeout];
201-
});
131+
if (pendingUpdate) {
132+
didUpdate = true;
133+
int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue];
134+
if (0 != rollbackTimeout) {
135+
dispatch_async(dispatch_get_main_queue(), ^{
136+
[self startRollbackTimer:rollbackTimeout];
137+
});
138+
}
139+
140+
// Clear the pending update and sync
141+
[preferences removeObjectForKey:PendingUpdateKey];
142+
[preferences synchronize];
202143
}
203144
}
204145

@@ -390,10 +331,22 @@ - (void)startRollbackTimer:(int)rollbackTimeout
390331
if (error) {
391332
reject(error);
392333
} else {
393-
if (installMode != CodePushInstallModeImmediate) {
394-
_resumablePendingUpdateAvailable = (installMode == CodePushInstallModeOnNextResume);
395-
[self savePendingUpdate:updatePackage[@"packageHash"]
396-
rollbackTimeout:rollbackTimeout];
334+
[self savePendingUpdate:updatePackage[@"packageHash"]
335+
rollbackTimeout:rollbackTimeout];
336+
337+
if (installMode == CodePushInstallModeImmediate) {
338+
[self loadBundle];
339+
} else if (installMode == CodePushInstallModeOnNextResume) {
340+
// Ensure we do not add the listener twice.
341+
if (!_hasResumeListener) {
342+
// Register for app resume notifications so that we
343+
// can check for pending updates which support "restart on resume"
344+
[[NSNotificationCenter defaultCenter] addObserver:self
345+
selector:@selector(loadBundle)
346+
name:UIApplicationWillEnterForegroundNotification
347+
object:[UIApplication sharedApplication]];
348+
_hasResumeListener = true;
349+
}
397350
}
398351
// Signal to JS that the update has been applied.
399352
resolve(nil);
@@ -446,15 +399,28 @@ - (void)startRollbackTimer:(int)rollbackTimeout
446399
*/
447400
RCT_EXPORT_METHOD(restartImmediateUpdate:(int)rollbackTimeout)
448401
{
449-
[self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:YES];
402+
[self loadBundle];
450403
}
451404

452405
/*
453406
* This method is the native side of the CodePush.restartPendingUpdate() method.
454407
*/
455408
RCT_EXPORT_METHOD(restartPendingUpdate)
456409
{
457-
[self checkForPendingUpdate:YES];
410+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
411+
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
412+
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
413+
414+
if (pendingUpdate) {
415+
NSError *error;
416+
NSString *pendingHash = pendingUpdate[PendingUpdateHashKey];
417+
NSString *currentHash = [CodePushPackage getCurrentPackageHash:&error];
418+
419+
NSAssert([pendingHash isEqualToString:currentHash], @"There is a pending update but it's hash doesn't match that of the current package.");
420+
421+
[self loadBundle];
422+
}
423+
});
458424
}
459425

460426
RCT_EXPORT_METHOD(setUsingTestFolder:(BOOL)shouldUseTestFolder)

android/app/src/main/java/com/microsoft/codepush/react/CodePush.java

Lines changed: 78 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.microsoft.codepush.react;
22

33
import com.facebook.react.ReactPackage;
4+
import com.facebook.react.bridge.ActivityEventListener;
45
import com.facebook.react.bridge.JavaScriptModule;
56
import com.facebook.react.bridge.LifecycleEventListener;
67
import com.facebook.react.bridge.NativeModule;
@@ -41,7 +42,6 @@
4142

4243
public class CodePush {
4344

44-
private boolean resumablePendingUpdateAvailable = false;
4545
private boolean didUpdate = false;
4646
private Timer timer;
4747
private boolean usingTestFolder = false;
@@ -87,7 +87,7 @@ public CodePush(String deploymentKey, Activity mainActivity) {
8787
throw new CodePushUnknownException("Unable to get package info for " + applicationContext.getPackageName(), e);
8888
}
8989

90-
checkForPendingUpdate(/*needsRestart*/ false);
90+
initializeUpdateAfterRestart();
9191
}
9292

9393
public ReactPackage getReactPackage() {
@@ -144,52 +144,6 @@ private void cancelRollbackTimer() {
144144
}
145145
}
146146

147-
private void checkForPendingUpdate(boolean needsRestart) {
148-
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
149-
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
150-
151-
if (pendingUpdateString != null) {
152-
try {
153-
JSONObject pendingUpdateJSON = new JSONObject(pendingUpdateString);
154-
String pendingHash = pendingUpdateJSON.getString(PENDING_UPDATE_HASH_KEY);
155-
String currentHash = codePushPackage.getCurrentPackageHash();
156-
if (!pendingHash.equals(currentHash)) {
157-
throw new CodePushUnknownException("Pending hash " + pendingHash +
158-
" and current hash " + currentHash + " are different");
159-
}
160-
161-
int rollbackTimeout = pendingUpdateJSON.getInt(PENDING_UPDATE_ROLLBACK_TIMEOUT_KEY);
162-
initializeUpdateWithRollbackTimeout(rollbackTimeout, needsRestart);
163-
settings.edit().remove(PENDING_UPDATE_KEY).commit();
164-
} catch (JSONException e) {
165-
// Should not happen.
166-
throw new CodePushUnknownException("Unable to parse pending update metadata " +
167-
pendingUpdateString + " stored in SharedPreferences", e);
168-
} catch (IOException e) {
169-
// There is no current package hash.
170-
throw new CodePushUnknownException("Should not register a pending update without a saving a current package", e);
171-
}
172-
}
173-
}
174-
175-
private void checkForPendingUpdateDuringResume() {
176-
if (resumablePendingUpdateAvailable) {
177-
checkForPendingUpdate(/*needsRestart*/ true);
178-
}
179-
}
180-
181-
private void initializeUpdateWithRollbackTimeout(int rollbackTimeout, boolean needsRestart) {
182-
didUpdate = true;
183-
184-
if (needsRestart) {
185-
codePushNativeModule.loadBundle();
186-
}
187-
188-
if (0 != rollbackTimeout) {
189-
startRollbackTimer(rollbackTimeout);
190-
}
191-
}
192-
193147
private boolean isFailedHash(String packageHash) {
194148
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
195149
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
@@ -277,8 +231,32 @@ public void run() {
277231
}, timeout);
278232
}
279233

234+
private void initializeUpdateAfterRestart() {
235+
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
236+
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
237+
238+
if (pendingUpdateString != null) {
239+
try {
240+
didUpdate = true;
241+
JSONObject pendingUpdateJSON = new JSONObject(pendingUpdateString);
242+
int rollbackTimeout = pendingUpdateJSON.getInt(PENDING_UPDATE_ROLLBACK_TIMEOUT_KEY);
243+
if (0 != rollbackTimeout) {
244+
startRollbackTimer(rollbackTimeout);
245+
}
246+
247+
settings.edit().remove(PENDING_UPDATE_KEY).commit();
248+
} catch (JSONException e) {
249+
// Should not happen.
250+
throw new CodePushUnknownException("Unable to parse pending update metadata " +
251+
pendingUpdateString + " stored in SharedPreferences", e);
252+
}
253+
}
254+
}
255+
280256
private class CodePushNativeModule extends ReactContextBaseJavaModule {
281257

258+
private LifecycleEventListener lifecycleEventListener = null;
259+
282260
private void loadBundle() {
283261
Intent intent = mainActivity.getIntent();
284262
mainActivity.finish();
@@ -289,15 +267,37 @@ private void loadBundle() {
289267
public void installUpdate(ReadableMap updatePackage, int rollbackTimeout, int installMode, Promise promise) {
290268
try {
291269
codePushPackage.installPackage(updatePackage);
270+
271+
String pendingHash = CodePushUtils.tryGetString(updatePackage, codePushPackage.PACKAGE_HASH_KEY);
272+
if (pendingHash == null) {
273+
throw new CodePushUnknownException("Update package to be installed has no hash.");
274+
} else {
275+
savePendingUpdate(pendingHash, rollbackTimeout);
276+
}
277+
292278
if (installMode != CodePushInstallMode.IMMEDIATE.getValue()) {
293-
resumablePendingUpdateAvailable = installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue();
294-
String pendingHash = CodePushUtils.tryGetString(updatePackage, codePushPackage.PACKAGE_HASH_KEY);
295-
if (pendingHash == null) {
296-
throw new CodePushUnknownException("Update package to be installed has no hash.");
297-
} else {
298-
savePendingUpdate(pendingHash, rollbackTimeout);
279+
loadBundle();
280+
} else if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue()) {
281+
// Ensure we do not add the listener twice.
282+
if (lifecycleEventListener == null) {
283+
lifecycleEventListener = new LifecycleEventListener() {
284+
@Override
285+
public void onHostResume() {
286+
loadBundle();
287+
}
288+
289+
@Override
290+
public void onHostPause() {
291+
}
292+
293+
@Override
294+
public void onHostDestroy() {
295+
}
296+
};
297+
getReactApplicationContext().addLifecycleEventListener(lifecycleEventListener);
299298
}
300299
}
300+
301301
promise.resolve("");
302302
} catch (IOException e) {
303303
e.printStackTrace();
@@ -377,12 +377,34 @@ public void setUsingTestFolder(boolean shouldUseTestFolder) {
377377

378378
@ReactMethod
379379
public void restartImmediateUpdate(int rollbackTimeout) {
380-
initializeUpdateWithRollbackTimeout(rollbackTimeout, /*needsRestart*/ true);
380+
loadBundle();
381381
}
382382

383383
@ReactMethod
384384
public void restartPendingUpdate() {
385-
checkForPendingUpdate(/*needsRestart*/ true);
385+
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
386+
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
387+
388+
if (pendingUpdateString != null) {
389+
try {
390+
JSONObject pendingUpdateJSON = new JSONObject(pendingUpdateString);
391+
String pendingHash = pendingUpdateJSON.getString(PENDING_UPDATE_HASH_KEY);
392+
String currentHash = codePushPackage.getCurrentPackageHash();
393+
if (!pendingHash.equals(currentHash)) {
394+
throw new CodePushUnknownException("Pending hash " + pendingHash +
395+
" and current hash " + currentHash + " are different");
396+
}
397+
398+
loadBundle();
399+
} catch (JSONException e) {
400+
// Should not happen.
401+
throw new CodePushUnknownException("Unable to parse pending update metadata " +
402+
pendingUpdateString + " stored in SharedPreferences", e);
403+
} catch (IOException e) {
404+
// There is no current package hash.
405+
throw new CodePushUnknownException("Should not register a pending update without a saving a current package", e);
406+
}
407+
}
386408
}
387409

388410
@Override
@@ -414,21 +436,6 @@ public List<NativeModule> createNativeModules(ReactApplicationContext reactAppli
414436
nativeModules.add(CodePush.this.codePushNativeModule);
415437
nativeModules.add(dialogModule);
416438

417-
reactApplicationContext.addLifecycleEventListener(new LifecycleEventListener() {
418-
@Override
419-
public void onHostResume() {
420-
CodePush.this.checkForPendingUpdateDuringResume();
421-
}
422-
423-
@Override
424-
public void onHostPause() {
425-
}
426-
427-
@Override
428-
public void onHostDestroy() {
429-
}
430-
});
431-
432439
return nativeModules;
433440
}
434441

0 commit comments

Comments
 (0)