From 525e1601dd36edff971160f3917f151dcf3f9ce8 Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Thu, 25 Feb 2016 15:49:57 -0800 Subject: [PATCH] feedback --- CodePush.js | 34 ++--- .../microsoft/codepush/react/CodePush.java | 22 +-- .../codepush/react/CodePushUpdateUtils.java | 26 ++++ ios/CodePush/CodePush.h | 3 +- ios/CodePush/CodePush.m | 61 ++++---- ios/CodePush/CodePushPackage.m | 4 +- ios/CodePush/CodePushUpdateUtils.m | 132 ++++++++++++------ 7 files changed, 165 insertions(+), 117 deletions(-) diff --git a/CodePush.js b/CodePush.js index a66f475..9f16383 100644 --- a/CodePush.js +++ b/CodePush.js @@ -40,9 +40,16 @@ async function checkForUpdate(deploymentKey = null) { * to send the app version to the server, since we are interested * in any updates for current app store version, regardless of hash. */ - const queryPackage = localPackage && localPackage.appVersion && semver.compare(localPackage.appVersion, config.appVersion) === 0 - ? localPackage - : { appVersion: config.appVersion }; + const queryPackage; + if (localPackage && localPackage.appVersion && semver.compare(localPackage.appVersion, config.appVersion) === 0) { + queryPackage = localPackage; + } else { + queryPackage = { appVersion: config.appVersion }; + if (Platform.OS === "ios" && config.packageHash) { + queryPackage.packageHash = config.packageHash; + } + } + const update = await sdk.queryUpdateWithCurrentPackage(queryPackage); /* @@ -57,26 +64,19 @@ async function checkForUpdate(deploymentKey = null) { * the currently running update. This should _never_ happen, unless there is a * bug in the server, but we're adding this check just to double-check that the * client app is resilient to a potential issue with the update check. - * 4) On Android: the server said there is an update, but the update's hash is the - * same as that of the binary's currently running version. We did not attach the - * binary's hash to the updateCheck request because we want to avoid having to - * install diff updates against the binary's version, which we can't do yet on - * Android. + * 4) The server said there is an update, but the update's hash is the same as that + * of the binary's currently running version. This should only happen in Android - + * unlike iOS, we don't attach the binary's hash to the updateCheck request + * because we want to avoid having to install diff updates against the binary's + * version, which we can't do yet on Android. */ - if (!update || update.updateAppVersion || localPackage && (update.packageHash === localPackage.packageHash)) { + if (!update || update.updateAppVersion || localPackage && (update.packageHash === localPackage.packageHash) || config.packageHash === localPackage.packageHash) { if (update && update.updateAppVersion) { log("An update is available but it is targeting a newer binary version than you are currently running."); } return null; - } else { - if (Platform.OS === "android" && !localPackage) { - const binaryHash = await NativeCodePush.getBinaryHash(); - if (update.packageHash === binaryHash) { - return null; - } - } - + } else { const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) }; remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash); remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey; 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 36d15f8..396728a 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 @@ -50,7 +50,6 @@ public class CodePush { private final String ASSETS_BUNDLE_PREFIX = "assets://"; private final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime"; - private final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; private final String CODE_PUSH_PREFERENCES = "CodePush"; private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; private final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES"; @@ -70,9 +69,9 @@ public class CodePush { private CodePushTelemetryManager codePushTelemetryManager; // Config properties. - private String deploymentKey; private String appVersion; private int buildVersion; + private String deploymentKey; private final String serverUrl = "https://codepush.azurewebsites.net/"; private Activity mainActivity; @@ -393,20 +392,6 @@ public class CodePush { asyncTask.execute(); } - @ReactMethod - public void getBinaryHash(Promise promise) { - try { - promise.resolve(CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CODE_PUSH_HASH_FILE_NAME))); - } catch (IOException e) { - if (!isDebugMode) { - // Only print this message in "Release" mode. In "Debug", we may not have the - // hash if the build skips bundling the files. - CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition."); - } - promise.resolve(""); - } - } - @ReactMethod public void getConfiguration(Promise promise) { WritableNativeMap configMap = new WritableNativeMap(); @@ -417,6 +402,11 @@ public class CodePush { configMap.putString("clientUniqueId", Settings.Secure.getString(mainActivity.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID)); + String binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode); + if (binaryHash != null) { + configMap.putString(PACKAGE_HASH_KEY, binaryHash); + } + promise.resolve(configMap); } diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java index d8c38ff..5914f4c 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java @@ -1,5 +1,6 @@ package com.microsoft.codepush.react; +import android.app.Activity; import android.util.Base64; import com.facebook.react.bridge.ReadableArray; @@ -21,6 +22,12 @@ import java.util.Collections; public class CodePushUpdateUtils { + private static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; + + // These variables are used to cache the hash of the binary contents in memory. + private static String binaryHash = null; + private static boolean didLoadBinaryHash = false; + private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList manifest) { File folder = new File(folderPath); File[] folderFiles = folder.listFiles(); @@ -102,6 +109,25 @@ public class CodePushUpdateUtils { return null; } + public static String getHashForBinaryContents(Activity mainActivity, boolean isDebugMode) { + if (!didLoadBinaryHash) { + didLoadBinaryHash = true; + try { + binaryHash = CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CODE_PUSH_HASH_FILE_NAME)); + } catch (IOException e) { + if (!isDebugMode) { + // Only print this message in "Release" mode. In "Debug", we may not have the + // hash if the build skips bundling the files. + CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition."); + } + + return null; + } + } + + return binaryHash; + } + public static void verifyHashForDiffUpdate(String folderPath, String expectedHash) { ArrayList updateContentsManifest = new ArrayList(); addContentsOfFolderToManifest(folderPath, "", updateContentsManifest); diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index cf77e86..22d5ae5 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -115,13 +115,14 @@ failCallback:(void (^)(NSError *err))failCallback; + (NSString *)findMainBundleInFolder:(NSString *)folderPath error:(NSError **)error; -+ (NSString *)getDefaultAssetsFolderName; ++ (NSString *)getAssetsFolderName; + (NSString *)getDefaultJsBundleName; + (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl error:(NSError **)error; + (NSString *)getManifestFolderPrefix; ++ (NSString *)modifiedDateStringOfFileAtURL:(NSURL *)fileURL; + (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder expectedHash:(NSString *)expectedHash diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.m index 5028f78..b96c11c 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.m @@ -93,7 +93,7 @@ static NSString *bundleResourceName = @"main"; NSString *packageDate = [currentPackageMetadata objectForKey:BinaryBundleDateKey]; NSString *packageAppVersion = [currentPackageMetadata objectForKey:AppVersionKey]; - if ([[self modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) { + if ([[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) { // Return package file because it is newer than the app store binary's JS bundle NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile]; NSLog(logMessageFormat, packageUrl); @@ -288,20 +288,6 @@ static NSString *bundleResourceName = @"main"; }); } -/* - * This returns the modified date as a string for a given file URL. - */ -+ (NSString *)modifiedDateStringOfFileAtURL:(NSURL *)fileURL -{ - if (fileURL != nil) { - NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:nil]; - NSDate *modifiedDate = [fileAttributes objectForKey:NSFileModificationDate]; - return [NSString stringWithFormat:@"%f", [modifiedDate timeIntervalSince1970]]; - } else { - return nil; - } -} - /* * This method is used when an update has failed installation * and the app needs to be rolled back to the previous bundle. @@ -398,7 +384,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage NSDictionary *mutableUpdatePackage = [updatePackage mutableCopy]; NSURL *binaryBundleURL = [CodePush binaryBundleURL]; if (binaryBundleURL != nil) { - [mutableUpdatePackage setValue:[CodePush modifiedDateStringOfFileAtURL:binaryBundleURL] + [mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] forKey:BinaryBundleDateKey]; } @@ -449,7 +435,29 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve([[CodePushConfig current] configuration]); + NSDictionary *configuration = [[CodePushConfig current] configuration]; + NSError *error; + if (isRunningBinaryVersion) { + // isRunningBinaryVersion will not get set to "YES" if running against the packager. + NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error]; + if (error) { + NSLog(@"Error obtaining hash for binary contents: %@", error); + resolve(configuration); + return; + } + + if (binaryHash == nil) { + resolve(configuration); + return; + } + + NSMutableDictionary *mutableConfiguration = [configuration mutableCopy]; + [mutableConfiguration setObject:binaryHash forKey:PackageHashKey]; + resolve(mutableConfiguration); + return; + } + + resolve(configuration); } /* @@ -459,25 +467,6 @@ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSError *error; - if (isRunningBinaryVersion) { - // isRunningBinaryVersion will not get set to "YES" if running against the packager. - NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error]; - if (error) { - NSLog(@"Error obtaining hash for binary contents: %@", error); - resolve(nil); - return; - } else if (binaryHash == nil) { - resolve(nil); - return; - } - - resolve(@{ - PackageHashKey:binaryHash, - AppVersionKey:[[CodePushConfig current] appVersion] - }); - return; - } - NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy]; if (error) { diff --git a/ios/CodePush/CodePushPackage.m b/ios/CodePush/CodePushPackage.m index 269adba..a6dfcd0 100644 --- a/ios/CodePush/CodePushPackage.m +++ b/ios/CodePush/CodePushPackage.m @@ -15,7 +15,7 @@ NSString * const UnzippedFolderName = @"unzipped"; + (NSString *)getBinaryAssetsPath { - return [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:[CodePushUpdateUtils getDefaultAssetsFolderName]]; + return [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:[CodePushUpdateUtils getAssetsFolderName]]; } + (NSString *)getCodePushPath @@ -286,7 +286,7 @@ NSString * const UnzippedFolderName = @"unzipped"; } [[NSFileManager defaultManager] copyItemAtPath:[self getBinaryAssetsPath] - toPath:[newUpdateCodePushPath stringByAppendingPathComponent:[CodePushUpdateUtils getDefaultAssetsFolderName]] + toPath:[newUpdateCodePushPath stringByAppendingPathComponent:[CodePushUpdateUtils getAssetsFolderName]] error:&error]; if (error) { failCallback(error); diff --git a/ios/CodePush/CodePushUpdateUtils.m b/ios/CodePush/CodePushUpdateUtils.m index 5b1c460..3b5c409 100644 --- a/ios/CodePush/CodePushUpdateUtils.m +++ b/ios/CodePush/CodePushUpdateUtils.m @@ -3,9 +3,14 @@ @implementation CodePushUpdateUtils -NSString * const ManifestFolderPrefix = @"CodePush"; +NSString * const AssetsFolderName = @"assets"; +NSString * const BinaryHashKey = @"CodePushBinaryHash"; NSString * const DefaultJsBundleName = @"main.jsbundle"; -NSString * const DefaultAssetsFolderName = @"assets"; +NSString * const ManifestFolderPrefix = @"CodePush"; + +// These variables are used to cache the hash of the binary contents in memory. +static NSString *binaryHash = nil; +static BOOL didLoadBinaryHash = false; + (void)addContentsOfFolderToManifest:(NSString *)folderPath pathPrefix:(NSString *)pathPrefix @@ -34,13 +39,32 @@ NSString * const DefaultAssetsFolderName = @"assets"; } } else { NSData *fileContents = [NSData dataWithContentsOfFile:fullFilePath]; - NSString *fileContentsHash = [self computeHash:fileContents]; + NSString *fileContentsHash = [self computeHashForData:fileContents]; [manifest addObject:[[relativePath stringByAppendingString:@":"] stringByAppendingString:fileContentsHash]]; } } } -+ (NSString *)computeHash:(NSData *)inputData ++ (NSString *)computeFinalHashFromManifest:(NSMutableArray *)manifest + error:(NSError **)error +{ + NSArray *sortedManifest = [manifest sortedArrayUsingSelector:@selector(compare:)]; + NSData *manifestData = [NSJSONSerialization dataWithJSONObject:sortedManifest + options:kNilOptions + error:error]; + if (*error) { + return nil; + } + + NSString *manifestString = [[NSString alloc] initWithData:manifestData + encoding:NSUTF8StringEncoding]; + // The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png" + manifestString = [manifestString stringByReplacingOccurrencesOfString:@"\\/" + withString:@"/"]; + return [self computeHashForData:[NSData dataWithBytes:manifestString.UTF8String length:manifestString.length]]; +} + ++ (NSString *)computeHashForData:(NSData *)inputData { uint8_t digest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(inputData.bytes, inputData.length, digest); @@ -131,9 +155,9 @@ NSString * const DefaultAssetsFolderName = @"assets"; return nil; } -+ (NSString *)getDefaultAssetsFolderName ++ (NSString *)getAssetsFolderName { - return DefaultAssetsFolderName; + return AssetsFolderName; } + (NSString *)getDefaultJsBundleName @@ -144,35 +168,50 @@ NSString * const DefaultAssetsFolderName = @"assets"; + (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl error:(NSError **)error { - NSString *assetsPath = [CodePushPackage getBinaryAssetsPath]; - NSMutableArray *manifest = [NSMutableArray array]; - [self addContentsOfFolderToManifest:assetsPath - pathPrefix:[NSString stringWithFormat:@"%@/%@", [self getManifestFolderPrefix], @"assets"] - manifest:manifest - error:error]; - if (*error) { - return nil; + if (!didLoadBinaryHash) { + didLoadBinaryHash = true; + + // Get the cached hash from user preferences if it exists. + NSString *binaryModifiedDate = [self modifiedDateStringOfFileAtURL:binaryBundleUrl]; + NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *binaryHashDictionary = [preferences objectForKey:BinaryHashKey]; + if (binaryHashDictionary != nil) { + binaryHash = [binaryHashDictionary objectForKey:binaryModifiedDate]; + if (binaryHash == nil) { + [preferences removeObjectForKey:BinaryHashKey]; + [preferences synchronize]; + } else { + return binaryHash; + } + } + + binaryHashDictionary = [NSMutableDictionary dictionary]; + + NSString *assetsPath = [CodePushPackage getBinaryAssetsPath]; + NSMutableArray *manifest = [NSMutableArray array]; + [self addContentsOfFolderToManifest:assetsPath + pathPrefix:[NSString stringWithFormat:@"%@/%@", [self getManifestFolderPrefix], @"assets"] + manifest:manifest + error:error]; + if (*error) { + return nil; + } + + NSData *jsBundleContents = [NSData dataWithContentsOfURL:binaryBundleUrl]; + NSString *jsBundleContentsHash = [self computeHashForData:jsBundleContents]; + [manifest addObject:[[NSString stringWithFormat:@"%@/%@", [self getManifestFolderPrefix], [self getDefaultJsBundleName]] stringByAppendingString:jsBundleContentsHash]]; + binaryHash = [self computeFinalHashFromManifest:manifest error:error]; + + // Cache the hash in user preferences. This assumes that the modified date for the + // JS bundle changes every time a new bundle is generated by the packager. + [binaryHashDictionary setObject:binaryHash forKey:binaryModifiedDate]; + [preferences setObject:binaryHashDictionary forKey:BinaryHashKey]; + [preferences synchronize]; + return binaryHash; } - NSData *jsBundleContents = [NSData dataWithContentsOfURL:binaryBundleUrl]; - NSString *jsBundleContentsHash = [self computeHash:jsBundleContents]; - [manifest addObject:[[NSString stringWithFormat:@"%@/%@", [self getManifestFolderPrefix], [self getDefaultJsBundleName]] stringByAppendingString:jsBundleContentsHash]]; - - NSArray *sortedManifest = [manifest sortedArrayUsingSelector:@selector(compare:)]; - NSData *manifestData = [NSJSONSerialization dataWithJSONObject:sortedManifest - options:kNilOptions - error:error]; - if (*error) { - return nil; - } - - NSString *manifestString = [[NSString alloc] initWithData:manifestData - encoding:NSUTF8StringEncoding]; - // The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png" - manifestString = [manifestString stringByReplacingOccurrencesOfString:@"\\/" - withString:@"/"]; - NSString *manifestHash = [self computeHash:[NSData dataWithBytes:manifestString.UTF8String length:manifestString.length]]; - return manifestHash; + // Use the cached hash in memory. + return binaryHash; } + (NSString *)getManifestFolderPrefix @@ -180,6 +219,17 @@ NSString * const DefaultAssetsFolderName = @"assets"; return ManifestFolderPrefix; } ++ (NSString *)modifiedDateStringOfFileAtURL:(NSURL *)fileURL +{ + if (fileURL != nil) { + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:nil]; + NSDate *modifiedDate = [fileAttributes objectForKey:NSFileModificationDate]; + return [NSString stringWithFormat:@"%f", [modifiedDate timeIntervalSince1970]]; + } else { + return nil; + } +} + + (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder expectedHash:(NSString *)expectedHash error:(NSError **)error @@ -193,20 +243,12 @@ NSString * const DefaultAssetsFolderName = @"assets"; return NO; } - NSArray *sortedUpdateContentsManifest = [updateContentsManifest sortedArrayUsingSelector:@selector(compare:)]; - NSData *updateContentsManifestData = [NSJSONSerialization dataWithJSONObject:sortedUpdateContentsManifest - options:kNilOptions - error:error]; - if (*error) { + NSString *updateContentsManifestHash = [self computeFinalHashFromManifest:updateContentsManifest + error:error]; + if (*error || updateContentsManifestHash == nil) { return NO; } - - NSString *updateContentsManifestString = [[NSString alloc] initWithData:updateContentsManifestData - encoding:NSUTF8StringEncoding]; - // The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png" - updateContentsManifestString = [updateContentsManifestString stringByReplacingOccurrencesOfString:@"\\/" - withString:@"/"]; - NSString *updateContentsManifestHash = [self computeHash:[NSData dataWithBytes:updateContentsManifestString.UTF8String length:updateContentsManifestString.length]]; + return [updateContentsManifestHash isEqualToString:expectedHash]; }