From 05697c40b7b32ce833f35f4376c14a81092e4d28 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Wed, 9 Dec 2015 18:49:45 -0800 Subject: [PATCH] remove rollback timeout --- CodePush.js | 3 +- CodePush.m | 73 +++++-------- .../microsoft/codepush/react/CodePush.java | 103 +++++++++--------- package-mixins.js | 4 +- 4 files changed, 84 insertions(+), 99 deletions(-) diff --git a/CodePush.js b/CodePush.js index 6b95b1a..2ed0ef3 100644 --- a/CodePush.js +++ b/CodePush.js @@ -167,7 +167,6 @@ function sync(options = {}, syncStatusChangeCallback, downloadProgressCallback) deploymentKey: null, ignoreFailedUpdates: true, installMode: CodePush.InstallMode.ON_NEXT_RESTART, - rollbackTimeout: 0, updateDialog: null, ...options @@ -230,7 +229,7 @@ function sync(options = {}, syncStatusChangeCallback, downloadProgressCallback) remotePackage.download(downloadProgressCallback) .then((localPackage) => { syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE); - return localPackage.install(syncOptions.rollbackTimeout, syncOptions.installMode, () => { + return localPackage.install(syncOptions.installMode, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); resolve(CodePush.SyncStatus.UPDATE_INSTALLED); }); diff --git a/CodePush.m b/CodePush.m index fa05255..811ef9f 100644 --- a/CodePush.m +++ b/CodePush.m @@ -22,7 +22,7 @@ static NSString *const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; // These keys are already "namespaced" by the PendingUpdateKey, so // their values don't need to be obfuscated to prevent collision with app data static NSString *const PendingUpdateHashKey = @"hash"; -static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; +static NSString *const PendingUpdateWasInitializedKey = @"wasInitialized"; @synthesize bridge = _bridge; @@ -70,20 +70,6 @@ static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; // Private API methods -/* - * This method cancels the currently running rollback - * timer, which has the effect of stopping an automatic - * rollback from occurring. - * - * Note: This method is safe to call from any thread. - */ -- (void)cancelRollbackTimer -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [_timer invalidate]; - }); -} - /* * This method is used by the React Native bridge to allow * our plugin to expose constants to the JS-side. In our case @@ -130,16 +116,17 @@ static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; if (pendingUpdate) { _isFirstRunAfterUpdate = YES; - int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue]; - if (0 != rollbackTimeout) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self startRollbackTimer:rollbackTimeout]; - }); + BOOL wasInitialized = [pendingUpdate[PendingUpdateWasInitializedKey] boolValue]; + if (wasInitialized) { + // Pending update was initialized, but notifiyApplicationReady was not called. + // Therefore, deduce that it is a broken update and rollback. + [self rollbackPackage]; + } else { + // Mark that we tried to initiazlie the new update, so that if it crashes, + // we will know that we need to rollback when the app next starts. + [self savePendingUpdate:pendingUpdate[PendingUpdateHashKey] + wasInitialized:YES]; } - - // Clear the pending update and sync - [preferences removeObjectForKey:PendingUpdateKey]; - [preferences synchronize]; } } @@ -189,7 +176,7 @@ static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; // Do the actual rollback and then // refresh the app with the previous package - [CodePushPackage rollbackPackage]; + [self removePendingUpdate]; [self loadBundle]; } @@ -215,39 +202,36 @@ static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [preferences synchronize]; } +/* + * This method is called in notifyApplicationReady to register the fact that + * the pending update succeeded and therefore can be removed. + */ +- (void)removePendingUpdate +{ + NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; + [preferences removeObjectForKey:PendingUpdateKey]; + [preferences synchronize]; +} + /* * When an update is installed whose mode isn't IMMEDIATE, this method - * can be called to store the pending update's metadata (e.g. rollbackTimeout) + * can be called to store the pending update's metadata (e.g. packageHash) * so that it can be used when the actual update application occurs at a later point. */ - (void)savePendingUpdate:(NSString *)packageHash - rollbackTimeout:(int)rollbackTimeout + wasInitialized:(BOOL)wasInitialized { // Since we're not restarting, we need to store the fact that the update // was installed, but hasn't yet become "active". NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSDictionary *pendingUpdate = [[NSDictionary alloc] initWithObjectsAndKeys: packageHash,PendingUpdateHashKey, - [NSNumber numberWithInt:rollbackTimeout],PendingUpdateRollbackTimeoutKey, nil]; + [NSNumber numberWithBool:wasInitialized],PendingUpdateWasInitializedKey, nil]; [preferences setObject:pendingUpdate forKey:PendingUpdateKey]; [preferences synchronize]; } -/* - * This method handles starting the actual rollback timer - * after an update has been installed. - */ -- (void)startRollbackTimer:(int)rollbackTimeout -{ - double timeoutInSeconds = rollbackTimeout / 1000; - _timer = [NSTimer scheduledTimerWithTimeInterval:timeoutInSeconds - target:self - selector:@selector(rollbackPackage) - userInfo:nil - repeats:NO]; -} - // JavaScript-exported module methods /* @@ -318,7 +302,6 @@ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve * This method is the native side of the LocalPackage.install method. */ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage - rollbackTimeout:(int)rollbackTimeout installMode:(CodePushInstallMode)installMode resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) @@ -332,7 +315,7 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage reject(error); } else { [self savePendingUpdate:updatePackage[@"packageHash"] - rollbackTimeout:rollbackTimeout]; + wasInitialized:NO]; if (installMode == CodePushInstallModeImmediate) { [self loadBundle]; @@ -389,7 +372,7 @@ RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [self cancelRollbackTimer]; + [self removePendingUpdate]; resolve([NSNull null]); } diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java index d34050e..7e26afa 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java @@ -1,7 +1,6 @@ package com.microsoft.codepush.react; import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.NativeModule; @@ -31,20 +30,15 @@ import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class CodePush { private boolean didUpdate = false; - private Timer timer; private boolean usingTestFolder = false; private String assetsBundleFileName; @@ -52,7 +46,7 @@ public class CodePush { private final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES"; private final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE"; private final String PENDING_UPDATE_HASH_KEY = "hash"; - private final String PENDING_UPDATE_ROLLBACK_TIMEOUT_KEY = "rollbackTimeout"; + private final String PENDING_UPDATE_WAS_INITIALIZED_KEY = "wasInitialized"; private final String ASSETS_BUNDLE_PREFIX = "assets://"; private final String CODE_PUSH_PREFERENCES = "CodePush"; private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; @@ -98,10 +92,6 @@ public class CodePush { return codePushReactPackage; } - public String getDocumentsDirectory() { - return codePushPackage.getDocumentsDirectory(); - } - public String getBundleUrl(String assetsBundleFileName) { this.assetsBundleFileName = assetsBundleFileName; String binaryJsBundleUrl = ASSETS_BUNDLE_PREFIX + assetsBundleFileName; @@ -138,13 +128,6 @@ public class CodePush { } } - private void cancelRollbackTimer() { - if(timer != null) { - timer.cancel(); - timer = null; - } - } - private boolean isFailedHash(String packageHash) { SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null); @@ -176,7 +159,10 @@ public class CodePush { throw new CodePushUnknownException("Error in rolling back package", e); } - codePushNativeModule.loadBundle(); + removePendingUpdate(); + if (codePushNativeModule != null) { + codePushNativeModule.loadBundle(); + } } private void saveFailedUpdate(String packageHash) { @@ -204,52 +190,60 @@ public class CodePush { } } - private void savePendingUpdate(String packageHash, int rollbackTimeout) { + private void removePendingUpdate() { + SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); + settings.edit().remove(PENDING_UPDATE_KEY).commit(); + } + + private void savePendingUpdate(String packageHash, boolean wasInitialized) { SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); JSONObject pendingUpdate = new JSONObject(); try { pendingUpdate.put(PENDING_UPDATE_HASH_KEY, packageHash); - pendingUpdate.put(PENDING_UPDATE_ROLLBACK_TIMEOUT_KEY, rollbackTimeout); + pendingUpdate.put(PENDING_UPDATE_WAS_INITIALIZED_KEY, wasInitialized); settings.edit().putString(PENDING_UPDATE_KEY, pendingUpdate.toString()).commit(); } catch (JSONException e) { // Should not happen. throw new CodePushUnknownException("Unable to save pending update.", e); } - } - private void startRollbackTimer(int rollbackTimeout) { - timer = new Timer(); - Calendar c = Calendar.getInstance(); - c.setTime(new Date()); - c.add(Calendar.MILLISECOND, rollbackTimeout); - Date timeout = c.getTime(); - timer.schedule(new TimerTask() { - @Override - public void run() { - rollbackPackage(); - } - }, timeout); + private JSONObject getPendingUpdate() { + SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); + String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null); + if (pendingUpdateString == null) { + return null; + } + + try { + JSONObject pendingUpdate = new JSONObject(pendingUpdateString); + return pendingUpdate; + } catch (JSONException e) { + // Should not happen. + throw new CodePushUnknownException("Unable to parse pending update metadata " + + pendingUpdateString + " stored in SharedPreferences", e); + } } private void initializeUpdateAfterRestart() { - SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0); - String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null); - - if (pendingUpdateString != null) { + JSONObject pendingUpdate = getPendingUpdate(); + if (pendingUpdate != null) { + didUpdate = true; try { - didUpdate = true; - JSONObject pendingUpdateJSON = new JSONObject(pendingUpdateString); - int rollbackTimeout = pendingUpdateJSON.getInt(PENDING_UPDATE_ROLLBACK_TIMEOUT_KEY); - if (0 != rollbackTimeout) { - startRollbackTimer(rollbackTimeout); + boolean wasInitialized = pendingUpdate.getBoolean(PENDING_UPDATE_WAS_INITIALIZED_KEY); + if (wasInitialized) { + // Pending update was initialized, but notifiyApplicationReady was not called. + // Therefore, deduce that it is a broken update and rollback. + rollbackPackage(); + } else { + // Mark that we tried to initiazlie the new update, so that if it crashes, + // we will know that we need to rollback when the app next starts. + savePendingUpdate(pendingUpdate.getString(PENDING_UPDATE_HASH_KEY), + /* wasInitialized */true); } - - settings.edit().remove(PENDING_UPDATE_KEY).commit(); } catch (JSONException e) { // Should not happen. - throw new CodePushUnknownException("Unable to parse pending update metadata " + - pendingUpdateString + " stored in SharedPreferences", e); + throw new CodePushUnknownException("Unable to read pending update metadata stored in SharedPreferences", e); } } } @@ -257,15 +251,24 @@ public class CodePush { private class CodePushNativeModule extends ReactContextBaseJavaModule { private LifecycleEventListener lifecycleEventListener = null; + private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; + + private void clearReactDevBundleCache() { + File cachedDevBundle = new File(getReactApplicationContext().getFilesDir(), JS_BUNDLE_FILE_NAME); + if (cachedDevBundle.exists()) { + cachedDevBundle.delete(); + } + } private void loadBundle() { + clearReactDevBundleCache(); Intent intent = mainActivity.getIntent(); mainActivity.finish(); mainActivity.startActivity(intent); } @ReactMethod - public void installUpdate(final ReadableMap updatePackage, final int rollbackTimeout, final int installMode, final Promise promise) { + public void installUpdate(final ReadableMap updatePackage, final int installMode, final Promise promise) { AsyncTask asyncTask = new AsyncTask() { @Override protected Void doInBackground(Object[] params) { @@ -276,7 +279,7 @@ public class CodePush { if (pendingHash == null) { throw new CodePushUnknownException("Update package to be installed has no hash."); } else { - savePendingUpdate(pendingHash, rollbackTimeout); + savePendingUpdate(pendingHash, /* wasInitialized */false); } if (installMode == CodePushInstallMode.IMMEDIATE.getValue()) { @@ -394,7 +397,7 @@ public class CodePush { @ReactMethod public void notifyApplicationReady(Promise promise) { - cancelRollbackTimer(); + removePendingUpdate(); promise.resolve(""); } diff --git a/package-mixins.js b/package-mixins.js index a3acf64..3a61d02 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -35,8 +35,8 @@ module.exports = (NativeCodePush) => { }; var local = { - install: function install(rollbackTimeout = 0, installMode = NativeCodePush.codePushInstallModeOnNextRestart, updateInstalledCallback) { - return NativeCodePush.installUpdate(this, rollbackTimeout, installMode) + install: function install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, updateInstalledCallback) { + return NativeCodePush.installUpdate(this, installMode) .then(function() { updateInstalledCallback && updateInstalledCallback(); if (installMode == NativeCodePush.codePushInstallModeImmediate) {