This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,16 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@protocol EXUpdatesAppLauncher
@property (nullable, nonatomic, strong, readonly) EXUpdatesUpdate *launchedUpdate;
@property (nullable, nonatomic, strong, readonly) NSURL *launchAssetUrl;
@property (nullable, nonatomic, strong, readonly) NSDictionary *assetFilesMap;
@property (nonatomic, assign, readonly) BOOL isUsingEmbeddedAssets;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,17 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLauncher.h>
#import <EXUpdates/EXUpdatesConfig.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesAppLauncherNoDatabase : NSObject <EXUpdatesAppLauncher>
- (void)launchUpdateWithConfig:(EXUpdatesConfig *)config;
- (void)launchUpdateWithConfig:(EXUpdatesConfig *)config fatalError:(NSError *)error;
+ (nullable NSString *)consumeError;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,109 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAsset.h>
#import <EXUpdates/EXUpdatesAppLauncherNoDatabase.h>
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesErrorLogFile = @"expo-error.log";
@interface EXUpdatesAppLauncherNoDatabase ()
@property (nullable, nonatomic, strong, readwrite) EXUpdatesUpdate *launchedUpdate;
@property (nullable, nonatomic, strong, readwrite) NSURL *launchAssetUrl;
@property (nullable, nonatomic, strong, readwrite) NSMutableDictionary *assetFilesMap;
@end
@implementation EXUpdatesAppLauncherNoDatabase
- (void)launchUpdateWithConfig:(EXUpdatesConfig *)config
{
_launchedUpdate = [EXUpdatesEmbeddedAppLoader embeddedManifestWithConfig:config database:nil];
if (_launchedUpdate) {
if (_launchedUpdate.status == EXUpdatesUpdateStatusEmbedded) {
NSAssert(_assetFilesMap == nil, @"assetFilesMap should be null for embedded updates");
_launchAssetUrl = [[NSBundle mainBundle] URLForResource:EXUpdatesBareEmbeddedBundleFilename withExtension:EXUpdatesBareEmbeddedBundleFileType];
} else {
_launchAssetUrl = [[NSBundle mainBundle] URLForResource:EXUpdatesEmbeddedBundleFilename withExtension:EXUpdatesEmbeddedBundleFileType];
NSMutableDictionary *assetFilesMap = [NSMutableDictionary new];
for (EXUpdatesAsset *asset in _launchedUpdate.assets) {
NSURL *localUrl = [[NSBundle mainBundle] URLForResource:asset.mainBundleFilename withExtension:asset.type];
if (localUrl && asset.key) {
assetFilesMap[asset.key] = localUrl.absoluteString;
}
}
_assetFilesMap = assetFilesMap;
}
}
}
- (BOOL)isUsingEmbeddedAssets
{
return _assetFilesMap == nil;
}
- (void)launchUpdateWithConfig:(EXUpdatesConfig *)config fatalError:(NSError *)error;
{
[self launchUpdateWithConfig:config];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self _writeErrorToLog:error];
});
}
+ (nullable NSString *)consumeError;
{
NSString *errorLogFilePath = [[self class] _errorLogFilePath];
NSData *data = [NSData dataWithContentsOfFile:errorLogFilePath options:kNilOptions error:nil];
if (data) {
NSError *err;
if (![NSFileManager.defaultManager removeItemAtPath:errorLogFilePath error:&err]) {
NSLog(@"Could not delete error log: %@", err.localizedDescription);
}
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
} else {
return nil;
}
}
- (void)_writeErrorToLog:(NSError *)error
{
NSString *serializedError = [NSString stringWithFormat:@"Expo encountered a fatal error: %@", [self _serializeError:error]];
NSData *data = [serializedError dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
if (![data writeToFile:[[self class] _errorLogFilePath] options:NSDataWritingAtomic error:&err]) {
NSLog(@"Could not write fatal error to log: %@", error.localizedDescription);
}
}
- (NSString *)_serializeError:(NSError *)error
{
NSString *localizedFailureReason = error.localizedFailureReason;
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
NSMutableString *serialization = [[NSString stringWithFormat:@"Time: %f\nDomain: %@\nCode: %li\nDescription: %@",
[[NSDate date] timeIntervalSince1970] * 1000,
error.domain,
(long)error.code,
error.localizedDescription] mutableCopy];
if (localizedFailureReason) {
[serialization appendFormat:@"\nFailure Reason: %@", localizedFailureReason];
}
if (underlyingError) {
[serialization appendFormat:@"\n\nUnderlying Error:\n%@", [self _serializeError:underlyingError]];
}
return serialization;
}
+ (NSString *)_errorLogFilePath
{
NSURL *applicationDocumentsDirectory = [[NSFileManager.defaultManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
return [[applicationDocumentsDirectory URLByAppendingPathComponent:EXUpdatesErrorLogFile] path];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,29 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLauncher.h>
#import <EXUpdates/EXUpdatesSelectionPolicy.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^EXUpdatesAppLauncherCompletionBlock)(NSError * _Nullable error, BOOL success);
typedef void (^EXUpdatesAppLauncherUpdateCompletionBlock)(NSError * _Nullable error, EXUpdatesUpdate * _Nullable launchableUpdate);
@interface EXUpdatesAppLauncherWithDatabase : NSObject <EXUpdatesAppLauncher>
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
completionQueue:(dispatch_queue_t)completionQueue;
- (void)launchUpdateWithSelectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
completion:(EXUpdatesAppLauncherCompletionBlock)completion;
+ (void)launchableUpdateWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
selectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
completion:(EXUpdatesAppLauncherUpdateCompletionBlock)completion
completionQueue:(dispatch_queue_t)completionQueue;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,288 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLauncherWithDatabase.h>
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
#import <EXUpdates/EXUpdatesUtils.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesAppLauncherWithDatabase ()
@property (nullable, nonatomic, strong, readwrite) EXUpdatesUpdate *launchedUpdate;
@property (nullable, nonatomic, strong, readwrite) NSURL *launchAssetUrl;
@property (nullable, nonatomic, strong, readwrite) NSMutableDictionary *assetFilesMap;
@property (nonatomic, strong) EXUpdatesConfig *config;
@property (nonatomic, strong) EXUpdatesDatabase *database;
@property (nonatomic, strong) NSURL *directory;
@property (nonatomic, strong) EXUpdatesFileDownloader *downloader;
@property (nonatomic, copy) EXUpdatesAppLauncherCompletionBlock completion;
@property (nonatomic, strong) dispatch_queue_t completionQueue;
@property (nonatomic, strong) dispatch_queue_t launcherQueue;
@property (nonatomic, assign) NSUInteger completedAssets;
@property (nonatomic, strong) NSError *launchAssetError;
@end
static NSString * const EXUpdatesAppLauncherErrorDomain = @"AppLauncher";
@implementation EXUpdatesAppLauncherWithDatabase
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
completionQueue:(dispatch_queue_t)completionQueue
{
if (self = [super init]) {
_launcherQueue = dispatch_queue_create("expo.launcher.LauncherQueue", DISPATCH_QUEUE_SERIAL);
_completedAssets = 0;
_config = config;
_database = database;
_directory = directory;
_completionQueue = completionQueue;
}
return self;
}
+ (void)launchableUpdateWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
selectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
completion:(EXUpdatesAppLauncherUpdateCompletionBlock)completion
completionQueue:(dispatch_queue_t)completionQueue
{
dispatch_async(database.databaseQueue, ^{
NSError *error;
NSArray<EXUpdatesUpdate *> *launchableUpdates = [database launchableUpdatesWithConfig:config error:&error];
dispatch_async(completionQueue, ^{
if (!launchableUpdates) {
completion(error, nil);
}
// We can only run an update marked as embedded if it's actually the update embedded in the
// current binary. We might have an older update from a previous binary still listed in the
// database with Embedded status so we need to filter that out here.
EXUpdatesUpdate *embeddedManifest = [EXUpdatesEmbeddedAppLoader embeddedManifestWithConfig:config database:database];
NSMutableArray<EXUpdatesUpdate *>*filteredLaunchableUpdates = [NSMutableArray new];
for (EXUpdatesUpdate *update in launchableUpdates) {
if (update.status == EXUpdatesUpdateStatusEmbedded) {
if (embeddedManifest && ![update.updateId isEqual:embeddedManifest.updateId]) {
continue;
}
}
[filteredLaunchableUpdates addObject:update];
}
completion(nil, [selectionPolicy launchableUpdateWithUpdates:filteredLaunchableUpdates]);
});
});
}
- (void)launchUpdateWithSelectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
completion:(EXUpdatesAppLauncherCompletionBlock)completion
{
NSAssert(!_completion, @"EXUpdatesAppLauncher:launchUpdateWithSelectionPolicy:successBlock should not be called twice on the same instance");
_completion = completion;
if (!_launchedUpdate) {
[[self class] launchableUpdateWithConfig:_config database:_database selectionPolicy:selectionPolicy completion:^(NSError * _Nullable error, EXUpdatesUpdate * _Nullable launchableUpdate) {
if (error || !launchableUpdate) {
if (self->_completion) {
dispatch_async(self->_completionQueue, ^{
NSMutableDictionary *userInfo = [NSMutableDictionary new];
userInfo[NSLocalizedDescriptionKey] = @"No launchable updates found in database";
if (error) {
userInfo[NSUnderlyingErrorKey] = error;
}
self->_completion([NSError errorWithDomain:EXUpdatesAppLauncherErrorDomain code:1011 userInfo:userInfo], NO);
});
}
} else {
self->_launchedUpdate = launchableUpdate;
[self _ensureAllAssetsExist];
}
} completionQueue:_launcherQueue];
} else {
[self _ensureAllAssetsExist];
}
}
- (BOOL)isUsingEmbeddedAssets
{
return _assetFilesMap == nil;
}
- (void)_ensureAllAssetsExist
{
if (_launchedUpdate.status == EXUpdatesUpdateStatusEmbedded) {
NSAssert(_assetFilesMap == nil, @"assetFilesMap should be null for embedded updates");
_launchAssetUrl = [[NSBundle mainBundle] URLForResource:EXUpdatesBareEmbeddedBundleFilename withExtension:EXUpdatesBareEmbeddedBundleFileType];
dispatch_async(self->_completionQueue, ^{
self->_completion(self->_launchAssetError, self->_launchAssetUrl != nil);
self->_completion = nil;
});
return;
} else if (_launchedUpdate.status == EXUpdatesUpdateStatusDevelopment) {
dispatch_async(self->_completionQueue, ^{
self->_completion(nil, YES);
self->_completion = nil;
});
return;
}
_assetFilesMap = [NSMutableDictionary new];
if (_launchedUpdate) {
NSUInteger totalAssetCount = _launchedUpdate.assets.count;
for (EXUpdatesAsset *asset in _launchedUpdate.assets) {
NSURL *assetLocalUrl = [_directory URLByAppendingPathComponent:asset.filename];
[self _ensureAssetExists:asset withLocalUrl:assetLocalUrl completion:^(BOOL exists) {
dispatch_assert_queue(self->_launcherQueue);
self->_completedAssets++;
if (exists) {
if (asset.isLaunchAsset) {
self->_launchAssetUrl = assetLocalUrl;
} else {
if (asset.key) {
self->_assetFilesMap[asset.key] = assetLocalUrl.absoluteString;
}
}
}
if (self->_completedAssets == totalAssetCount) {
dispatch_async(self->_completionQueue, ^{
self->_completion(self->_launchAssetError, self->_launchAssetUrl != nil);
self->_completion = nil;
});
}
}];
}
}
}
- (void)_ensureAssetExists:(EXUpdatesAsset *)asset withLocalUrl:(NSURL *)assetLocalUrl completion:(void (^)(BOOL exists))completion
{
[self _checkExistenceOfAsset:asset withLocalUrl:assetLocalUrl completion:^(BOOL exists) {
if (exists) {
completion(YES);
return;
}
[self _maybeCopyAssetFromMainBundle:asset withLocalUrl:assetLocalUrl completion:^(BOOL success, NSError * _Nullable error) {
if (success) {
completion(YES);
return;
}
if (error) {
NSLog(@"Error copying embedded asset %@: %@", asset.key, error.localizedDescription);
}
[self _downloadAsset:asset withLocalUrl:assetLocalUrl completion:^(NSError * _Nullable error, EXUpdatesAsset *asset, NSURL *assetLocalUrl) {
if (error) {
if (asset.isLaunchAsset) {
// save the error -- since this is the launch asset, the launcher will fail
// so we want to propagate this error
self->_launchAssetError = error;
}
NSLog(@"Failed to load missing asset %@: %@", asset.key, error.localizedDescription);
completion(NO);
} else {
// attempt to update the database record to match the newly downloaded asset
// but don't block launching on this
dispatch_async(self->_database.databaseQueue, ^{
NSError *error;
[self->_database updateAsset:asset error:&error];
if (error) {
NSLog(@"Could not write data for downloaded asset to database: %@", error.localizedDescription);
}
});
completion(YES);
}
}];
}];
}];
}
- (void)_checkExistenceOfAsset:(EXUpdatesAsset *)asset withLocalUrl:(NSURL *)assetLocalUrl completion:(void (^)(BOOL exists))completion
{
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:[assetLocalUrl path]];
dispatch_async(self->_launcherQueue, ^{
completion(exists);
});
});
}
- (void)_maybeCopyAssetFromMainBundle:(EXUpdatesAsset *)asset
withLocalUrl:(NSURL *)assetLocalUrl
completion:(void (^)(BOOL success, NSError * _Nullable error))completion
{
EXUpdatesUpdate *embeddedManifest = [EXUpdatesEmbeddedAppLoader embeddedManifestWithConfig:_config database:_database];
if (embeddedManifest) {
EXUpdatesAsset *matchingAsset;
for (EXUpdatesAsset *embeddedAsset in embeddedManifest.assets) {
if ([embeddedAsset.key isEqualToString:asset.key]) {
matchingAsset = embeddedAsset;
break;
}
}
if (matchingAsset && matchingAsset.mainBundleFilename) {
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:matchingAsset.mainBundleFilename ofType:matchingAsset.type];
NSError *error;
BOOL success = [NSFileManager.defaultManager copyItemAtPath:bundlePath toPath:[assetLocalUrl path] error:&error];
dispatch_async(self->_launcherQueue, ^{
completion(success, error);
});
});
return;
}
}
completion(NO, nil);
}
- (void)_downloadAsset:(EXUpdatesAsset *)asset
withLocalUrl:(NSURL *)assetLocalUrl
completion:(void (^)(NSError * _Nullable error, EXUpdatesAsset *asset, NSURL *assetLocalUrl))completion
{
if (!asset.url) {
completion([NSError errorWithDomain:EXUpdatesAppLauncherErrorDomain code:1007 userInfo:@{NSLocalizedDescriptionKey: @"Failed to download asset with no URL provided"}], asset, assetLocalUrl);
}
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
[self.downloader downloadFileFromURL:asset.url toPath:[assetLocalUrl path] successBlock:^(NSData *data, NSURLResponse *response) {
dispatch_async(self->_launcherQueue, ^{
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
asset.headers = ((NSHTTPURLResponse *)response).allHeaderFields;
}
asset.contentHash = [EXUpdatesUtils sha256WithData:data];
asset.downloadTime = [NSDate date];
completion(nil, asset, assetLocalUrl);
});
} errorBlock:^(NSError *error, NSURLResponse *response) {
dispatch_async(self->_launcherQueue, ^{
completion(error, asset, assetLocalUrl);
});
}];
});
}
- (EXUpdatesFileDownloader *)downloader
{
if (!_downloader) {
_downloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:_config];
}
return _downloader;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,15 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@protocol EXUpdatesSelectionPolicy
- (nullable EXUpdatesUpdate *)launchableUpdateWithUpdates:(NSArray<EXUpdatesUpdate *> *)updates;
- (NSArray<EXUpdatesUpdate *> *)updatesToDeleteWithLaunchedUpdate:(EXUpdatesUpdate *)launchedUpdate updates:(NSArray<EXUpdatesUpdate *> *)updates;
- (BOOL)shouldLoadNewUpdate:(nullable EXUpdatesUpdate *)newUpdate withLaunchedUpdate:(nullable EXUpdatesUpdate *)launchedUpdate;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,14 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesSelectionPolicy.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesSelectionPolicyNewest : NSObject <EXUpdatesSelectionPolicy>
- (instancetype)initWithRuntimeVersion:(NSString *)runtimeVersion;
- (instancetype)initWithRuntimeVersions:(NSArray<NSString *> *)runtimeVersions;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,83 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesSelectionPolicyNewest.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesSelectionPolicyNewest ()
@property (nonatomic, strong) NSArray<NSString *> *runtimeVersions;
@end
@implementation EXUpdatesSelectionPolicyNewest
- (instancetype)initWithRuntimeVersions:(NSArray<NSString *> *)runtimeVersions
{
if (self = [super init]) {
_runtimeVersions = runtimeVersions;
}
return self;
}
- (instancetype)initWithRuntimeVersion:(NSString *)runtimeVersion
{
return [self initWithRuntimeVersions:@[runtimeVersion]];
}
- (nullable EXUpdatesUpdate *)launchableUpdateWithUpdates:(NSArray<EXUpdatesUpdate *> *)updates
{
EXUpdatesUpdate *runnableUpdate;
NSDate *runnableUpdateCommitTime;
for (EXUpdatesUpdate *update in updates) {
if (![_runtimeVersions containsObject:update.runtimeVersion]) {
continue;
}
NSDate *commitTime = update.commitTime;
if (!runnableUpdateCommitTime || [runnableUpdateCommitTime compare:commitTime] == NSOrderedAscending) {
runnableUpdate = update;
runnableUpdateCommitTime = commitTime;
}
}
return runnableUpdate;
}
- (NSArray<EXUpdatesUpdate *> *)updatesToDeleteWithLaunchedUpdate:(EXUpdatesUpdate *)launchedUpdate updates:(NSArray<EXUpdatesUpdate *> *)updates
{
if (!launchedUpdate) {
return @[];
}
NSMutableArray<EXUpdatesUpdate *> *updatesToDelete = [NSMutableArray new];
// keep the launched update and one other, the next newest, to be safe and make rollbacks faster
EXUpdatesUpdate *nextNewestUpdate;
for (EXUpdatesUpdate *update in updates) {
if ([launchedUpdate.commitTime compare:update.commitTime] == NSOrderedDescending) {
[updatesToDelete addObject:update];
if (!nextNewestUpdate || [update.commitTime compare:nextNewestUpdate.commitTime] == NSOrderedDescending) {
nextNewestUpdate = update;
}
}
}
if (nextNewestUpdate) {
[updatesToDelete removeObject:nextNewestUpdate];
}
return updatesToDelete;
}
- (BOOL)shouldLoadNewUpdate:(nullable EXUpdatesUpdate *)newUpdate withLaunchedUpdate:(nullable EXUpdatesUpdate *)launchedUpdate
{
if (!newUpdate) {
return false;
}
if (!launchedUpdate) {
return true;
}
return [launchedUpdate.commitTime compare:newUpdate.commitTime] == NSOrderedAscending;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,29 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLoader.h>
#import <EXUpdates/EXUpdatesAsset.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesAppLoader ()
@property (nonatomic, strong) EXUpdatesConfig *config;
@property (nonatomic, strong) EXUpdatesDatabase *database;
@property (nonatomic, strong) NSURL *directory;
@property (nonatomic, strong) EXUpdatesUpdate *updateManifest;
@property (nonatomic, copy) EXUpdatesAppLoaderManifestBlock manifestBlock;
@property (nonatomic, copy) EXUpdatesAppLoaderSuccessBlock successBlock;
@property (nonatomic, copy) EXUpdatesAppLoaderErrorBlock errorBlock;
- (void)startLoadingFromManifest:(EXUpdatesUpdate *)updateManifest;
- (void)handleAssetDownloadAlreadyExists:(EXUpdatesAsset *)asset;
- (void)handleAssetDownloadWithData:(NSData *)data response:(nullable NSURLResponse *)response asset:(EXUpdatesAsset *)asset;
- (void)handleAssetDownloadWithError:(NSError *)error asset:(EXUpdatesAsset *)asset;
- (void)downloadAsset:(EXUpdatesAsset *)asset;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,35 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
typedef BOOL (^EXUpdatesAppLoaderManifestBlock)(EXUpdatesUpdate *update);
typedef void (^EXUpdatesAppLoaderSuccessBlock)(EXUpdatesUpdate * _Nullable update);
typedef void (^EXUpdatesAppLoaderErrorBlock)(NSError *error);
@interface EXUpdatesAppLoader : NSObject
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
completionQueue:(dispatch_queue_t)completionQueue;
/**
* Load an update from the given URL, which should respond with a valid manifest.
*
* The `onManifest` block is called as soon as the manifest has been downloaded.
* The block should determine whether or not the update described by this manifest
* should be downloaded, based on (for example) whether or not it already has the
* update downloaded locally, and return the corresponding BOOL value.
*/
- (void)loadUpdateFromUrl:(NSURL *)url
onManifest:(EXUpdatesAppLoaderManifestBlock)manifestBlock
success:(EXUpdatesAppLoaderSuccessBlock)success
error:(EXUpdatesAppLoaderErrorBlock)error;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,325 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLoader+Private.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
#import <EXUpdates/EXUpdatesUtils.h>
#import <UMCore/UMUtilities.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesAppLoader ()
@property (nonatomic, strong) NSMutableArray<EXUpdatesAsset *> *assetsToLoad;
@property (nonatomic, strong) NSMutableArray<EXUpdatesAsset *> *erroredAssets;
@property (nonatomic, strong) NSMutableArray<EXUpdatesAsset *> *finishedAssets;
@property (nonatomic, strong) NSMutableArray<EXUpdatesAsset *> *existingAssets;
@property (nonatomic, strong) NSLock *arrayLock;
@property (nonatomic, strong) dispatch_queue_t completionQueue;
@end
static NSString * const EXUpdatesAppLoaderErrorDomain = @"EXUpdatesAppLoader";
@implementation EXUpdatesAppLoader
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
completionQueue:(dispatch_queue_t)completionQueue
{
if (self = [super init]) {
_assetsToLoad = [NSMutableArray new];
_erroredAssets = [NSMutableArray new];
_finishedAssets = [NSMutableArray new];
_existingAssets = [NSMutableArray new];
_arrayLock = [[NSLock alloc] init];
_config = config;
_database = database;
_directory = directory;
_completionQueue = completionQueue;
}
return self;
}
- (void)_reset
{
_assetsToLoad = [NSMutableArray new];
_erroredAssets = [NSMutableArray new];
_finishedAssets = [NSMutableArray new];
_existingAssets = [NSMutableArray new];
_updateManifest = nil;
_manifestBlock = nil;
_successBlock = nil;
_errorBlock = nil;
}
# pragma mark - subclass methods
- (void)loadUpdateFromUrl:(NSURL *)url
onManifest:(EXUpdatesAppLoaderManifestBlock)manifestBlock
success:(EXUpdatesAppLoaderSuccessBlock)success
error:(EXUpdatesAppLoaderErrorBlock)error
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Should not call EXUpdatesAppLoader#loadUpdate -- use a subclass instead" userInfo:nil];
}
- (void)downloadAsset:(EXUpdatesAsset *)asset
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Should not call EXUpdatesAppLoader#loadUpdate -- use a subclass instead" userInfo:nil];
}
# pragma mark - loading and database logic
- (void)startLoadingFromManifest:(EXUpdatesUpdate *)updateManifest
{
if (![self _shouldStartLoadingUpdate:updateManifest]) {
if (_successBlock) {
_successBlock(nil);
}
return;
}
if (updateManifest.isDevelopmentMode) {
dispatch_async(_database.databaseQueue, ^{
NSError *updateError;
[self->_database addUpdate:updateManifest error:&updateError];
if (updateError) {
[self _finishWithError:updateError];
return;
}
NSError *updateReadyError;
[self->_database markUpdateFinished:updateManifest error:&updateReadyError];
if (updateReadyError) {
[self _finishWithError:updateReadyError];
return;
}
EXUpdatesAppLoaderSuccessBlock successBlock;
if (self->_successBlock) {
successBlock = self->_successBlock;
}
dispatch_async(self->_completionQueue, ^{
if (successBlock) {
successBlock(updateManifest);
}
[self _reset];
});
});
return;
}
dispatch_async(_database.databaseQueue, ^{
NSError *existingUpdateError;
EXUpdatesUpdate *existingUpdate = [self->_database updateWithId:updateManifest.updateId config:self->_config error:&existingUpdateError];
// if something has gone wrong on the server and we have two updates with the same id
// but different scope keys, we should try to launch something rather than show a cryptic
// error to the user.
if (existingUpdate && ![existingUpdate.scopeKey isEqualToString:updateManifest.scopeKey]) {
NSError *setScopeKeyError;
[self->_database setScopeKey:updateManifest.scopeKey onUpdate:existingUpdate error:&setScopeKeyError];
if (setScopeKeyError) {
[self _finishWithError:setScopeKeyError];
return;
}
NSLog(@"EXUpdatesAppLoader: Loaded an update with the same ID but a different scopeKey than one we already have on disk. This is a server error. Overwriting the scopeKey and loading the existing update.");
}
if (existingUpdate && existingUpdate.status == EXUpdatesUpdateStatusReady) {
if (self->_successBlock) {
dispatch_async(self->_completionQueue, ^{
self->_successBlock(updateManifest);
});
}
return;
}
if (existingUpdate) {
// we've already partially downloaded the update.
// however, it's not ready, so we should try to download all the assets again.
self->_updateManifest = updateManifest;
} else {
if (existingUpdateError) {
NSLog(@"Failed to select old update from DB: %@", existingUpdateError.localizedDescription);
}
// no update already exists with this ID, so we need to insert it and download everything.
self->_updateManifest = updateManifest;
NSError *updateError;
[self->_database addUpdate:self->_updateManifest error:&updateError];
if (updateError) {
[self _finishWithError:updateError];
return;
}
}
if (self->_updateManifest.assets && self->_updateManifest.assets.count > 0) {
self->_assetsToLoad = [self->_updateManifest.assets mutableCopy];
for (EXUpdatesAsset *asset in self->_updateManifest.assets) {
// before downloading, check to see if we already have this asset in the database
NSError *matchingAssetError;
EXUpdatesAsset *matchingDbEntry = [self->_database assetWithKey:asset.key error:&matchingAssetError];
if (matchingAssetError || !matchingDbEntry || !matchingDbEntry.filename) {
[self downloadAsset:asset];
} else {
NSError *mergeError;
[self->_database mergeAsset:asset withExistingEntry:matchingDbEntry error:&mergeError];
if (mergeError) {
NSLog(@"Failed to merge asset with existing database entry: %@", mergeError.localizedDescription);
}
// make sure the file actually exists on disk
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
NSURL *urlOnDisk = [self->_directory URLByAppendingPathComponent:asset.filename];
if ([[NSFileManager defaultManager] fileExistsAtPath:[urlOnDisk path]]) {
// file already exists, we don't need to download it again
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadAlreadyExists:asset];
});
} else {
[self downloadAsset:asset];
}
});
}
}
} else {
[self _finish];
}
});
}
- (void)handleAssetDownloadAlreadyExists:(EXUpdatesAsset *)asset
{
[_arrayLock lock];
[self->_assetsToLoad removeObject:asset];
[self->_existingAssets addObject:asset];
if (![self->_assetsToLoad count]) {
[self _finish];
}
[_arrayLock unlock];
}
- (void)handleAssetDownloadWithError:(NSError *)error asset:(EXUpdatesAsset *)asset
{
// TODO: retry. for now log an error
NSLog(@"error loading asset %@: %@", asset.key, error.localizedDescription);
[_arrayLock lock];
[self->_assetsToLoad removeObject:asset];
[self->_erroredAssets addObject:asset];
if (![self->_assetsToLoad count]) {
[self _finish];
}
[_arrayLock unlock];
}
- (void)handleAssetDownloadWithData:(NSData *)data response:(nullable NSURLResponse *)response asset:(EXUpdatesAsset *)asset
{
[_arrayLock lock];
[self->_assetsToLoad removeObject:asset];
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
asset.headers = ((NSHTTPURLResponse *)response).allHeaderFields;
}
asset.contentHash = [EXUpdatesUtils sha256WithData:data];
asset.downloadTime = [NSDate date];
[self->_finishedAssets addObject:asset];
if (![self->_assetsToLoad count]) {
[self _finish];
}
[_arrayLock unlock];
}
# pragma mark - internal
- (BOOL)_shouldStartLoadingUpdate:(EXUpdatesUpdate *)updateManifest
{
return _manifestBlock(updateManifest);
}
- (void)_finishWithError:(NSError *)error
{
dispatch_async(_completionQueue, ^{
if (self->_errorBlock) {
self->_errorBlock(error);
}
[self _reset];
});
}
- (void)_finish
{
dispatch_async(_database.databaseQueue, ^{
[self->_arrayLock lock];
for (EXUpdatesAsset *existingAsset in self->_existingAssets) {
NSError *error;
BOOL existingAssetFound = [self->_database addExistingAsset:existingAsset toUpdateWithId:self->_updateManifest.updateId error:&error];
if (!existingAssetFound) {
// the database and filesystem have gotten out of sync
// do our best to create a new entry for this file even though it already existed on disk
NSData *contents = [NSData dataWithContentsOfURL:[self->_directory URLByAppendingPathComponent:existingAsset.filename]];
existingAsset.contentHash = [EXUpdatesUtils sha256WithData:contents];
existingAsset.downloadTime = [NSDate date];
[self->_finishedAssets addObject:existingAsset];
}
if (error) {
NSLog(@"Error searching for existing asset in DB: %@", error.localizedDescription);
}
}
NSError *assetError;
[self->_database addNewAssets:self->_finishedAssets toUpdateWithId:self->_updateManifest.updateId error:&assetError];
if (assetError) {
[self->_arrayLock unlock];
[self _finishWithError:assetError];
return;
}
if (![self->_erroredAssets count]) {
NSError *updateReadyError;
[self->_database markUpdateFinished:self->_updateManifest error:&updateReadyError];
if (updateReadyError) {
[self->_arrayLock unlock];
[self _finishWithError:updateReadyError];
return;
}
}
EXUpdatesAppLoaderSuccessBlock successBlock;
EXUpdatesAppLoaderErrorBlock errorBlock;
if (self->_erroredAssets.count) {
if (self->_errorBlock) {
errorBlock = self->_errorBlock;
}
} else {
if (self->_successBlock) {
successBlock = self->_successBlock;
}
}
[self->_arrayLock unlock];
dispatch_async(self->_completionQueue, ^{
if (errorBlock) {
errorBlock([NSError errorWithDomain:EXUpdatesAppLoaderErrorDomain
code:1012
userInfo:@{NSLocalizedDescriptionKey: @"Failed to load all assets"}]);
} else if (successBlock) {
successBlock(self->_updateManifest);
}
[self _reset];
});
});
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,50 @@
// Copyright © 2020 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLauncher.h>
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesSelectionPolicy.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, EXUpdatesBackgroundUpdateStatus) {
EXUpdatesBackgroundUpdateStatusError = 0,
EXUpdatesBackgroundUpdateStatusNoUpdateAvailable = 1,
EXUpdatesBackgroundUpdateStatusUpdateAvailable = 2
};
@class EXUpdatesAppLoaderTask;
@protocol EXUpdatesAppLoaderTaskDelegate <NSObject>
/**
* This method gives the delegate a backdoor option to ignore the cached update and force
* a remote load if it decides the cached update is not runnable. Returning NO from this
* callback will force a remote load, overriding the timeout and configuration settings for
* whether or not to check for a remote update. Returning YES from this callback will make
* EXUpdatesAppLoaderTask proceed as usual.
*/
- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update;
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update;
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher isUpToDate:(BOOL)isUpToDate;
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error;
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error;
@end
@interface EXUpdatesAppLoaderTask : NSObject
@property (nonatomic, weak) id<EXUpdatesAppLoaderTaskDelegate> delegate;
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
selectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
delegateQueue:(dispatch_queue_t)delegateQueue;
- (void)start;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,299 @@
// Copyright © 2020 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLauncherWithDatabase.h>
#import <EXUpdates/EXUpdatesAppLoaderTask.h>
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
#import <EXUpdates/EXUpdatesReaper.h>
#import <EXUpdates/EXUpdatesRemoteAppLoader.h>
#import <EXUpdates/EXUpdatesUtils.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesAppLoaderTaskErrorDomain = @"EXUpdatesAppLoaderTask";
@interface EXUpdatesAppLoaderTask ()
@property (nonatomic, strong) EXUpdatesConfig *config;
@property (nonatomic, strong) EXUpdatesDatabase *database;
@property (nonatomic, strong) NSURL *directory;
@property (nonatomic, strong) id<EXUpdatesSelectionPolicy> selectionPolicy;
@property (nonatomic, strong) dispatch_queue_t delegateQueue;
@property (nonatomic, strong) id<EXUpdatesAppLauncher> candidateLauncher;
@property (nonatomic, strong) id<EXUpdatesAppLauncher> finalizedLauncher;
@property (nonatomic, strong) EXUpdatesEmbeddedAppLoader *embeddedAppLoader;
@property (nonatomic, strong) EXUpdatesRemoteAppLoader *remoteAppLoader;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) BOOL isReadyToLaunch;
@property (nonatomic, assign) BOOL isTimerFinished;
@property (nonatomic, assign) BOOL hasLaunched;
@property (nonatomic, assign) BOOL isUpToDate;
@property (nonatomic, strong) dispatch_queue_t loaderTaskQueue;
@end
@implementation EXUpdatesAppLoaderTask
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
selectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
delegateQueue:(dispatch_queue_t)delegateQueue
{
if (self = [super init]) {
_config = config;
_database = database;
_directory = directory;
_selectionPolicy = selectionPolicy;
_isUpToDate = NO;
_delegateQueue = delegateQueue;
_loaderTaskQueue = dispatch_queue_create("expo.loader.LoaderTaskQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)start
{
if (!_config.isEnabled) {
dispatch_async(_delegateQueue, ^{
[self->_delegate appLoaderTask:self
didFinishWithError:[NSError errorWithDomain:EXUpdatesAppLoaderTaskErrorDomain code:1030 userInfo:@{
NSLocalizedDescriptionKey: @"EXUpdatesAppLoaderTask was passed a configuration object with updates disabled. You should load updates from an embedded source rather than calling EXUpdatesAppLoaderTask, or enable updates in the configuration."
}]];
});
return;
}
if (!_config.updateUrl) {
dispatch_async(_delegateQueue, ^{
[self->_delegate appLoaderTask:self
didFinishWithError:[NSError errorWithDomain:EXUpdatesAppLoaderTaskErrorDomain code:1030 userInfo:@{
NSLocalizedDescriptionKey: @"EXUpdatesAppLoaderTask was passed a configuration object with a null URL. You must pass a nonnull URL in order to use EXUpdatesAppLoaderTask to load updates."
}]];
});
return;
}
if (!_directory) {
dispatch_async(_delegateQueue, ^{
[self->_delegate appLoaderTask:self
didFinishWithError:[NSError errorWithDomain:EXUpdatesAppLoaderTaskErrorDomain code:1030 userInfo:@{
NSLocalizedDescriptionKey: @"EXUpdatesAppLoaderTask directory must be nonnull."
}]];
});
return;
}
__block BOOL shouldCheckForUpdate = [EXUpdatesUtils shouldCheckForUpdateWithConfig:_config];
NSNumber *launchWaitMs = _config.launchWaitMs;
if ([launchWaitMs isEqualToNumber:@(0)] || !shouldCheckForUpdate) {
self->_isTimerFinished = YES;
} else {
NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:[launchWaitMs doubleValue] / 1000];
self->_timer = [[NSTimer alloc] initWithFireDate:fireDate interval:0 target:self selector:@selector(_timerDidFire) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self->_timer forMode:NSDefaultRunLoopMode];
}
[self _loadEmbeddedUpdateWithCompletion:^{
[self _launchWithCompletion:^(NSError * _Nullable error, BOOL success) {
if (!success) {
if (!shouldCheckForUpdate){
[self _finishWithError:error];
}
NSLog(@"Failed to launch embedded or launchable update: %@", error.localizedDescription);
} else {
if (self->_delegate &&
![self->_delegate appLoaderTask:self didLoadCachedUpdate:self->_candidateLauncher.launchedUpdate]) {
// ignore timer and other settings and force launch a remote update.
self->_candidateLauncher = nil;
[self _stopTimer];
shouldCheckForUpdate = YES;
} else {
self->_isReadyToLaunch = YES;
[self _maybeFinish];
}
}
if (shouldCheckForUpdate) {
[self _loadRemoteUpdateWithCompletion:^(NSError * _Nullable error, EXUpdatesUpdate * _Nullable update) {
[self _handleRemoteUpdateLoaded:update error:error];
}];
} else {
[self _runReaper];
}
}];
}];
}
- (void)_finishWithError:(nullable NSError *)error
{
dispatch_assert_queue(_loaderTaskQueue);
if (_hasLaunched) {
// we've already fired once, don't do it again
return;
}
_hasLaunched = YES;
_finalizedLauncher = _candidateLauncher;
if (_delegate) {
dispatch_async(_delegateQueue, ^{
if (self->_isReadyToLaunch && (self->_finalizedLauncher.launchAssetUrl || self->_finalizedLauncher.launchedUpdate.status == EXUpdatesUpdateStatusDevelopment)) {
[self->_delegate appLoaderTask:self didFinishWithLauncher:self->_finalizedLauncher isUpToDate:self->_isUpToDate];
} else {
[self->_delegate appLoaderTask:self didFinishWithError:error ?: [NSError errorWithDomain:EXUpdatesAppLoaderTaskErrorDomain code:1031 userInfo:@{
NSLocalizedDescriptionKey: @"EXUpdatesAppLoaderTask encountered an unexpected error and could not launch an update."
}]];
}
});
}
[self _stopTimer];
}
- (void)_maybeFinish
{
if (!_isTimerFinished || !_isReadyToLaunch) {
// too early, bail out
return;
}
[self _finishWithError:nil];
}
- (void)_timerDidFire
{
dispatch_async(_loaderTaskQueue, ^{
self->_isTimerFinished = YES;
[self _maybeFinish];
});
}
- (void)_stopTimer
{
if (_timer) {
[_timer invalidate];
_timer = nil;
}
_isTimerFinished = YES;
}
- (void)_runReaper
{
if (_finalizedLauncher.launchedUpdate) {
[EXUpdatesReaper reapUnusedUpdatesWithConfig:_config
database:_database
directory:_directory
selectionPolicy:_selectionPolicy
launchedUpdate:_finalizedLauncher.launchedUpdate];
}
}
- (void)_loadEmbeddedUpdateWithCompletion:(void (^)(void))completion
{
[EXUpdatesAppLauncherWithDatabase launchableUpdateWithConfig:_config database:_database selectionPolicy:_selectionPolicy completion:^(NSError * _Nullable error, EXUpdatesUpdate * _Nullable launchableUpdate) {
if (self->_config.hasEmbeddedUpdate &&
[self->_selectionPolicy shouldLoadNewUpdate:[EXUpdatesEmbeddedAppLoader embeddedManifestWithConfig:self->_config database:self->_database]
withLaunchedUpdate:launchableUpdate]) {
self->_embeddedAppLoader = [[EXUpdatesEmbeddedAppLoader alloc] initWithConfig:self->_config database:self->_database directory:self->_directory completionQueue:self->_loaderTaskQueue];
[self->_embeddedAppLoader loadUpdateFromEmbeddedManifestWithCallback:^BOOL(EXUpdatesUpdate * _Nonnull update) {
// we already checked using selection policy, so we don't need to check again
return YES;
} success:^(EXUpdatesUpdate * _Nullable update) {
completion();
} error:^(NSError * _Nonnull error) {
completion();
}];
} else {
completion();
}
} completionQueue:_loaderTaskQueue];
}
- (void)_launchWithCompletion:(void (^)(NSError * _Nullable error, BOOL success))completion
{
EXUpdatesAppLauncherWithDatabase *launcher = [[EXUpdatesAppLauncherWithDatabase alloc] initWithConfig:_config database:_database directory:_directory completionQueue:_loaderTaskQueue];
_candidateLauncher = launcher;
[launcher launchUpdateWithSelectionPolicy:_selectionPolicy completion:completion];
}
- (void)_loadRemoteUpdateWithCompletion:(void (^)(NSError * _Nullable error, EXUpdatesUpdate * _Nullable update))completion
{
_remoteAppLoader = [[EXUpdatesRemoteAppLoader alloc] initWithConfig:_config database:_database directory:_directory completionQueue:_loaderTaskQueue];
[_remoteAppLoader loadUpdateFromUrl:_config.updateUrl onManifest:^BOOL(EXUpdatesUpdate * _Nonnull update) {
if ([self->_selectionPolicy shouldLoadNewUpdate:update withLaunchedUpdate:self->_candidateLauncher.launchedUpdate]) {
self->_isUpToDate = NO;
if (self->_delegate) {
dispatch_async(self->_delegateQueue, ^{
[self->_delegate appLoaderTask:self didStartLoadingUpdate:update];
});
}
return YES;
} else {
self->_isUpToDate = YES;
return NO;
}
} success:^(EXUpdatesUpdate * _Nullable update) {
completion(nil, update);
} error:^(NSError *error) {
completion(error, nil);
}];
}
- (void)_handleRemoteUpdateLoaded:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error
{
// If the app has not yet been launched (because the timer is still running),
// create a new launcher so that we can launch with the newly downloaded update.
// Otherwise, we've already launched. Send an event to the notify JS of the new update.
dispatch_async(_loaderTaskQueue, ^{
[self _stopTimer];
if (update) {
if (!self->_hasLaunched) {
EXUpdatesAppLauncherWithDatabase *newLauncher = [[EXUpdatesAppLauncherWithDatabase alloc] initWithConfig:self->_config database:self->_database directory:self->_directory completionQueue:self->_loaderTaskQueue];
[newLauncher launchUpdateWithSelectionPolicy:self->_selectionPolicy completion:^(NSError * _Nullable error, BOOL success) {
if (success) {
if (!self->_hasLaunched) {
self->_candidateLauncher = newLauncher;
self->_isReadyToLaunch = YES;
self->_isUpToDate = YES;
[self _finishWithError:nil];
}
} else {
[self _finishWithError:error];
NSLog(@"Downloaded update but failed to relaunch: %@", error.localizedDescription);
}
[self _runReaper];
}];
} else {
[self _didFinishBackgroundUpdateWithStatus:EXUpdatesBackgroundUpdateStatusUpdateAvailable manifest:update error:nil];
[self _runReaper];
}
} else {
// there's no update, so signal we're ready to launch
[self _finishWithError:error];
if (error) {
[self _didFinishBackgroundUpdateWithStatus:EXUpdatesBackgroundUpdateStatusError manifest:nil error:error];
} else {
[self _didFinishBackgroundUpdateWithStatus:EXUpdatesBackgroundUpdateStatusNoUpdateAvailable manifest:nil error:nil];
}
[self _runReaper];
}
});
}
- (void)_didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status manifest:(nullable EXUpdatesUpdate *)manifest error:(nullable NSError *)error
{
if (_delegate) {
dispatch_async(_delegateQueue, ^{
[self->_delegate appLoaderTask:self didFinishBackgroundUpdateWithStatus:status update:manifest error:error];
});
}
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,32 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesAsset : NSObject
/**
* properties determined by asset source
*/
@property (nonatomic, strong) NSString *key;
@property (nonatomic, strong) NSString *type;
@property (nullable, nonatomic, strong) NSURL *url;
@property (nullable, nonatomic, strong) NSDictionary *metadata;
@property (nullable, nonatomic, strong) NSString *mainBundleDir; // used for embedded assets
@property (nullable, nonatomic, strong) NSString *mainBundleFilename; // used for embedded assets
@property (nonatomic, assign) BOOL isLaunchAsset;
/**
* properties determined at runtime by updates implementation
*/
@property (nullable, nonatomic, strong) NSDate *downloadTime;
@property (nullable, nonatomic, strong) NSString *filename;
@property (nullable, nonatomic, strong) NSString *contentHash;
@property (nullable, nonatomic, strong) NSDictionary *headers;
- (instancetype)initWithKey:(NSString *)key type:(NSString *)type;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,26 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAsset.h>
#import <EXUpdates/EXUpdatesUtils.h>
NS_ASSUME_NONNULL_BEGIN
@implementation EXUpdatesAsset
- (instancetype)initWithKey:(NSString *)key type:(NSString *)type
{
if (self = [super init]) {
_key = key;
_type = type;
}
return self;
}
- (nullable NSString *)filename
{
return _key;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,21 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^EXUpdatesVerifySignatureSuccessBlock)(BOOL isValid);
typedef void (^EXUpdatesVerifySignatureErrorBlock)(NSError *error);
@interface EXUpdatesCrypto : NSObject
+ (void)verifySignatureWithData:(NSString *)data
signature:(NSString *)signature
config:(EXUpdatesConfig *)config
cacheDirectory:(NSURL *)cacheDirectory
successBlock:(EXUpdatesVerifySignatureSuccessBlock)successBlock
errorBlock:(EXUpdatesVerifySignatureErrorBlock)errorBlock;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,217 @@
// Copyright 2019-present 650 Industries. All rights reserved.
#import <CommonCrypto/CommonDigest.h>
#import <EXUpdates/EXUpdatesCrypto.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesCryptoPublicKeyUrl = @"https://exp.host/--/manifest-public-key";
static NSString * const EXUpdatesCryptoPublicKeyTag = @"exp.host.publickey";
static NSString * const EXUpdatesCryptoPublicKeyFilename = @"manifestPublicKey.pem";
@implementation EXUpdatesCrypto
+ (void)verifySignatureWithData:(NSString *)data
signature:(NSString *)signature
config:(EXUpdatesConfig *)config
cacheDirectory:(NSURL *)cacheDirectory
successBlock:(EXUpdatesVerifySignatureSuccessBlock)successBlock
errorBlock:(EXUpdatesVerifySignatureErrorBlock)errorBlock
{
[self fetchAndVerifySignatureWithData:data
signature:signature
config:config
cacheDirectory:cacheDirectory
useCache:YES
successBlock:successBlock
errorBlock:errorBlock];
}
+ (void)fetchAndVerifySignatureWithData:(NSString *)data
signature:(NSString *)signature
config:(EXUpdatesConfig *)config
cacheDirectory:(NSURL *)cacheDirectory
useCache:(BOOL)useCache
successBlock:(EXUpdatesVerifySignatureSuccessBlock)successBlock
errorBlock:(EXUpdatesVerifySignatureErrorBlock)errorBlock
{
if (!data || !signature) {
errorBlock([NSError errorWithDomain:@"EXUpdatesCrypto" code:1001 userInfo:@{ NSLocalizedDescriptionKey: @"Cannot verify the manifest because it is empty or has no signature." }]);
return;
}
NSURL *cachedPublicKeyUrl = [cacheDirectory URLByAppendingPathComponent:EXUpdatesCryptoPublicKeyFilename];
if (useCache) {
NSData *publicKeyData = [NSData dataWithContentsOfFile:[cachedPublicKeyUrl absoluteString]];
[[self class] verifyWithPublicKey:publicKeyData signature:signature signedString:data callback:^(BOOL isValid) {
if (isValid) {
successBlock(isValid);
} else {
[[self class] fetchAndVerifySignatureWithData:data
signature:signature
config:config
cacheDirectory:cacheDirectory
useCache:NO
successBlock:successBlock
errorBlock:errorBlock];
}
}];
} else {
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
configuration.requestCachePolicy = NSURLRequestReloadIgnoringCacheData;
EXUpdatesFileDownloader *fileDownloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:config URLSessionConfiguration:configuration];
[fileDownloader downloadFileFromURL:[NSURL URLWithString:EXUpdatesCryptoPublicKeyUrl]
toPath:[cachedPublicKeyUrl path]
successBlock:^(NSData *publicKeyData, NSURLResponse *response) {
[[self class] verifyWithPublicKey:publicKeyData signature:signature signedString:data callback:successBlock];
}
errorBlock:^(NSError *error, NSURLResponse *response) {
errorBlock(error);
}
];
}
}
+ (void)verifyWithPublicKey:(NSData *)publicKeyData
signature:(NSString *)signature
signedString:(NSString *)signedString
callback:(EXUpdatesVerifySignatureSuccessBlock)callback
{
if (!publicKeyData) {
callback(NO);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
SecKeyRef publicKey = [self keyRefFromPEMData:publicKeyData];
NSData *signatureData = [[NSData alloc] initWithBase64EncodedString:signature options:0];
NSData *signedData = [signedString dataUsingEncoding:NSUTF8StringEncoding];
BOOL isValid = NO;
if (publicKey) {
isValid = [self verifyRSASHA256SignedData:signedData signatureData:signatureData publicKey:publicKey];
CFRelease(publicKey);
}
callback(isValid);
});
}
}
/**
* Returns a CFRef to a SecKey given the raw pem data.
* The CFRef should be CFReleased when you're finished.
*
* Here is the Apple doc for this black hole:
* https://developer.apple.com/library/prerelease/content/documentation/Security/Conceptual/CertKeyTrustProgGuide/iPhone_Tasks/iPhone_Tasks.html#//apple_ref/doc/uid/TP40001358-CH208-SW13
*/
+ (nullable SecKeyRef)keyRefFromPEMData:(NSData *)pemData
{
NSString *pemString = [[NSString alloc] initWithData:pemData encoding:NSUTF8StringEncoding];
NSString *key = [NSString string];
NSArray<NSString *> *keyLines = [pemString componentsSeparatedByString:@"\n"];
BOOL foundKey = NO;
for (NSString *line in keyLines) {
if ([line isEqualToString:@"-----BEGIN PUBLIC KEY-----"]) {
foundKey = YES;
} else if ([line isEqualToString:@"-----END PUBLIC KEY-----"]) {
foundKey = NO;
} else if (foundKey) {
key = [key stringByAppendingString:line];
}
}
if (key.length == 0) {
return nil;
}
NSData *keyData = [[NSData alloc] initWithBase64EncodedString:key options:0];
if (keyData == nil) {
return nil;
}
NSData *tag = [NSData dataWithBytes:[EXUpdatesCryptoPublicKeyTag UTF8String] length:[EXUpdatesCryptoPublicKeyTag length]];
// Delete any old lingering key with the same tag.
NSDictionary *deleteParams = @{
(__bridge id)kSecClass: (__bridge id)kSecClassKey,
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrApplicationTag: tag,
};
OSStatus secStatus = SecItemDelete((CFDictionaryRef)deleteParams);
SecKeyRef savedKeyRef = nil;
// Add key to system keychain.
NSDictionary *saveParams = @{
(__bridge id)kSecClass: (__bridge id) kSecClassKey,
(__bridge id)kSecAttrKeyType: (__bridge id) kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrApplicationTag: tag,
(__bridge id)kSecAttrKeyClass: (__bridge id) kSecAttrKeyClassPublic,
(__bridge id)kSecReturnPersistentRef: (__bridge id)kCFBooleanTrue,
(__bridge id)kSecValueData: keyData,
(__bridge id)kSecAttrKeySizeInBits: [NSNumber numberWithUnsignedInteger:keyData.length],
(__bridge id)kSecAttrEffectiveKeySize: [NSNumber numberWithUnsignedInteger:keyData.length],
(__bridge id)kSecAttrCanDerive: (__bridge id) kCFBooleanFalse,
(__bridge id)kSecAttrCanEncrypt: (__bridge id) kCFBooleanTrue,
(__bridge id)kSecAttrCanDecrypt: (__bridge id) kCFBooleanFalse,
(__bridge id)kSecAttrCanVerify: (__bridge id) kCFBooleanTrue,
(__bridge id)kSecAttrCanSign: (__bridge id) kCFBooleanFalse,
(__bridge id)kSecAttrCanWrap: (__bridge id) kCFBooleanTrue,
(__bridge id)kSecAttrCanUnwrap: (__bridge id) kCFBooleanFalse,
};
secStatus = SecItemAdd((CFDictionaryRef)saveParams, (CFTypeRef *)&savedKeyRef);
if (savedKeyRef != nil) {
CFRelease(savedKeyRef);
}
if (secStatus != noErr && secStatus != errSecDuplicateItem) {
return nil;
}
// Fetch the SecKeyRef version of the key.
// note that kSecAttrKeyClass: kSecAttrKeyClassPublic doesn't seem to be required here.
// also: this doesn't work on iOS < 10.0
SecKeyRef keyRef = nil;
NSDictionary *queryParams = @{
(__bridge id)kSecClass: (__bridge id) kSecClassKey,
(__bridge id)kSecAttrKeyType: (__bridge id) kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrApplicationTag: tag,
(__bridge id)kSecReturnRef: (__bridge id) kCFBooleanTrue,
};
secStatus = SecItemCopyMatching((CFDictionaryRef)queryParams, (CFTypeRef *)&keyRef);
if (secStatus != noErr) {
return nil;
}
return keyRef;
}
+ (BOOL)verifyRSASHA256SignedData:(NSData *)signedData signatureData:(NSData *)signatureData publicKey:(nullable SecKeyRef)publicKey
{
if (!publicKey) {
return NO;
}
uint8_t hashBytes[CC_SHA256_DIGEST_LENGTH];
if (!CC_SHA256([signedData bytes], (CC_LONG)[signedData length], hashBytes)) {
return NO;
}
OSStatus status = SecKeyRawVerify(publicKey,
kSecPaddingPKCS1SHA256,
hashBytes,
CC_SHA256_DIGEST_LENGTH,
[signatureData bytes],
[signatureData length]);
return status == errSecSuccess;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,25 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLoader+Private.h>
NS_ASSUME_NONNULL_BEGIN
extern NSString * const EXUpdatesEmbeddedManifestName;
extern NSString * const EXUpdatesEmbeddedManifestType;
extern NSString * const EXUpdatesEmbeddedBundleFilename;
extern NSString * const EXUpdatesEmbeddedBundleFileType;
extern NSString * const EXUpdatesBareEmbeddedBundleFilename;
extern NSString * const EXUpdatesBareEmbeddedBundleFileType;
@interface EXUpdatesEmbeddedAppLoader : EXUpdatesAppLoader
+ (nullable EXUpdatesUpdate *)embeddedManifestWithConfig:(EXUpdatesConfig *)config
database:(nullable EXUpdatesDatabase *)database;
- (void)loadUpdateFromEmbeddedManifestWithCallback:(EXUpdatesAppLoaderManifestBlock)manifestBlock
success:(EXUpdatesAppLoaderSuccessBlock)success
error:(EXUpdatesAppLoaderErrorBlock)error;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,124 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesFileDownloader.h>
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
NS_ASSUME_NONNULL_BEGIN
NSString * const EXUpdatesEmbeddedManifestName = @"app";
NSString * const EXUpdatesEmbeddedManifestType = @"manifest";
NSString * const EXUpdatesEmbeddedBundleFilename = @"app";
NSString * const EXUpdatesEmbeddedBundleFileType = @"bundle";
NSString * const EXUpdatesBareEmbeddedBundleFilename = @"main";
NSString * const EXUpdatesBareEmbeddedBundleFileType = @"jsbundle";
static NSString * const EXUpdatesEmbeddedAppLoaderErrorDomain = @"EXUpdatesEmbeddedAppLoader";
@implementation EXUpdatesEmbeddedAppLoader
+ (nullable EXUpdatesUpdate *)embeddedManifestWithConfig:(EXUpdatesConfig *)config
database:(nullable EXUpdatesDatabase *)database
{
static EXUpdatesUpdate *embeddedManifest;
static dispatch_once_t once;
dispatch_once(&once, ^{
if (!config.hasEmbeddedUpdate) {
embeddedManifest = nil;
} else if (!embeddedManifest) {
NSString *path = [[NSBundle mainBundle] pathForResource:EXUpdatesEmbeddedManifestName ofType:EXUpdatesEmbeddedManifestType];
NSData *manifestData = [NSData dataWithContentsOfFile:path];
if (!manifestData) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"The embedded manifest is invalid or could not be read. Make sure you have configured expo-updates correctly in your Xcode Build Phases."
userInfo:@{}];
}
NSError *err;
id manifest = [NSJSONSerialization JSONObjectWithData:manifestData options:kNilOptions error:&err];
if (!manifest) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"The embedded manifest is invalid or could not be read. Make sure you have configured expo-updates correctly in your Xcode Build Phases."
userInfo:@{}];
} else {
NSAssert([manifest isKindOfClass:[NSDictionary class]], @"embedded manifest should be a valid JSON file");
NSMutableDictionary *mutableManifest = [manifest mutableCopy];
// automatically verify embedded manifest since it was already codesigned
mutableManifest[@"isVerified"] = @(YES);
embeddedManifest = [EXUpdatesUpdate updateWithEmbeddedManifest:[mutableManifest copy]
config:config
database:database];
if (!embeddedManifest.updateId) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"The embedded manifest is invalid. Make sure you have configured expo-updates correctly in your Xcode Build Phases."
userInfo:@{}];
}
}
}
});
return embeddedManifest;
}
- (void)loadUpdateFromEmbeddedManifestWithCallback:(EXUpdatesAppLoaderManifestBlock)manifestBlock
success:(EXUpdatesAppLoaderSuccessBlock)success
error:(EXUpdatesAppLoaderErrorBlock)error
{
EXUpdatesUpdate *embeddedManifest = [[self class] embeddedManifestWithConfig:self.config
database:self.database];
if (embeddedManifest) {
self.manifestBlock = manifestBlock;
self.successBlock = success;
self.errorBlock = error;
[self startLoadingFromManifest:embeddedManifest];
} else {
error([NSError errorWithDomain:EXUpdatesEmbeddedAppLoaderErrorDomain
code:1008
userInfo:@{NSLocalizedDescriptionKey: @"Failed to load embedded manifest. Make sure you have configured expo-updates correctly."}]);
}
}
- (void)downloadAsset:(EXUpdatesAsset *)asset
{
NSURL *destinationUrl = [self.directory URLByAppendingPathComponent:asset.filename];
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
if ([[NSFileManager defaultManager] fileExistsAtPath:[destinationUrl path]]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadAlreadyExists:asset];
});
} else {
NSAssert(asset.mainBundleFilename, @"embedded asset mainBundleFilename must be nonnull");
NSString *bundlePath = asset.mainBundleDir
? [[NSBundle mainBundle] pathForResource:asset.mainBundleFilename ofType:asset.type inDirectory:asset.mainBundleDir]
: [[NSBundle mainBundle] pathForResource:asset.mainBundleFilename ofType:asset.type];
NSAssert(bundlePath, @"NSBundle must contain the expected assets");
if (!bundlePath) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Could not find the expected embedded asset %@.%@. Check that expo-updates is installed correctly.", asset.mainBundleFilename, asset.type]
userInfo:nil];
}
NSError *err;
if ([[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:[destinationUrl path] error:&err]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadWithData:[NSData dataWithContentsOfFile:bundlePath] response:nil asset:asset];
});
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadWithError:err asset:asset];
});
}
}
});
}
- (void)loadUpdateFromUrl:(NSURL *)url
success:(EXUpdatesAppLoaderSuccessBlock)success
error:(EXUpdatesAppLoaderErrorBlock)error
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Should not call EXUpdatesEmbeddedAppLoader#loadUpdateFromUrl" userInfo:nil];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,37 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^EXUpdatesFileDownloaderSuccessBlock)(NSData *data, NSURLResponse *response);
typedef void (^EXUpdatesFileDownloaderManifestSuccessBlock)(EXUpdatesUpdate *update);
typedef void (^EXUpdatesFileDownloaderErrorBlock)(NSError *error, NSURLResponse *response);
@interface EXUpdatesFileDownloader : NSObject
- (instancetype)initWithUpdatesConfig:(EXUpdatesConfig *)updatesConfig;
- (instancetype)initWithUpdatesConfig:(EXUpdatesConfig *)updatesConfig
URLSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration;
- (void)downloadDataFromURL:(NSURL *)url
successBlock:(EXUpdatesFileDownloaderSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock;
- (void)downloadFileFromURL:(NSURL *)url
toPath:(NSString *)destinationPath
successBlock:(EXUpdatesFileDownloaderSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock;
- (void)downloadManifestFromURL:(NSURL *)url
withDatabase:(EXUpdatesDatabase *)database
cacheDirectory:(NSURL *)cacheDirectory
successBlock:(EXUpdatesFileDownloaderManifestSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock;
+ (dispatch_queue_t)assetFilesQueue;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,291 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLauncherNoDatabase.h>
#import <EXUpdates/EXUpdatesCrypto.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
NS_ASSUME_NONNULL_BEGIN
NSString * const EXUpdatesFileDownloaderErrorDomain = @"EXUpdatesFileDownloader";
NSTimeInterval const EXUpdatesDefaultTimeoutInterval = 60;
@interface EXUpdatesFileDownloader () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionConfiguration *sessionConfiguration;
@property (nonatomic, strong) EXUpdatesConfig *config;
@end
@implementation EXUpdatesFileDownloader
- (instancetype)initWithUpdatesConfig:(EXUpdatesConfig *)updatesConfig
{
return [self initWithUpdatesConfig:updatesConfig
URLSessionConfiguration:NSURLSessionConfiguration.defaultSessionConfiguration];
}
- (instancetype)initWithUpdatesConfig:(EXUpdatesConfig *)updatesConfig
URLSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration
{
if (self = [super init]) {
_sessionConfiguration = sessionConfiguration;
_session = [NSURLSession sessionWithConfiguration:_sessionConfiguration delegate:self delegateQueue:nil];
_config = updatesConfig;
}
return self;
}
- (void)dealloc
{
[_session finishTasksAndInvalidate];
}
+ (dispatch_queue_t)assetFilesQueue
{
static dispatch_queue_t theQueue;
static dispatch_once_t once;
dispatch_once(&once, ^{
if (!theQueue) {
theQueue = dispatch_queue_create("expo.controller.AssetFilesQueue", DISPATCH_QUEUE_SERIAL);
}
});
return theQueue;
}
- (void)downloadFileFromURL:(NSURL *)url
toPath:(NSString *)destinationPath
successBlock:(EXUpdatesFileDownloaderSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock
{
[self downloadDataFromURL:url successBlock:^(NSData *data, NSURLResponse *response) {
NSError *error;
if ([data writeToFile:destinationPath options:NSDataWritingAtomic error:&error]) {
successBlock(data, response);
} else {
errorBlock([NSError errorWithDomain:EXUpdatesFileDownloaderErrorDomain
code:1002
userInfo:@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Could not write to path %@: %@", destinationPath, error.localizedDescription],
NSUnderlyingErrorKey: error
}
], response);
}
} errorBlock:errorBlock];
}
- (void)downloadManifestFromURL:(NSURL *)url
withDatabase:(EXUpdatesDatabase *)database
cacheDirectory:(NSURL *)cacheDirectory
successBlock:(EXUpdatesFileDownloaderManifestSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:EXUpdatesDefaultTimeoutInterval];
[self _setManifestHTTPHeaderFields:request];
[self _downloadDataWithRequest:request successBlock:^(NSData *data, NSURLResponse *response) {
NSError *err;
id parsedJson = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&err];
if (err) {
errorBlock(err, response);
return;
}
NSDictionary *manifest = [self _extractManifest:parsedJson error:&err];
if (err) {
errorBlock(err, response);
return;
}
id innerManifestString = manifest[@"manifestString"];
id signature = manifest[@"signature"];
BOOL isSigned = innerManifestString != nil && signature != nil;
// XDL serves unsigned manifests with the `signature` key set to "UNSIGNED".
// We should treat these manifests as unsigned rather than signed with an invalid signature.
if (isSigned && [signature isKindOfClass:[NSString class]] && [(NSString *)signature isEqualToString:@"UNSIGNED"]) {
isSigned = NO;
NSError *err;
manifest = [NSJSONSerialization JSONObjectWithData:[(NSString *)innerManifestString dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&err];
NSAssert(!err && manifest && [manifest isKindOfClass:[NSDictionary class]], @"manifest should be a valid JSON object");
NSMutableDictionary *mutableManifest = [manifest mutableCopy];
mutableManifest[@"isVerified"] = @(NO);
manifest = [mutableManifest copy];
}
if (isSigned) {
NSAssert([innerManifestString isKindOfClass:[NSString class]], @"manifestString should be a string");
NSAssert([signature isKindOfClass:[NSString class]], @"signature should be a string");
[EXUpdatesCrypto verifySignatureWithData:(NSString *)innerManifestString
signature:(NSString *)signature
config:self->_config
cacheDirectory:cacheDirectory
successBlock:^(BOOL isValid) {
if (isValid) {
NSError *err;
id innerManifest = [NSJSONSerialization JSONObjectWithData:[(NSString *)innerManifestString dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&err];
NSAssert(!err && innerManifest && [innerManifest isKindOfClass:[NSDictionary class]], @"manifest should be a valid JSON object");
NSMutableDictionary *mutableInnerManifest = [(NSDictionary *)innerManifest mutableCopy];
mutableInnerManifest[@"isVerified"] = @(YES);
EXUpdatesUpdate *update = [EXUpdatesUpdate updateWithManifest:[mutableInnerManifest copy]
config:self->_config
database:database];
successBlock(update);
} else {
NSError *error = [NSError errorWithDomain:EXUpdatesFileDownloaderErrorDomain code:1003 userInfo:@{NSLocalizedDescriptionKey: @"Manifest verification failed"}];
errorBlock(error, response);
}
}
errorBlock:^(NSError *error) {
errorBlock(error, response);
}
];
} else {
EXUpdatesUpdate *update = [EXUpdatesUpdate updateWithManifest:(NSDictionary *)manifest
config:self->_config
database:database];
successBlock(update);
}
} errorBlock:errorBlock];
}
- (void)downloadDataFromURL:(NSURL *)url
successBlock:(EXUpdatesFileDownloaderSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock
{
// pass any custom cache policy onto this specific request
NSURLRequestCachePolicy cachePolicy = _sessionConfiguration ? _sessionConfiguration.requestCachePolicy : NSURLRequestUseProtocolCachePolicy;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:cachePolicy timeoutInterval:EXUpdatesDefaultTimeoutInterval];
[self _setHTTPHeaderFields:request];
[self _downloadDataWithRequest:request successBlock:successBlock errorBlock:errorBlock];
}
- (void)_downloadDataWithRequest:(NSURLRequest *)request
successBlock:(EXUpdatesFileDownloaderSuccessBlock)successBlock
errorBlock:(EXUpdatesFileDownloaderErrorBlock)errorBlock
{
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error && [response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
NSStringEncoding encoding = [self _encodingFromResponse:response];
NSString *body = [[NSString alloc] initWithData:data encoding:encoding];
error = [self _errorFromResponse:httpResponse body:body];
}
}
if (error) {
errorBlock(error, response);
} else {
successBlock(data, response);
}
}];
[task resume];
}
- (nullable NSDictionary *)_extractManifest:(id)parsedJson error:(NSError **)error
{
if ([parsedJson isKindOfClass:[NSDictionary class]]) {
return (NSDictionary *)parsedJson;
} else if ([parsedJson isKindOfClass:[NSArray class]]) {
// TODO: either add support for runtimeVersion or deprecate multi-manifests
for (id providedManifest in (NSArray *)parsedJson) {
if ([providedManifest isKindOfClass:[NSDictionary class]] && providedManifest[@"sdkVersion"]){
NSString *sdkVersion = providedManifest[@"sdkVersion"];
NSArray<NSString *> *supportedSdkVersions = [_config.sdkVersion componentsSeparatedByString:@","];
if ([supportedSdkVersions containsObject:sdkVersion]){
return providedManifest;
}
}
}
}
if (error) {
*error = [NSError errorWithDomain:EXUpdatesFileDownloaderErrorDomain code:1009 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No compatible update found at %@. Only %@ are supported.", _config.updateUrl.absoluteString, _config.sdkVersion]}];
}
return nil;
}
- (void)_setHTTPHeaderFields:(NSMutableURLRequest *)request
{
[request setValue:@"ios" forHTTPHeaderField:@"Expo-Platform"];
[request setValue:@"1" forHTTPHeaderField:@"Expo-Api-Version"];
[request setValue:@"BARE" forHTTPHeaderField:@"Expo-Updates-Environment"];
for (NSString *key in _config.requestHeaders) {
[request setValue:_config.requestHeaders[key] forHTTPHeaderField:key];
}
}
- (void)_setManifestHTTPHeaderFields:(NSMutableURLRequest *)request
{
[request setValue:@"application/expo+json,application/json" forHTTPHeaderField:@"Accept"];
[request setValue:@"true" forHTTPHeaderField:@"Expo-JSON-Error"];
[request setValue:@"true" forHTTPHeaderField:@"Expo-Accept-Signature"];
[request setValue:_config.releaseChannel forHTTPHeaderField:@"Expo-Release-Channel"];
NSString *runtimeVersion = _config.runtimeVersion;
if (runtimeVersion) {
[request setValue:runtimeVersion forHTTPHeaderField:@"Expo-Runtime-Version"];
} else {
[request setValue:_config.sdkVersion forHTTPHeaderField:@"Expo-SDK-Version"];
}
NSString *previousFatalError = [EXUpdatesAppLauncherNoDatabase consumeError];
if (previousFatalError) {
// some servers can have max length restrictions for headers,
// so we restrict the length of the string to 1024 characters --
// this should satisfy the requirements of most servers
if ([previousFatalError length] > 1024) {
previousFatalError = [previousFatalError substringToIndex:1024];
}
[request setValue:previousFatalError forHTTPHeaderField:@"Expo-Fatal-Error"];
}
[self _setHTTPHeaderFields:request];
}
#pragma mark - NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler
{
completionHandler(request);
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
completionHandler(proposedResponse);
}
#pragma mark - Parsing the response
- (NSStringEncoding)_encodingFromResponse:(NSURLResponse *)response
{
if (response.textEncodingName) {
CFStringRef cfEncodingName = (__bridge CFStringRef)response.textEncodingName;
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding(cfEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) {
return CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
// Default to UTF-8
return NSUTF8StringEncoding;
}
- (NSError *)_errorFromResponse:(NSHTTPURLResponse *)response body:(NSString *)body
{
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: body,
};
return [NSError errorWithDomain:EXUpdatesFileDownloaderErrorDomain code:response.statusCode userInfo:userInfo];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,11 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLoader+Private.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesRemoteAppLoader : EXUpdatesAppLoader
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,77 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesRemoteAppLoader.h>
#import <EXUpdates/EXUpdatesCrypto.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesRemoteAppLoader ()
@property (nonatomic, strong) EXUpdatesFileDownloader *downloader;
@end
static NSString * const EXUpdatesRemoteAppLoaderErrorDomain = @"EXUpdatesRemoteAppLoader";
@implementation EXUpdatesRemoteAppLoader
- (instancetype)initWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
completionQueue:(dispatch_queue_t)completionQueue
{
if (self = [super initWithConfig:config database:database directory:directory completionQueue:completionQueue]) {
_downloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:self.config];
}
return self;
}
- (void)loadUpdateFromUrl:(NSURL *)url
onManifest:(EXUpdatesAppLoaderManifestBlock)manifestBlock
success:(EXUpdatesAppLoaderSuccessBlock)success
error:(EXUpdatesAppLoaderErrorBlock)error
{
self.manifestBlock = manifestBlock;
self.successBlock = success;
self.errorBlock = error;
[_downloader downloadManifestFromURL:url withDatabase:self.database cacheDirectory:self.directory successBlock:^(EXUpdatesUpdate *update) {
[self startLoadingFromManifest:update];
} errorBlock:^(NSError *error, NSURLResponse *response) {
if (self.errorBlock) {
self.errorBlock(error);
}
}];
}
- (void)downloadAsset:(EXUpdatesAsset *)asset
{
NSURL *urlOnDisk = [self.directory URLByAppendingPathComponent:asset.filename];
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
if ([[NSFileManager defaultManager] fileExistsAtPath:[urlOnDisk path]]) {
// file already exists, we don't need to download it again
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadAlreadyExists:asset];
});
} else {
if (!asset.url) {
[self handleAssetDownloadWithError:[NSError errorWithDomain:EXUpdatesRemoteAppLoaderErrorDomain code:1006 userInfo:@{NSLocalizedDescriptionKey: @"Failed to download asset with no URL provided"}] asset:asset];
return;
}
[self->_downloader downloadFileFromURL:asset.url toPath:[urlOnDisk path] successBlock:^(NSData *data, NSURLResponse *response) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadWithData:data response:response asset:asset];
});
} errorBlock:^(NSError *error, NSURLResponse *response) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAssetDownloadWithError:error asset:asset];
});
}];
}
});
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,39 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAsset.h>
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, EXUpdatesDatabaseHashType) {
EXUpdatesDatabaseHashTypeSha1 = 0
};
@interface EXUpdatesDatabase : NSObject
@property (nonatomic, strong) dispatch_queue_t databaseQueue;
- (BOOL)openDatabaseInDirectory:(NSURL *)directory withError:(NSError ** _Nullable)error;
- (void)closeDatabase;
- (void)addUpdate:(EXUpdatesUpdate *)update error:(NSError ** _Nullable)error;
- (void)addNewAssets:(NSArray<EXUpdatesAsset *> *)assets toUpdateWithId:(NSUUID *)updateId error:(NSError ** _Nullable)error;
- (BOOL)addExistingAsset:(EXUpdatesAsset *)asset toUpdateWithId:(NSUUID *)updateId error:(NSError ** _Nullable)error;
- (void)updateAsset:(EXUpdatesAsset *)asset error:(NSError ** _Nullable)error;
- (void)mergeAsset:(EXUpdatesAsset *)asset withExistingEntry:(EXUpdatesAsset *)existingAsset error:(NSError ** _Nullable)error;
- (void)markUpdateFinished:(EXUpdatesUpdate *)update error:(NSError ** _Nullable)error;
- (void)setScopeKey:(NSString *)scopeKey onUpdate:(EXUpdatesUpdate *)update error:(NSError ** _Nullable)error;
- (void)deleteUpdates:(NSArray<EXUpdatesUpdate *> *)updates error:(NSError ** _Nullable)error;
- (nullable NSArray<EXUpdatesAsset *> *)deleteUnusedAssetsWithError:(NSError ** _Nullable)error;
- (nullable NSArray<EXUpdatesUpdate *> *)allUpdatesWithConfig:(EXUpdatesConfig *)config error:(NSError ** _Nullable)error;
- (nullable NSArray<EXUpdatesUpdate *> *)launchableUpdatesWithConfig:(EXUpdatesConfig *)config error:(NSError ** _Nullable)error;
- (nullable EXUpdatesUpdate *)updateWithId:(NSUUID *)updateId config:(EXUpdatesConfig *)config error:(NSError ** _Nullable)error;
- (nullable NSArray<EXUpdatesAsset *> *)assetsWithUpdateId:(NSUUID *)updateId error:(NSError ** _Nullable)error;
- (nullable EXUpdatesAsset *)assetWithKey:(NSString *)key error:(NSError ** _Nullable)error;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,644 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesDatabase.h>
#import <sqlite3.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesDatabase ()
@property (nonatomic, assign) sqlite3 *db;
@property (nonatomic, readwrite, strong) NSLock *lock;
@end
static NSString * const EXUpdatesDatabaseErrorDomain = @"EXUpdatesDatabase";
static NSString * const EXUpdatesDatabaseFilename = @"expo-v4.db";
@implementation EXUpdatesDatabase
# pragma mark - lifecycle
- (instancetype)init
{
if (self = [super init]) {
_databaseQueue = dispatch_queue_create("expo.database.DatabaseQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (BOOL)openDatabaseInDirectory:(NSURL *)directory withError:(NSError ** _Nullable)error
{
sqlite3 *db;
NSURL *dbUrl = [directory URLByAppendingPathComponent:EXUpdatesDatabaseFilename];
BOOL shouldInitializeDatabase = ![[NSFileManager defaultManager] fileExistsAtPath:[dbUrl path]];
int resultCode = sqlite3_open([[dbUrl path] UTF8String], &db);
if (resultCode != SQLITE_OK) {
NSLog(@"Error opening SQLite db: %@", [self _errorFromSqlite:_db].localizedDescription);
sqlite3_close(db);
if (resultCode == SQLITE_CORRUPT || resultCode == SQLITE_NOTADB) {
NSString *archivedDbFilename = [NSString stringWithFormat:@"%f-%@", [[NSDate date] timeIntervalSince1970], EXUpdatesDatabaseFilename];
NSURL *destinationUrl = [directory URLByAppendingPathComponent:archivedDbFilename];
NSError *err;
if ([[NSFileManager defaultManager] moveItemAtURL:dbUrl toURL:destinationUrl error:&err]) {
NSLog(@"Moved corrupt SQLite db to %@", archivedDbFilename);
if (sqlite3_open([[dbUrl absoluteString] UTF8String], &db) != SQLITE_OK) {
if (error != nil) {
*error = [self _errorFromSqlite:_db];
}
return NO;
}
shouldInitializeDatabase = YES;
} else {
NSString *description = [NSString stringWithFormat:@"Could not move existing corrupt database: %@", [err localizedDescription]];
if (error != nil) {
*error = [NSError errorWithDomain:EXUpdatesDatabaseErrorDomain
code:1004
userInfo:@{ NSLocalizedDescriptionKey: description, NSUnderlyingErrorKey: err }];
}
return NO;
}
} else {
if (error != nil) {
*error = [self _errorFromSqlite:_db];
}
return NO;
}
}
_db = db;
if (shouldInitializeDatabase) {
return [self _initializeDatabase:error];
}
return YES;
}
- (void)closeDatabase
{
sqlite3_close(_db);
_db = nil;
}
- (void)dealloc
{
[self closeDatabase];
}
- (BOOL)_initializeDatabase:(NSError **)error
{
NSAssert(_db, @"Missing database handle");
dispatch_assert_queue(_databaseQueue);
NSString * const createTableStmts = @"\
PRAGMA foreign_keys = ON;\
CREATE TABLE \"updates\" (\
\"id\" BLOB UNIQUE,\
\"scope_key\" TEXT NOT NULL,\
\"commit_time\" INTEGER NOT NULL,\
\"runtime_version\" TEXT NOT NULL,\
\"launch_asset_id\" INTEGER,\
\"metadata\" TEXT,\
\"status\" INTEGER NOT NULL,\
\"keep\" INTEGER NOT NULL,\
PRIMARY KEY(\"id\"),\
FOREIGN KEY(\"launch_asset_id\") REFERENCES \"assets\"(\"id\") ON DELETE CASCADE\
);\
CREATE TABLE \"assets\" (\
\"id\" INTEGER PRIMARY KEY AUTOINCREMENT,\
\"url\" TEXT,\
\"key\" TEXT NOT NULL UNIQUE,\
\"headers\" TEXT,\
\"type\" TEXT NOT NULL,\
\"metadata\" TEXT,\
\"download_time\" INTEGER NOT NULL,\
\"relative_path\" TEXT NOT NULL,\
\"hash\" BLOB NOT NULL,\
\"hash_type\" INTEGER NOT NULL,\
\"marked_for_deletion\" INTEGER NOT NULL\
);\
CREATE TABLE \"updates_assets\" (\
\"update_id\" BLOB NOT NULL,\
\"asset_id\" INTEGER NOT NULL,\
FOREIGN KEY(\"update_id\") REFERENCES \"updates\"(\"id\") ON DELETE CASCADE,\
FOREIGN KEY(\"asset_id\") REFERENCES \"assets\"(\"id\") ON DELETE CASCADE\
);\
CREATE TABLE \"json_data\" (\
\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\
\"key\" TEXT NOT NULL,\
\"value\" TEXT NOT NULL,\
\"last_updated\" INTEGER NOT NULL,\
\"scope_key\" TEXT NOT NULL\
);\
CREATE UNIQUE INDEX \"index_updates_scope_key_commit_time\" ON \"updates\" (\"scope_key\", \"commit_time\");\
CREATE INDEX \"index_updates_launch_asset_id\" ON \"updates\" (\"launch_asset_id\");\
CREATE INDEX \"index_json_data_scope_key\" ON \"json_data\" (\"scope_key\")\
";
char *errMsg;
if (sqlite3_exec(_db, [createTableStmts UTF8String], NULL, NULL, &errMsg) != SQLITE_OK) {
if (error != nil) {
*error = [self _errorFromSqlite:_db];
}
sqlite3_free(errMsg);
return NO;
};
return YES;
}
# pragma mark - insert and update
- (void)addUpdate:(EXUpdatesUpdate *)update error:(NSError ** _Nullable)error
{
NSString * const sql = @"INSERT INTO \"updates\" (\"id\", \"scope_key\", \"commit_time\", \"runtime_version\", \"metadata\", \"status\" , \"keep\")\
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1);";
[self _executeSql:sql
withArgs:@[
update.updateId,
update.scopeKey,
@([update.commitTime timeIntervalSince1970] * 1000),
update.runtimeVersion,
update.metadata ?: [NSNull null],
@(EXUpdatesUpdateStatusPending)
]
error:error];
}
- (void)addNewAssets:(NSArray<EXUpdatesAsset *> *)assets toUpdateWithId:(NSUUID *)updateId error:(NSError ** _Nullable)error
{
sqlite3_exec(_db, "BEGIN;", NULL, NULL, NULL);
for (EXUpdatesAsset *asset in assets) {
NSAssert(asset.downloadTime, @"asset downloadTime should be nonnull");
NSAssert(asset.filename, @"asset filename should be nonnull");
NSAssert(asset.contentHash, @"asset contentHash should be nonnull");
NSString * const assetInsertSql = @"INSERT OR REPLACE INTO \"assets\" (\"key\", \"url\", \"headers\", \"type\", \"metadata\", \"download_time\", \"relative_path\", \"hash\", \"hash_type\", \"marked_for_deletion\")\
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 0);";
if ([self _executeSql:assetInsertSql
withArgs:@[
asset.key,
asset.url ? asset.url.absoluteString : [NSNull null],
asset.headers ?: [NSNull null],
asset.type,
asset.metadata ?: [NSNull null],
@(asset.downloadTime.timeIntervalSince1970 * 1000),
asset.filename,
asset.contentHash,
@(EXUpdatesDatabaseHashTypeSha1)
]
error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return;
}
// statements must stay in precisely this order for last_insert_rowid() to work correctly
if (asset.isLaunchAsset) {
NSString * const updateSql = @"UPDATE updates SET launch_asset_id = last_insert_rowid() WHERE id = ?1;";
if ([self _executeSql:updateSql withArgs:@[updateId] error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return;
}
}
NSString * const updateInsertSql = @"INSERT OR REPLACE INTO updates_assets (\"update_id\", \"asset_id\") VALUES (?1, last_insert_rowid());";
if ([self _executeSql:updateInsertSql withArgs:@[updateId] error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return;
}
}
sqlite3_exec(_db, "COMMIT;", NULL, NULL, NULL);
}
- (BOOL)addExistingAsset:(EXUpdatesAsset *)asset toUpdateWithId:(NSUUID *)updateId error:(NSError ** _Nullable)error
{
BOOL success;
sqlite3_exec(_db, "BEGIN;", NULL, NULL, NULL);
NSString * const assetSelectSql = @"SELECT id FROM assets WHERE \"key\" = ?1 LIMIT 1;";
NSArray<NSDictionary *> *rows = [self _executeSql:assetSelectSql withArgs:@[asset.key] error:error];
if (!rows || ![rows count]) {
success = NO;
} else {
NSNumber *assetId = rows[0][@"id"];
NSString * const insertSql = @"INSERT OR REPLACE INTO updates_assets (\"update_id\", \"asset_id\") VALUES (?1, ?2);";
if ([self _executeSql:insertSql withArgs:@[updateId, assetId] error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return NO;
}
if (asset.isLaunchAsset) {
NSString * const updateSql = @"UPDATE updates SET launch_asset_id = ?1 WHERE id = ?2;";
if ([self _executeSql:updateSql withArgs:@[assetId, updateId] error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return NO;
}
}
success = YES;
}
sqlite3_exec(_db, "COMMIT;", NULL, NULL, NULL);
return success;
}
- (void)updateAsset:(EXUpdatesAsset *)asset error:(NSError ** _Nullable)error
{
NSAssert(asset.downloadTime, @"asset downloadTime should be nonnull");
NSAssert(asset.filename, @"asset filename should be nonnull");
NSAssert(asset.contentHash, @"asset contentHash should be nonnull");
NSString * const assetUpdateSql = @"UPDATE \"assets\" SET \"headers\" = ?2, \"type\" = ?3, \"metadata\" = ?4, \"download_time\" = ?5, \"relative_path\" = ?6, \"hash\" = ?7, \"url\" = ?8 WHERE \"key\" = ?1;";
[self _executeSql:assetUpdateSql
withArgs:@[
asset.key,
asset.headers ?: [NSNull null],
asset.type,
asset.metadata ?: [NSNull null],
@(asset.downloadTime.timeIntervalSince1970 * 1000),
asset.filename,
asset.contentHash,
asset.url ? asset.url.absoluteString : [NSNull null]
]
error:error];
}
- (void)mergeAsset:(EXUpdatesAsset *)asset withExistingEntry:(EXUpdatesAsset *)existingAsset error:(NSError ** _Nullable)error
{
// if the existing entry came from an embedded manifest, it may not have a URL in the database
if (asset.url && !existingAsset.url) {
existingAsset.url = asset.url;
[self updateAsset:existingAsset error:error];
}
// all other properties should be overridden by database values
asset.filename = existingAsset.filename;
asset.contentHash = existingAsset.contentHash;
asset.downloadTime = existingAsset.downloadTime;
}
- (void)markUpdateFinished:(EXUpdatesUpdate *)update error:(NSError ** _Nullable)error
{
if (update.status != EXUpdatesUpdateStatusDevelopment) {
update.status = EXUpdatesUpdateStatusReady;
}
NSString * const updateSql = @"UPDATE updates SET status = ?1, keep = 1 WHERE id = ?2;";
[self _executeSql:updateSql
withArgs:@[
@(update.status),
update.updateId
]
error:error];
}
- (void)setScopeKey:(NSString *)scopeKey onUpdate:(EXUpdatesUpdate *)update error:(NSError ** _Nullable)error
{
NSString * const updateSql = @"UPDATE updates SET scope_key = ?1 WHERE id = ?2;";
[self _executeSql:updateSql withArgs:@[scopeKey, update.updateId] error:error];
}
# pragma mark - delete
- (void)deleteUpdates:(NSArray<EXUpdatesUpdate *> *)updates error:(NSError ** _Nullable)error
{
sqlite3_exec(_db, "BEGIN;", NULL, NULL, NULL);
NSString * const sql = @"DELETE FROM updates WHERE id = ?1;";
for (EXUpdatesUpdate *update in updates) {
if ([self _executeSql:sql withArgs:@[update.updateId] error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return;
}
}
sqlite3_exec(_db, "COMMIT;", NULL, NULL, NULL);
}
- (nullable NSArray<EXUpdatesAsset *> *)deleteUnusedAssetsWithError:(NSError ** _Nullable)error
{
// the simplest way to mark the assets we want to delete
// is to mark all assets for deletion, then go back and unmark
// those assets in updates we want to keep
// this is safe as long as we do this inside of a transaction
sqlite3_exec(_db, "BEGIN;", NULL, NULL, NULL);
NSString * const update1Sql = @"UPDATE assets SET marked_for_deletion = 1;";
if ([self _executeSql:update1Sql withArgs:nil error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return nil;
}
NSString * const update2Sql = @"UPDATE assets SET marked_for_deletion = 0 WHERE id IN (\
SELECT asset_id \
FROM updates_assets \
INNER JOIN updates ON updates_assets.update_id = updates.id\
WHERE updates.keep = 1\
);";
if ([self _executeSql:update2Sql withArgs:nil error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return nil;
}
NSString * const selectSql = @"SELECT * FROM assets WHERE marked_for_deletion = 1;";
NSArray<NSDictionary *> *rows = [self _executeSql:selectSql withArgs:nil error:error];
if (!rows) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return nil;
}
NSMutableArray *assets = [NSMutableArray new];
for (NSDictionary *row in rows) {
[assets addObject:[self _assetWithRow:row]];
}
NSString * const deleteSql = @"DELETE FROM assets WHERE marked_for_deletion = 1;";
if ([self _executeSql:deleteSql withArgs:nil error:error] == nil) {
sqlite3_exec(_db, "ROLLBACK;", NULL, NULL, NULL);
return nil;
}
sqlite3_exec(_db, "COMMIT;", NULL, NULL, NULL);
return assets;
}
# pragma mark - select
- (nullable NSArray<EXUpdatesUpdate *> *)allUpdatesWithConfig:(EXUpdatesConfig *)config error:(NSError ** _Nullable)error
{
NSString * const sql = @"SELECT * FROM updates WHERE scope_key = ?1;";
NSArray<NSDictionary *> *rows = [self _executeSql:sql withArgs:@[config.scopeKey] error:error];
if (!rows) {
return nil;
}
NSMutableArray<EXUpdatesUpdate *> *launchableUpdates = [NSMutableArray new];
for (NSDictionary *row in rows) {
[launchableUpdates addObject:[self _updateWithRow:row config:config]];
}
return launchableUpdates;
}
- (nullable NSArray<EXUpdatesUpdate *> *)launchableUpdatesWithConfig:(EXUpdatesConfig *)config error:(NSError ** _Nullable)error
{
NSString *sql = [NSString stringWithFormat:@"SELECT *\
FROM updates\
WHERE scope_key = ?1\
AND status IN (%li, %li, %li);", (long)EXUpdatesUpdateStatusReady, (long)EXUpdatesUpdateStatusEmbedded, (long)EXUpdatesUpdateStatusDevelopment];
NSArray<NSDictionary *> *rows = [self _executeSql:sql withArgs:@[config.scopeKey] error:error];
if (!rows) {
return nil;
}
NSMutableArray<EXUpdatesUpdate *> *launchableUpdates = [NSMutableArray new];
for (NSDictionary *row in rows) {
[launchableUpdates addObject:[self _updateWithRow:row config:config]];
}
return launchableUpdates;
}
- (nullable EXUpdatesUpdate *)updateWithId:(NSUUID *)updateId config:(EXUpdatesConfig *)config error:(NSError ** _Nullable)error
{
NSString * const sql = @"SELECT *\
FROM updates\
WHERE updates.id = ?1;";
NSArray<NSDictionary *> *rows = [self _executeSql:sql withArgs:@[updateId] error:error];
if (!rows || ![rows count]) {
return nil;
} else {
return [self _updateWithRow:rows[0] config:config];
}
}
- (nullable NSArray<EXUpdatesAsset *> *)assetsWithUpdateId:(NSUUID *)updateId error:(NSError ** _Nullable)error
{
NSString * const sql = @"SELECT assets.id, \"key\", url, type, relative_path, assets.metadata, launch_asset_id\
FROM assets\
INNER JOIN updates_assets ON updates_assets.asset_id = assets.id\
INNER JOIN updates ON updates_assets.update_id = updates.id\
WHERE updates.id = ?1;";
NSArray<NSDictionary *> *rows = [self _executeSql:sql withArgs:@[updateId] error:error];
if (!rows) {
return nil;
}
NSMutableArray<EXUpdatesAsset *> *assets = [NSMutableArray arrayWithCapacity:rows.count];
for (NSDictionary *row in rows) {
[assets addObject:[self _assetWithRow:row]];
}
return assets;
}
- (nullable EXUpdatesAsset *)assetWithKey:(NSString *)key error:(NSError ** _Nullable)error
{
NSString * const sql = @"SELECT * FROM assets WHERE \"key\" = ?1 LIMIT 1;";
NSArray<NSDictionary *> *rows = [self _executeSql:sql withArgs:@[key] error:error];
if (!rows || ![rows count]) {
return nil;
} else {
return [self _assetWithRow:rows[0]];
}
}
# pragma mark - helper methods
- (nullable NSArray<NSDictionary *> *)_executeSql:(NSString *)sql withArgs:(nullable NSArray *)args error:(NSError ** _Nullable)error
{
NSAssert(_db, @"Missing database handle");
dispatch_assert_queue(_databaseQueue);
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
if (error != nil) {
*error = [self _errorFromSqlite:_db];
}
return nil;
}
if (args) {
if (![self _bindStatement:stmt withArgs:args]) {
if (error != nil) {
*error = [self _errorFromSqlite:_db];
}
return nil;
}
}
NSMutableArray *rows = [NSMutableArray arrayWithCapacity:0];
NSMutableArray *columnNames = [NSMutableArray arrayWithCapacity:0];
int columnCount = 0;
BOOL didFetchColumns = NO;
int result;
BOOL hasMore = YES;
BOOL didError = NO;
while (hasMore) {
result = sqlite3_step(stmt);
switch (result) {
case SQLITE_ROW: {
if (!didFetchColumns) {
// get all column names once at the beginning
columnCount = sqlite3_column_count(stmt);
for (int i = 0; i < columnCount; i++) {
[columnNames addObject:[NSString stringWithUTF8String:sqlite3_column_name(stmt, i)]];
}
didFetchColumns = YES;
}
NSMutableDictionary *entry = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
id columnValue = [self _getValueWithStatement:stmt column:i];
entry[columnNames[i]] = columnValue;
}
[rows addObject:entry];
break;
}
case SQLITE_DONE:
hasMore = NO;
break;
default:
didError = YES;
hasMore = NO;
break;
}
}
if (didError && error != nil) {
*error = [self _errorFromSqlite:_db];
}
sqlite3_finalize(stmt);
return didError ? nil : rows;
}
- (id)_getValueWithStatement:(sqlite3_stmt *)stmt column:(int)column
{
int columnType = sqlite3_column_type(stmt, column);
switch (columnType) {
case SQLITE_INTEGER:
return @(sqlite3_column_int64(stmt, column));
case SQLITE_FLOAT:
return @(sqlite3_column_double(stmt, column));
case SQLITE_BLOB:
NSAssert(sqlite3_column_bytes(stmt, column) == 16, @"SQLite BLOB value should be a valid UUID");
return [[NSUUID alloc] initWithUUIDBytes:sqlite3_column_blob(stmt, column)];
case SQLITE_TEXT:
return [[NSString alloc] initWithBytes:(char *)sqlite3_column_text(stmt, column)
length:sqlite3_column_bytes(stmt, column)
encoding:NSUTF8StringEncoding];
}
return [NSNull null];
}
- (BOOL)_bindStatement:(sqlite3_stmt *)stmt withArgs:(NSArray *)args
{
__block BOOL success = YES;
[args enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isKindOfClass:[NSUUID class]]) {
uuid_t bytes;
[((NSUUID *)obj) getUUIDBytes:bytes];
if (sqlite3_bind_blob(stmt, (int)idx + 1, bytes, 16, SQLITE_TRANSIENT) != SQLITE_OK) {
success = NO;
*stop = YES;
}
} else if ([obj isKindOfClass:[NSNumber class]]) {
if (sqlite3_bind_int64(stmt, (int)idx + 1, [((NSNumber *)obj) longLongValue]) != SQLITE_OK) {
success = NO;
*stop = YES;
}
} else if ([obj isKindOfClass:[NSDictionary class]]) {
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:(NSDictionary *)obj options:kNilOptions error:&error];
if (!error && sqlite3_bind_text(stmt, (int)idx + 1, jsonData.bytes, (int)jsonData.length, SQLITE_TRANSIENT) != SQLITE_OK) {
success = NO;
*stop = YES;
}
} else if ([obj isKindOfClass:[NSNull class]]) {
if (sqlite3_bind_null(stmt, (int)idx + 1) != SQLITE_OK) {
success = NO;
*stop = YES;
}
} else {
// convert to string
NSString *string = [obj isKindOfClass:[NSString class]] ? (NSString *)obj : [obj description];
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
if (sqlite3_bind_text(stmt, (int)idx + 1, data.bytes, (int)data.length, SQLITE_TRANSIENT) != SQLITE_OK) {
success = NO;
*stop = YES;
}
}
}];
return success;
}
- (NSError *)_errorFromSqlite:(struct sqlite3 *)db
{
int code = sqlite3_errcode(db);
int extendedCode = sqlite3_extended_errcode(db);
NSString *message = [NSString stringWithUTF8String:sqlite3_errmsg(db)];
return [NSError errorWithDomain:EXUpdatesDatabaseErrorDomain
code:extendedCode
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error code %i: %@ (extended error code %i)", code, message, extendedCode]}];
}
- (EXUpdatesUpdate *)_updateWithRow:(NSDictionary *)row config:(EXUpdatesConfig *)config
{
NSError *error;
id metadata = nil;
id rowMetadata = row[@"metadata"];
if ([rowMetadata isKindOfClass:[NSString class]]) {
metadata = [NSJSONSerialization JSONObjectWithData:[(NSString *)rowMetadata dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error];
NSAssert(!error && metadata && [metadata isKindOfClass:[NSDictionary class]], @"Update metadata should be a valid JSON object");
}
EXUpdatesUpdate *update = [EXUpdatesUpdate updateWithId:row[@"id"]
scopeKey:row[@"scope_key"]
commitTime:[NSDate dateWithTimeIntervalSince1970:[(NSNumber *)row[@"commit_time"] doubleValue] / 1000]
runtimeVersion:row[@"runtime_version"]
metadata:metadata
status:(EXUpdatesUpdateStatus)[(NSNumber *)row[@"status"] integerValue]
keep:[(NSNumber *)row[@"keep"] boolValue]
config:config
database:self];
return update;
}
- (EXUpdatesAsset *)_assetWithRow:(NSDictionary *)row
{
NSError *error;
id metadata = nil;
id rowMetadata = row[@"metadata"];
if ([rowMetadata isKindOfClass:[NSString class]]) {
metadata = [NSJSONSerialization JSONObjectWithData:[(NSString *)rowMetadata dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error];
NSAssert(!error && metadata && [metadata isKindOfClass:[NSDictionary class]], @"Asset metadata should be a valid JSON object");
}
id launchAssetId = row[@"launch_asset_id"];
id rowUrl = row[@"url"];
NSURL *url;
if (rowUrl && [rowUrl isKindOfClass:[NSString class]]) {
url = [NSURL URLWithString:rowUrl];
}
EXUpdatesAsset *asset = [[EXUpdatesAsset alloc] initWithKey:row[@"key"] type:row[@"type"]];
asset.url = url;
asset.downloadTime = [NSDate dateWithTimeIntervalSince1970:([(NSNumber *)row[@"download_time"] doubleValue] / 1000)];
asset.filename = row[@"relative_path"];
asset.contentHash = row[@"hash"];
asset.metadata = metadata;
asset.isLaunchAsset = (launchAssetId && [launchAssetId isKindOfClass:[NSNumber class]])
? [(NSNumber *)launchAssetId isEqualToNumber:(NSNumber *)row[@"id"]]
: NO;
return asset;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,20 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesSelectionPolicy.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesReaper : NSObject
+ (void)reapUnusedUpdatesWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
selectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
launchedUpdate:(EXUpdatesUpdate *)launchedUpdate;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,79 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesFileDownloader.h>
#import <EXUpdates/EXUpdatesReaper.h>
NS_ASSUME_NONNULL_BEGIN
@implementation EXUpdatesReaper
+ (void)reapUnusedUpdatesWithConfig:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
directory:(NSURL *)directory
selectionPolicy:(id<EXUpdatesSelectionPolicy>)selectionPolicy
launchedUpdate:(EXUpdatesUpdate *)launchedUpdate
{
dispatch_async(database.databaseQueue, ^{
NSError *error;
NSDate *beginDeleteFromDatabase = [NSDate date];
[database markUpdateFinished:launchedUpdate error:&error];
if (error) {
NSLog(@"Error reaping updates: %@", error.localizedDescription);
return;
}
NSArray<EXUpdatesUpdate *> *allUpdates = [database allUpdatesWithConfig:config error:&error];
if (!allUpdates || error) {
NSLog(@"Error reaping updates: %@", error.localizedDescription);
return;
}
NSArray<EXUpdatesUpdate *> *updatesToDelete = [selectionPolicy updatesToDeleteWithLaunchedUpdate:launchedUpdate updates:allUpdates];
[database deleteUpdates:updatesToDelete error:&error];
if (error) {
NSLog(@"Error reaping updates: %@", error.localizedDescription);
return;
}
NSArray<EXUpdatesAsset *> *assetsForDeletion = [database deleteUnusedAssetsWithError:&error];
if (error) {
NSLog(@"Error reaping updates: %@", error.localizedDescription);
return;
}
NSLog(@"Deleted assets and updates from SQLite in %f ms", [beginDeleteFromDatabase timeIntervalSinceNow] * -1000);
dispatch_async([EXUpdatesFileDownloader assetFilesQueue], ^{
NSUInteger deletedAssets = 0;
NSMutableArray<EXUpdatesAsset *> *erroredAssets = [NSMutableArray new];
NSDate *beginDeleteAssets = [NSDate date];
for (EXUpdatesAsset *asset in assetsForDeletion) {
NSURL *localUrl = [directory URLByAppendingPathComponent:asset.filename];
NSError *error;
if ([NSFileManager.defaultManager fileExistsAtPath:localUrl.path] && ![NSFileManager.defaultManager removeItemAtURL:localUrl error:&error]) {
NSLog(@"Error deleting asset at %@: %@", localUrl, error.localizedDescription);
[erroredAssets addObject:asset];
} else {
deletedAssets++;
}
}
NSLog(@"Deleted %lu assets from disk in %f ms", (unsigned long)deletedAssets, [beginDeleteAssets timeIntervalSinceNow] * -1000);
NSDate *beginRetryDeletes = [NSDate date];
// retry errored deletions
for (EXUpdatesAsset *asset in erroredAssets) {
NSURL *localUrl = [directory URLByAppendingPathComponent:asset.filename];
NSError *error;
if ([NSFileManager.defaultManager fileExistsAtPath:localUrl.path] && ![NSFileManager.defaultManager removeItemAtURL:localUrl error:&error]) {
NSLog(@"Retried deleting asset at %@ and failed again: %@", localUrl, error.localizedDescription);
}
}
NSLog(@"Retried deleting assets from disk in %f ms", [beginRetryDeletes timeIntervalSinceNow] * -1000);
});
});
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,99 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppLoader.h>
#import <EXUpdates/EXUpdatesAppLoaderTask.h>
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
#import <EXUpdates/EXUpdatesSelectionPolicy.h>
#import <EXUpdates/EXUpdatesService.h>
#import <React/RCTBridge.h>
NS_ASSUME_NONNULL_BEGIN
@class EXUpdatesAppController;
@protocol EXUpdatesAppControllerDelegate <NSObject>
- (void)appController:(EXUpdatesAppController *)appController didStartWithSuccess:(BOOL)success;
@end
@interface EXUpdatesAppController : NSObject <EXUpdatesAppLoaderTaskDelegate>
/**
Delegate which will be notified when EXUpdates has an update ready to launch and
`launchAssetUrl` is nonnull.
*/
@property (nonatomic, weak) id<EXUpdatesAppControllerDelegate> delegate;
/**
The RCTBridge for which EXUpdates is providing the JS bundle and assets.
This is optional, but required in order for `Updates.reload()` and Updates module events to work.
*/
@property (nonatomic, weak) RCTBridge *bridge;
/**
The URL on disk to source asset for the RCTBridge.
Will be null until the EXUpdatesAppController delegate method is called.
This should be provided in the `sourceURLForBridge:` method of RCTBridgeDelegate.
*/
@property (nullable, nonatomic, readonly, strong) NSURL *launchAssetUrl;
/**
A dictionary of the locally downloaded assets for the current update. Keys are the remote URLs
of the assets and values are local paths. This should be exported by the Updates JS module and
can be used by `expo-asset` or a similar module to override React Native's asset resolution and
use the locally downloaded assets.
*/
@property (nullable, nonatomic, readonly, strong) NSDictionary *assetFilesMap;
@property (nonatomic, readonly, assign) BOOL isUsingEmbeddedAssets;
/**
for internal use in EXUpdates
*/
@property (nonatomic, readonly) EXUpdatesConfig *config;
@property (nonatomic, readonly) EXUpdatesDatabase *database;
@property (nonatomic, readonly) id<EXUpdatesSelectionPolicy> selectionPolicy;
@property (nonatomic, readonly) NSURL *updatesDirectory;
@property (nonatomic, readonly) dispatch_queue_t assetFilesQueue;
@property (nonatomic, readonly, assign) BOOL isStarted;
@property (nonatomic, readonly, assign) BOOL isEmergencyLaunch;
@property (nullable, nonatomic, readonly, strong) EXUpdatesUpdate *launchedUpdate;
+ (instancetype)sharedInstance;
/**
Overrides the configuration values specified in Expo.plist with the ones provided in this
dictionary. This method can be used if any of these values should be determined at runtime
instead of buildtime. If used, this method must be called before any other method on the
shared instance of EXUpdatesAppController.
*/
- (void)setConfiguration:(NSDictionary *)configuration;
/**
Starts the update process to launch a previously-loaded update and (if configured to do so)
check for a new update from the server. This method should be called as early as possible in
the application's lifecycle.
Note that iOS may stop showing the app's splash screen in case the update is taking a while
to load. If your splash screen setup is simple, you may want to use the
`startAndShowLaunchScreen:` method instead.
*/
- (void)start;
/**
Starts the update process to launch a previously-loaded update and (if configured to do so)
check for a new update from the server. This method should be called as early as possible in
the application's lifecycle.
Note that iOS may stop showing the app's splash screen in case the update is taking a while
to load. This method will attempt to find `LaunchScreen.xib` and load it into view while the
update is loading.
*/
- (void)startAndShowLaunchScreen:(UIWindow *)window;
- (void)requestRelaunchWithCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,286 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppController.h>
#import <EXUpdates/EXUpdatesAppLauncher.h>
#import <EXUpdates/EXUpdatesAppLauncherNoDatabase.h>
#import <EXUpdates/EXUpdatesAppLauncherWithDatabase.h>
#import <EXUpdates/EXUpdatesReaper.h>
#import <EXUpdates/EXUpdatesSelectionPolicyNewest.h>
#import <EXUpdates/EXUpdatesUtils.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesAppControllerErrorDomain = @"EXUpdatesAppController";
static NSString * const EXUpdatesConfigPlistName = @"Expo";
static NSString * const EXUpdatesUpdateAvailableEventName = @"updateAvailable";
static NSString * const EXUpdatesNoUpdateAvailableEventName = @"noUpdateAvailable";
static NSString * const EXUpdatesErrorEventName = @"error";
@interface EXUpdatesAppController ()
@property (nonatomic, readwrite, strong) EXUpdatesConfig *config;
@property (nonatomic, readwrite, strong) id<EXUpdatesAppLauncher> launcher;
@property (nonatomic, readwrite, strong) EXUpdatesDatabase *database;
@property (nonatomic, readwrite, strong) id<EXUpdatesSelectionPolicy> selectionPolicy;
@property (nonatomic, readwrite, strong) dispatch_queue_t assetFilesQueue;
@property (nonatomic, readwrite, strong) NSURL *updatesDirectory;
@property (nonatomic, strong) id<EXUpdatesAppLauncher> candidateLauncher;
@property (nonatomic, assign) BOOL hasLaunched;
@property (nonatomic, strong) dispatch_queue_t controllerQueue;
@property (nonatomic, assign) BOOL isStarted;
@property (nonatomic, assign) BOOL isEmergencyLaunch;
@end
@implementation EXUpdatesAppController
+ (instancetype)sharedInstance
{
static EXUpdatesAppController *theController;
static dispatch_once_t once;
dispatch_once(&once, ^{
if (!theController) {
theController = [[EXUpdatesAppController alloc] init];
}
});
return theController;
}
- (instancetype)init
{
if (self = [super init]) {
_config = [self _loadConfigFromExpoPlist];
_database = [[EXUpdatesDatabase alloc] init];
_selectionPolicy = [[EXUpdatesSelectionPolicyNewest alloc] initWithRuntimeVersion:[EXUpdatesUtils getRuntimeVersionWithConfig:_config]];
_assetFilesQueue = dispatch_queue_create("expo.controller.AssetFilesQueue", DISPATCH_QUEUE_SERIAL);
_controllerQueue = dispatch_queue_create("expo.controller.ControllerQueue", DISPATCH_QUEUE_SERIAL);
_isStarted = NO;
}
return self;
}
- (void)setConfiguration:(NSDictionary *)configuration
{
if (_updatesDirectory) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"EXUpdatesAppController:setConfiguration should not be called after start"
userInfo:@{}];
}
[_config loadConfigFromDictionary:configuration];
_selectionPolicy = [[EXUpdatesSelectionPolicyNewest alloc] initWithRuntimeVersion:[EXUpdatesUtils getRuntimeVersionWithConfig:_config]];
}
- (void)start
{
NSAssert(!_updatesDirectory, @"EXUpdatesAppController:start should only be called once per instance");
if (!_config.isEnabled) {
EXUpdatesAppLauncherNoDatabase *launcher = [[EXUpdatesAppLauncherNoDatabase alloc] init];
_launcher = launcher;
[launcher launchUpdateWithConfig:_config];
if (_delegate) {
[_delegate appController:self didStartWithSuccess:self.launchAssetUrl != nil];
}
return;
}
if (!_config.updateUrl) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"expo-updates is enabled, but no valid URL is configured under EXUpdatesURL. If you are making a release build for the first time, make sure you have run `expo publish` at least once."
userInfo:@{}];
}
_isStarted = YES;
NSError *fsError;
_updatesDirectory = [EXUpdatesUtils initializeUpdatesDirectoryWithError:&fsError];
if (fsError) {
[self _emergencyLaunchWithFatalError:fsError];
return;
}
__block BOOL dbSuccess;
__block NSError *dbError;
dispatch_semaphore_t dbSemaphore = dispatch_semaphore_create(0);
dispatch_async(_database.databaseQueue, ^{
dbSuccess = [self->_database openDatabaseInDirectory:self->_updatesDirectory withError:&dbError];
dispatch_semaphore_signal(dbSemaphore);
});
dispatch_semaphore_wait(dbSemaphore, DISPATCH_TIME_FOREVER);
if (!dbSuccess) {
[self _emergencyLaunchWithFatalError:dbError];
return;
}
EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config
database:_database
directory:_updatesDirectory
selectionPolicy:_selectionPolicy
delegateQueue:_controllerQueue];
loaderTask.delegate = self;
[loaderTask start];
}
- (void)startAndShowLaunchScreen:(UIWindow *)window
{
NSBundle *mainBundle = [NSBundle mainBundle];
UIViewController *rootViewController = [UIViewController new];
NSString *launchScreen = (NSString *)[mainBundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"] ?: @"LaunchScreen";
if ([mainBundle pathForResource:launchScreen ofType:@"nib"] != nil) {
NSArray *views = [mainBundle loadNibNamed:launchScreen owner:self options:nil];
rootViewController.view = views.firstObject;
rootViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
} else if ([mainBundle pathForResource:launchScreen ofType:@"storyboard"] != nil ||
[mainBundle pathForResource:launchScreen ofType:@"storyboardc"] != nil) {
UIStoryboard *launchScreenStoryboard = [UIStoryboard storyboardWithName:launchScreen bundle:nil];
rootViewController = [launchScreenStoryboard instantiateInitialViewController];
} else {
NSLog(@"Launch screen could not be loaded from a .xib or .storyboard. Unexpected loading behavior may occur.");
UIView *view = [UIView new];
view.backgroundColor = [UIColor whiteColor];
rootViewController.view = view;
}
window.rootViewController = rootViewController;
[window makeKeyAndVisible];
[self start];
}
- (void)requestRelaunchWithCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion
{
if (_bridge) {
EXUpdatesAppLauncherWithDatabase *launcher = [[EXUpdatesAppLauncherWithDatabase alloc] initWithConfig:_config database:_database directory:_updatesDirectory completionQueue:_controllerQueue];
_candidateLauncher = launcher;
[launcher launchUpdateWithSelectionPolicy:self->_selectionPolicy completion:^(NSError * _Nullable error, BOOL success) {
if (success) {
self->_launcher = self->_candidateLauncher;
completion(YES);
[self->_bridge reload];
[self _runReaper];
} else {
NSLog(@"Failed to relaunch: %@", error.localizedDescription);
completion(NO);
}
}];
} else {
NSLog(@"EXUpdatesAppController: Failed to reload because bridge was nil. Did you set the bridge property on the controller singleton?");
completion(NO);
}
}
- (nullable EXUpdatesUpdate *)launchedUpdate
{
return _launcher.launchedUpdate ?: nil;
}
- (nullable NSURL *)launchAssetUrl
{
return _launcher.launchAssetUrl ?: nil;
}
- (nullable NSDictionary *)assetFilesMap
{
return _launcher.assetFilesMap ?: nil;
}
- (BOOL)isUsingEmbeddedAssets
{
if (!_launcher) {
return YES;
}
return _launcher.isUsingEmbeddedAssets;
}
# pragma mark - EXUpdatesAppLoaderTaskDelegate
- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(nonnull EXUpdatesUpdate *)update
{
return YES;
}
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update
{
// do nothing here for now
}
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher isUpToDate:(BOOL)isUpToDate
{
_launcher = launcher;
if (self->_delegate) {
[EXUpdatesUtils runBlockOnMainThread:^{
[self->_delegate appController:self didStartWithSuccess:YES];
}];
}
}
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error
{
[self _emergencyLaunchWithFatalError:error];
}
- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error
{
if (status == EXUpdatesBackgroundUpdateStatusError) {
NSAssert(error != nil, @"Background update with error status must have a nonnull error object");
[EXUpdatesUtils sendEventToBridge:_bridge withType:EXUpdatesErrorEventName body:@{@"message": error.localizedDescription}];
} else if (status == EXUpdatesBackgroundUpdateStatusUpdateAvailable) {
NSAssert(update != nil, @"Background update with error status must have a nonnull update object");
[EXUpdatesUtils sendEventToBridge:_bridge withType:EXUpdatesUpdateAvailableEventName body:@{@"manifest": update.rawManifest}];
} else if (status == EXUpdatesBackgroundUpdateStatusNoUpdateAvailable) {
[EXUpdatesUtils sendEventToBridge:_bridge withType:EXUpdatesNoUpdateAvailableEventName body:@{}];
}
}
# pragma mark - internal
- (EXUpdatesConfig *)_loadConfigFromExpoPlist
{
NSString *configPath = [[NSBundle mainBundle] pathForResource:EXUpdatesConfigPlistName ofType:@"plist"];
if (!configPath) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Cannot load configuration from Expo.plist. Please ensure you've followed the setup and installation instructions for expo-updates to create Expo.plist and add it to your Xcode project."
userInfo:@{}];
}
return [EXUpdatesConfig configWithDictionary:[NSDictionary dictionaryWithContentsOfFile:configPath]];
}
- (void)_runReaper
{
if (_launcher.launchedUpdate) {
[EXUpdatesReaper reapUnusedUpdatesWithConfig:_config
database:_database
directory:_updatesDirectory
selectionPolicy:_selectionPolicy
launchedUpdate:_launcher.launchedUpdate];
}
}
- (void)_emergencyLaunchWithFatalError:(NSError *)error
{
_isEmergencyLaunch = YES;
EXUpdatesAppLauncherNoDatabase *launcher = [[EXUpdatesAppLauncherNoDatabase alloc] init];
_launcher = launcher;
[launcher launchUpdateWithConfig:_config fatalError:error];
if (_delegate) {
[EXUpdatesUtils runBlockOnMainThread:^{
[self->_delegate appController:self didStartWithSuccess:self.launchAssetUrl != nil];
}];
}
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,34 @@
// Copyright © 2019 650 Industries. All rights reserved.
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, EXUpdatesCheckAutomaticallyConfig) {
EXUpdatesCheckAutomaticallyConfigAlways = 0,
EXUpdatesCheckAutomaticallyConfigWifiOnly = 1,
EXUpdatesCheckAutomaticallyConfigNever = 2
};
@interface EXUpdatesConfig : NSObject
@property (nonatomic, readonly) BOOL isEnabled;
@property (nonatomic, readonly) NSString *scopeKey;
@property (nonatomic, readonly) NSURL *updateUrl;
@property (nonatomic, readonly) NSDictionary *requestHeaders;
@property (nonatomic, readonly) NSString *releaseChannel;
@property (nonatomic, readonly) NSNumber *launchWaitMs;
@property (nonatomic, readonly) EXUpdatesCheckAutomaticallyConfig checkOnLaunch;
@property (nullable, nonatomic, readonly) NSString *sdkVersion;
@property (nullable, nonatomic, readonly) NSString *runtimeVersion;
@property (nonatomic, readonly) BOOL usesLegacyManifest;
@property (nonatomic, readonly) BOOL hasEmbeddedUpdate;
+ (instancetype)configWithDictionary:(NSDictionary *)config;
- (void)loadConfigFromDictionary:(NSDictionary *)config;
+ (NSString *)normalizedURLOrigin:(NSURL *)url;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,172 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesConfig ()
@property (nonatomic, readwrite, assign) BOOL isEnabled;
@property (nonatomic, readwrite, strong) NSString *scopeKey;
@property (nonatomic, readwrite, strong) NSURL *updateUrl;
@property (nonatomic, readwrite, strong) NSDictionary *requestHeaders;
@property (nonatomic, readwrite, strong) NSString *releaseChannel;
@property (nonatomic, readwrite, strong) NSNumber *launchWaitMs;
@property (nonatomic, readwrite, assign) EXUpdatesCheckAutomaticallyConfig checkOnLaunch;
@property (nullable, nonatomic, readwrite, strong) NSString *sdkVersion;
@property (nullable, nonatomic, readwrite, strong) NSString *runtimeVersion;
@end
static NSString * const EXUpdatesDefaultReleaseChannelName = @"default";
static NSString * const EXUpdatesConfigEnabledKey = @"EXUpdatesEnabled";
static NSString * const EXUpdatesConfigScopeKeyKey = @"EXUpdatesScopeKey";
static NSString * const EXUpdatesConfigUpdateUrlKey = @"EXUpdatesURL";
static NSString * const EXUpdatesConfigRequestHeadersKey = @"EXUpdatesRequestHeaders";
static NSString * const EXUpdatesConfigReleaseChannelKey = @"EXUpdatesReleaseChannel";
static NSString * const EXUpdatesConfigLaunchWaitMsKey = @"EXUpdatesLaunchWaitMs";
static NSString * const EXUpdatesConfigCheckOnLaunchKey = @"EXUpdatesCheckOnLaunch";
static NSString * const EXUpdatesConfigSDKVersionKey = @"EXUpdatesSDKVersion";
static NSString * const EXUpdatesConfigRuntimeVersionKey = @"EXUpdatesRuntimeVersion";
static NSString * const EXUpdatesConfigUsesLegacyManifestKey = @"EXUpdatesUsesLegacyManifest";
static NSString * const EXUpdatesConfigHasEmbeddedUpdateKey = @"EXUpdatesHasEmbeddedUpdate";
static NSString * const EXUpdatesConfigAlwaysString = @"ALWAYS";
static NSString * const EXUpdatesConfigWifiOnlyString = @"WIFI_ONLY";
static NSString * const EXUpdatesConfigNeverString = @"NEVER";
@implementation EXUpdatesConfig
- (instancetype)init
{
if (self = [super init]) {
_isEnabled = YES;
_requestHeaders = @{};
_releaseChannel = EXUpdatesDefaultReleaseChannelName;
_launchWaitMs = @(0);
_checkOnLaunch = EXUpdatesCheckAutomaticallyConfigAlways;
_usesLegacyManifest = YES;
_hasEmbeddedUpdate = YES;
}
return self;
}
+ (instancetype)configWithDictionary:(NSDictionary *)config
{
EXUpdatesConfig *updatesConfig = [[EXUpdatesConfig alloc] init];
[updatesConfig loadConfigFromDictionary:config];
return updatesConfig;
}
- (void)loadConfigFromDictionary:(NSDictionary *)config
{
id isEnabled = config[EXUpdatesConfigEnabledKey];
if (isEnabled && [isEnabled isKindOfClass:[NSNumber class]]) {
_isEnabled = [(NSNumber *)isEnabled boolValue];
}
id updateUrl = config[EXUpdatesConfigUpdateUrlKey];
if (updateUrl && [updateUrl isKindOfClass:[NSString class]]) {
NSURL *url = [NSURL URLWithString:(NSString *)updateUrl];
_updateUrl = url;
}
id scopeKey = config[EXUpdatesConfigScopeKeyKey];
if (scopeKey && [scopeKey isKindOfClass:[NSString class]]) {
_scopeKey = (NSString *)scopeKey;
}
// set updateUrl as the default value if none is provided
if (!_scopeKey) {
if (_updateUrl) {
_scopeKey = [[self class] normalizedURLOrigin:_updateUrl];
} else {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"expo-updates must be configured with a valid update URL or scope key."
userInfo:@{}];
}
}
id requestHeaders = config[EXUpdatesConfigRequestHeadersKey];
if (requestHeaders && [requestHeaders isKindOfClass:[NSDictionary class]]) {
_requestHeaders = (NSDictionary *)requestHeaders;
}
id releaseChannel = config[EXUpdatesConfigReleaseChannelKey];
if (releaseChannel && [releaseChannel isKindOfClass:[NSString class]]) {
_releaseChannel = (NSString *)releaseChannel;
}
id launchWaitMs = config[EXUpdatesConfigLaunchWaitMsKey];
if (launchWaitMs && [launchWaitMs isKindOfClass:[NSNumber class]]) {
_launchWaitMs = (NSNumber *)launchWaitMs;
} else if (launchWaitMs && [launchWaitMs isKindOfClass:[NSString class]]) {
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterNoStyle;
_launchWaitMs = [formatter numberFromString:(NSString *)launchWaitMs];
}
id checkOnLaunch = config[EXUpdatesConfigCheckOnLaunchKey];
if (checkOnLaunch && [checkOnLaunch isKindOfClass:[NSString class]]) {
if ([EXUpdatesConfigNeverString isEqualToString:(NSString *)checkOnLaunch]) {
_checkOnLaunch = EXUpdatesCheckAutomaticallyConfigNever;
} else if ([EXUpdatesConfigWifiOnlyString isEqualToString:(NSString *)checkOnLaunch]) {
_checkOnLaunch = EXUpdatesCheckAutomaticallyConfigWifiOnly;
} else if ([EXUpdatesConfigAlwaysString isEqualToString:(NSString *)checkOnLaunch]) {
_checkOnLaunch = EXUpdatesCheckAutomaticallyConfigAlways;
}
}
id sdkVersion = config[EXUpdatesConfigSDKVersionKey];
if (sdkVersion && [sdkVersion isKindOfClass:[NSString class]]) {
_sdkVersion = (NSString *)sdkVersion;
}
id runtimeVersion = config[EXUpdatesConfigRuntimeVersionKey];
if (runtimeVersion && [runtimeVersion isKindOfClass:[NSString class]]) {
_runtimeVersion = (NSString *)runtimeVersion;
}
NSAssert(_sdkVersion || _runtimeVersion, @"One of EXUpdatesSDKVersion or EXUpdatesRuntimeVersion must be configured in expo-updates");
id usesLegacyManifest = config[EXUpdatesConfigUsesLegacyManifestKey];
if (usesLegacyManifest && [usesLegacyManifest isKindOfClass:[NSNumber class]]) {
_usesLegacyManifest = [(NSNumber *)usesLegacyManifest boolValue];
}
id hasEmbeddedUpdate = config[EXUpdatesConfigHasEmbeddedUpdateKey];
if (hasEmbeddedUpdate && [hasEmbeddedUpdate isKindOfClass:[NSNumber class]]) {
_hasEmbeddedUpdate = [(NSNumber *)hasEmbeddedUpdate boolValue];
}
}
+ (NSString *)normalizedURLOrigin:(NSURL *)url
{
NSString *scheme = url.scheme;
NSNumber *port = url.port;
if (port && port.integerValue > -1 && [port isEqual:[[self class] defaultPortForScheme:scheme]]) {
port = nil;
}
return (port && port.integerValue > -1)
? [NSString stringWithFormat:@"%@://%@:%ld", scheme, url.host, (long)port.integerValue]
: [NSString stringWithFormat:@"%@://%@", scheme, url.host];
}
+ (nullable NSNumber *)defaultPortForScheme:(NSString *)scheme
{
if ([@"http" isEqualToString:scheme] || [@"ws" isEqualToString:scheme]) {
return @(80);
} else if ([@"https" isEqualToString:scheme] || [@"wss" isEqualToString:scheme]) {
return @(443);
} else if ([@"ftp" isEqualToString:scheme]) {
return @(21);
}
return nil;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,7 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <UMCore/UMExportedModule.h>
#import <UMCore/UMModuleRegistryConsumer.h>
@interface EXUpdatesModule : UMExportedModule <UMModuleRegistryConsumer>
@end

View File

@ -0,0 +1,129 @@
// Copyright 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
#import <EXUpdates/EXUpdatesModule.h>
#import <EXUpdates/EXUpdatesRemoteAppLoader.h>
#import <EXUpdates/EXUpdatesService.h>
#import <EXUpdates/EXUpdatesUpdate.h>
@interface EXUpdatesModule ()
@property (nonatomic, weak) id<EXUpdatesInterface> updatesService;
@end
@implementation EXUpdatesModule
UM_EXPORT_MODULE(ExpoUpdates);
- (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
{
_updatesService = [moduleRegistry getModuleImplementingProtocol:@protocol(EXUpdatesInterface)];
}
- (NSDictionary *)constantsToExport
{
if (!_updatesService.isStarted) {
return @{
@"isEnabled": @(NO)
};
}
EXUpdatesUpdate *launchedUpdate = _updatesService.launchedUpdate;
if (!launchedUpdate) {
return @{
@"isEnabled": @(NO)
};
} else {
return @{
@"isEnabled": @(YES),
@"isUsingEmbeddedAssets": @(_updatesService.isUsingEmbeddedAssets),
@"updateId": launchedUpdate.updateId.UUIDString ?: @"",
@"manifest": launchedUpdate.rawManifest ?: @{},
@"releaseChannel": _updatesService.config.releaseChannel,
@"localAssets": _updatesService.assetFilesMap ?: @{},
@"isEmergencyLaunch": @(_updatesService.isEmergencyLaunch)
};
}
}
UM_EXPORT_METHOD_AS(reload,
reloadAsync:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (!_updatesService.canRelaunch) {
reject(@"ERR_UPDATES_DISABLED", @"The updates module controller has not been properly initialized. If you're in development mode, you cannot use this method. Otherwise, make sure you have called [[EXUpdatesAppController sharedInstance] start].", nil);
return;
}
[_updatesService requestRelaunchWithCompletion:^(BOOL success) {
if (success) {
resolve(nil);
} else {
reject(@"ERR_UPDATES_RELOAD", @"Could not reload application. Ensure you have set the `bridge` property of EXUpdatesAppController.", nil);
}
}];
}
UM_EXPORT_METHOD_AS(checkForUpdateAsync,
checkForUpdateAsync:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (!_updatesService.isStarted) {
reject(@"ERR_UPDATES_DISABLED", @"The updates module controller has not been properly initialized. If you're in development mode, you cannot check for updates. Otherwise, make sure you have called [[EXUpdatesAppController sharedInstance] start].", nil);
return;
}
EXUpdatesFileDownloader *fileDownloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:_updatesService.config];
[fileDownloader downloadManifestFromURL:_updatesService.config.updateUrl
withDatabase:_updatesService.database
cacheDirectory:_updatesService.directory
successBlock:^(EXUpdatesUpdate *update) {
EXUpdatesUpdate *launchedUpdate = self->_updatesService.launchedUpdate;
id<EXUpdatesSelectionPolicy> selectionPolicy = self->_updatesService.selectionPolicy;
if ([selectionPolicy shouldLoadNewUpdate:update withLaunchedUpdate:launchedUpdate]) {
resolve(@{
@"isAvailable": @(YES),
@"manifest": update.rawManifest
});
} else {
resolve(@{
@"isAvailable": @(NO)
});
}
} errorBlock:^(NSError *error, NSURLResponse *response) {
reject(@"ERR_UPDATES_CHECK", error.localizedDescription, error);
}];
}
UM_EXPORT_METHOD_AS(fetchUpdateAsync,
fetchUpdateAsync:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (!_updatesService.isStarted) {
reject(@"ERR_UPDATES_DISABLED", @"The updates module controller has not been properly initialized. If you're in development mode, you cannot fetch updates. Otherwise, make sure you have called [[EXUpdatesAppController sharedInstance] start].", nil);
return;
}
EXUpdatesRemoteAppLoader *remoteAppLoader = [[EXUpdatesRemoteAppLoader alloc] initWithConfig:_updatesService.config database:_updatesService.database directory:_updatesService.directory completionQueue:self.methodQueue];
[remoteAppLoader loadUpdateFromUrl:_updatesService.config.updateUrl onManifest:^BOOL(EXUpdatesUpdate * _Nonnull update) {
return [self->_updatesService.selectionPolicy shouldLoadNewUpdate:update withLaunchedUpdate:self->_updatesService.launchedUpdate];
} success:^(EXUpdatesUpdate * _Nullable update) {
if (update) {
resolve(@{
@"isNew": @(YES),
@"manifest": update.rawManifest
});
} else {
resolve(@{
@"isNew": @(NO)
});
}
} error:^(NSError * _Nonnull error) {
reject(@"ERR_UPDATES_FETCH", @"Failed to download new update", error);
}];
}
@end

View File

@ -0,0 +1,35 @@
// Copyright 2020-present 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesConfig.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesSelectionPolicy.h>
#import <EXUpdates/EXUpdatesUpdate.h>
#import <UMCore/UMInternalModule.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^EXUpdatesAppRelaunchCompletionBlock)(BOOL success);
@protocol EXUpdatesInterface
@property (nonatomic, readonly) EXUpdatesConfig *config;
@property (nonatomic, readonly) EXUpdatesDatabase *database;
@property (nonatomic, readonly) id<EXUpdatesSelectionPolicy> selectionPolicy;
@property (nonatomic, readonly) NSURL *directory;
@property (nullable, nonatomic, readonly, strong) EXUpdatesUpdate *launchedUpdate;
@property (nullable, nonatomic, readonly, strong) NSDictionary *assetFilesMap;
@property (nonatomic, readonly, assign) BOOL isUsingEmbeddedAssets;
@property (nonatomic, readonly, assign) BOOL isStarted;
@property (nonatomic, readonly, assign) BOOL isEmergencyLaunch;
@property (nonatomic, readonly, assign) BOOL canRelaunch;
- (void)requestRelaunchWithCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion;
@end
@interface EXUpdatesService : NSObject <UMInternalModule, EXUpdatesInterface>
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,75 @@
// Copyright 2020-present 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAppController.h>
#import <EXUpdates/EXUpdatesService.h>
#import <UMCore/UMUtilities.h>
NS_ASSUME_NONNULL_BEGIN
@implementation EXUpdatesService
UM_REGISTER_MODULE();
+ (const NSArray<Protocol *> *)exportedInterfaces
{
return @[@protocol(EXUpdatesInterface)];
}
- (EXUpdatesConfig *)config
{
return EXUpdatesAppController.sharedInstance.config;
}
- (EXUpdatesDatabase *)database
{
return EXUpdatesAppController.sharedInstance.database;
}
- (id<EXUpdatesSelectionPolicy>)selectionPolicy
{
return EXUpdatesAppController.sharedInstance.selectionPolicy;
}
- (NSURL *)directory
{
return EXUpdatesAppController.sharedInstance.updatesDirectory;
}
- (nullable EXUpdatesUpdate *)launchedUpdate
{
return EXUpdatesAppController.sharedInstance.launchedUpdate;
}
- (nullable NSDictionary *)assetFilesMap
{
return EXUpdatesAppController.sharedInstance.assetFilesMap;
}
- (BOOL)isUsingEmbeddedAssets
{
return EXUpdatesAppController.sharedInstance.isUsingEmbeddedAssets;
}
- (BOOL)isStarted
{
return EXUpdatesAppController.sharedInstance.isStarted;
}
- (BOOL)isEmergencyLaunch
{
return EXUpdatesAppController.sharedInstance.isEmergencyLaunch;
}
- (BOOL)canRelaunch
{
return EXUpdatesAppController.sharedInstance.isStarted;
}
- (void)requestRelaunchWithCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion
{
return [EXUpdatesAppController.sharedInstance requestRelaunchWithCompletion:completion];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,20 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <React/RCTBridge.h>
#import <EXUpdates/EXUpdatesConfig.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesUtils : NSObject
+ (void)runBlockOnMainThread:(void (^)(void))block;
+ (NSString *)sha256WithData:(NSData *)data;
+ (nullable NSURL *)initializeUpdatesDirectoryWithError:(NSError ** _Nullable)error;
+ (void)sendEventToBridge:(nullable RCTBridge *)bridge withType:(NSString *)eventType body:(NSDictionary *)body;
+ (BOOL)shouldCheckForUpdateWithConfig:(EXUpdatesConfig *)config;
+ (NSString *)getRuntimeVersionWithConfig:(EXUpdatesConfig *)config;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,106 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <CommonCrypto/CommonDigest.h>
#import <EXUpdates/EXUpdatesUtils.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <arpa/inet.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesEventName = @"Expo.nativeUpdatesEvent";
static NSString * const EXUpdatesUtilsErrorDomain = @"EXUpdatesUtils";
@implementation EXUpdatesUtils
+ (void)runBlockOnMainThread:(void (^)(void))block
{
if ([NSThread isMainThread]) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
}
+ (NSString *)sha256WithData:(NSData *)data
{
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++)
{
[output appendFormat:@"%02x", digest[i]];
}
return output;
}
+ (nullable NSURL *)initializeUpdatesDirectoryWithError:(NSError ** _Nullable)error
{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSURL *applicationDocumentsDirectory = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *updatesDirectory = [applicationDocumentsDirectory URLByAppendingPathComponent:@".expo-internal"];
NSString *updatesDirectoryPath = [updatesDirectory path];
BOOL isDir;
BOOL exists = [fileManager fileExistsAtPath:updatesDirectoryPath isDirectory:&isDir];
if (exists) {
if (!isDir) {
*error = [NSError errorWithDomain:EXUpdatesUtilsErrorDomain code:1005 userInfo:@{NSLocalizedDescriptionKey: @"Failed to create the Updates Directory; a file already exists with the required directory name"}];
return nil;
}
} else {
NSError *err;
BOOL wasCreated = [fileManager createDirectoryAtPath:updatesDirectoryPath withIntermediateDirectories:YES attributes:nil error:&err];
if (!wasCreated) {
*error = err;
return nil;
}
}
return updatesDirectory;
}
+ (void)sendEventToBridge:(nullable RCTBridge *)bridge withType:(NSString *)eventType body:(NSDictionary *)body
{
if (bridge) {
NSMutableDictionary *mutableBody = [body mutableCopy];
mutableBody[@"type"] = eventType;
[bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:@[EXUpdatesEventName, mutableBody]];
} else {
NSLog(@"EXUpdates: Could not emit %@ event. Did you set the bridge property on the controller singleton?", eventType);
}
}
+ (BOOL)shouldCheckForUpdateWithConfig:(EXUpdatesConfig *)config
{
switch (config.checkOnLaunch) {
case EXUpdatesCheckAutomaticallyConfigNever:
return NO;
case EXUpdatesCheckAutomaticallyConfigWifiOnly: {
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *) &zeroAddress);
SCNetworkReachabilityFlags flags;
SCNetworkReachabilityGetFlags(reachability, &flags);
return (flags & kSCNetworkReachabilityFlagsIsWWAN) == 0;
}
case EXUpdatesCheckAutomaticallyConfigAlways:
default:
return YES;
}
}
+ (NSString *)getRuntimeVersionWithConfig:(EXUpdatesConfig *)config
{
return config.runtimeVersion ?: config.sdkVersion;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,15 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesBareUpdate : NSObject
+ (EXUpdatesUpdate *)updateWithBareManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,84 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesBareUpdate.h>
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
#import <EXUpdates/EXUpdatesUpdate+Private.h>
#import <EXUpdates/EXUpdatesUtils.h>
NS_ASSUME_NONNULL_BEGIN
@implementation EXUpdatesBareUpdate
+ (EXUpdatesUpdate *)updateWithBareManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
{
EXUpdatesUpdate *update = [[EXUpdatesUpdate alloc] initWithRawManifest:manifest
config:config
database:database];
id updateId = manifest[@"id"];
id commitTime = manifest[@"commitTime"];
id metadata = manifest[@"metadata"];
id assets = manifest[@"assets"];
NSAssert([updateId isKindOfClass:[NSString class]], @"update ID should be a string");
NSAssert([commitTime isKindOfClass:[NSNumber class]], @"commitTime should be a number");
NSAssert(!metadata || [metadata isKindOfClass:[NSDictionary class]], @"metadata should be null or an object");
NSAssert(assets && [assets isKindOfClass:[NSArray class]], @"assets should be a nonnull array");
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:(NSString *)updateId];
NSAssert(uuid, @"update ID should be a valid UUID");
NSMutableArray<EXUpdatesAsset *> *processedAssets = [NSMutableArray new];
NSString *bundleKey = [NSString stringWithFormat:@"bundle-%@", commitTime];
EXUpdatesAsset *jsBundleAsset = [[EXUpdatesAsset alloc] initWithKey:bundleKey type:EXUpdatesBareEmbeddedBundleFileType];
jsBundleAsset.isLaunchAsset = YES;
jsBundleAsset.mainBundleFilename = EXUpdatesBareEmbeddedBundleFilename;
[processedAssets addObject:jsBundleAsset];
for (NSDictionary *assetDict in (NSArray *)assets) {
NSAssert([assetDict isKindOfClass:[NSDictionary class]], @"assets must be objects");
id packagerHash = assetDict[@"packagerHash"];
id type = assetDict[@"type"];
id mainBundleDir = assetDict[@"nsBundleDir"];
id mainBundleFilename = assetDict[@"nsBundleFilename"];
NSAssert(packagerHash && [packagerHash isKindOfClass:[NSString class]], @"asset key should be a nonnull string");
NSAssert(type && [type isKindOfClass:[NSString class]], @"asset type should be a nonnull string");
NSAssert(mainBundleFilename && [mainBundleFilename isKindOfClass:[NSString class]], @"asset nsBundleFilename should be a nonnull string");
if (mainBundleDir) {
NSAssert([mainBundleDir isKindOfClass:[NSString class]], @"asset nsBundleDir should be a string");
}
NSString *key = [NSString stringWithFormat:@"%@.%@", packagerHash, type];
EXUpdatesAsset *asset = [[EXUpdatesAsset alloc] initWithKey:key type:(NSString *)type];
asset.mainBundleDir = mainBundleDir;
asset.mainBundleFilename = mainBundleFilename;
[processedAssets addObject:asset];
}
update.updateId = uuid;
update.commitTime = [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)commitTime doubleValue] / 1000];
update.runtimeVersion = [EXUpdatesUtils getRuntimeVersionWithConfig:config];
if (metadata) {
update.metadata = (NSDictionary *)metadata;
}
update.status = EXUpdatesUpdateStatusEmbedded;
update.keep = YES;
update.assets = processedAssets;
if ([update.runtimeVersion containsString:@","]) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Should not be initializing EXUpdatesBareUpdate in an environment with multiple runtime versions."
userInfo:@{}];
}
return update;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,17 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesLegacyUpdate : NSObject
+ (EXUpdatesUpdate *)updateWithLegacyManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database;
+ (NSURL *)bundledAssetBaseUrlWithManifest:(NSDictionary *)manifest config:(EXUpdatesConfig *)config;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,158 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
#import <EXUpdates/EXUpdatesLegacyUpdate.h>
#import <EXUpdates/EXUpdatesUpdate+Private.h>
#import <EXUpdates/EXUpdatesUtils.h>
#import <React/RCTConvert.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesExpoAssetBaseUrl = @"https://d1wp6m56sqw74a.cloudfront.net/~assets/";
static NSString * const EXUpdatesExpoIoDomain = @"expo.io";
static NSString * const EXUpdatesExpHostDomain = @"exp.host";
static NSString * const EXUpdatesExpoTestDomain = @"expo.test";
@implementation EXUpdatesLegacyUpdate
+ (EXUpdatesUpdate *)updateWithLegacyManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
{
EXUpdatesUpdate *update = [[EXUpdatesUpdate alloc] initWithRawManifest:manifest
config:config
database:database];
if ([[self class] areDevToolsEnabledWithManifest:manifest]) {
// XDL does not set a releaseId or commitTime for development manifests.
// we do not need these so we just stub them out
update.updateId = [NSUUID UUID];
update.commitTime = [NSDate date];
} else {
id updateId = manifest[@"releaseId"];
NSAssert([updateId isKindOfClass:[NSString class]], @"update ID should be a string");
update.updateId = [[NSUUID alloc] initWithUUIDString:(NSString *)updateId];
NSAssert(update.updateId, @"update ID should be a valid UUID");
id commitTimeString = manifest[@"commitTime"];
NSAssert([commitTimeString isKindOfClass:[NSString class]], @"commitTime should be a string");
update.commitTime = [RCTConvert NSDate:commitTimeString];
}
if ([[self class] isDevelopmentModeManifest:manifest]) {
update.isDevelopmentMode = YES;
update.status = EXUpdatesUpdateStatusDevelopment;
} else {
update.status = EXUpdatesUpdateStatusPending;
}
id bundleUrlString = manifest[@"bundleUrl"];
id assets = manifest[@"bundledAssets"] ?: @[];
id sdkVersion = manifest[@"sdkVersion"];
id runtimeVersion = manifest[@"runtimeVersion"];
if (runtimeVersion && [runtimeVersion isKindOfClass:[NSDictionary class]]) {
id runtimeVersionIos = ((NSDictionary *)runtimeVersion)[@"ios"];
NSAssert([runtimeVersionIos isKindOfClass:[NSString class]], @"runtimeVersion['ios'] should be a string");
update.runtimeVersion = (NSString *)runtimeVersionIos;
} else if (runtimeVersion && [runtimeVersion isKindOfClass:[NSString class]]) {
update.runtimeVersion = (NSString *)runtimeVersion;
} else {
NSAssert([sdkVersion isKindOfClass:[NSString class]], @"sdkVersion should be a string");
update.runtimeVersion = (NSString *)sdkVersion;
}
NSAssert([bundleUrlString isKindOfClass:[NSString class]], @"bundleUrl should be a string");
NSAssert([assets isKindOfClass:[NSArray class]], @"assets should be a nonnull array");
NSURL *bundleUrl = [NSURL URLWithString:bundleUrlString];
NSAssert(bundleUrl, @"bundleUrl should be a valid URL");
NSMutableArray<EXUpdatesAsset *> *processedAssets = [NSMutableArray new];
NSString *bundleKey = [NSString stringWithFormat:@"bundle-%@", [EXUpdatesUtils sha256WithData:[(NSString *)bundleUrlString dataUsingEncoding:NSUTF8StringEncoding]]];
EXUpdatesAsset *jsBundleAsset = [[EXUpdatesAsset alloc] initWithKey:bundleKey type:EXUpdatesEmbeddedBundleFileType];
jsBundleAsset.url = bundleUrl;
jsBundleAsset.isLaunchAsset = YES;
jsBundleAsset.mainBundleFilename = EXUpdatesEmbeddedBundleFilename;
[processedAssets addObject:jsBundleAsset];
NSURL *bundledAssetBaseUrl = [[self class] bundledAssetBaseUrlWithManifest:manifest config:config];
for (NSString *bundledAsset in (NSArray *)assets) {
NSAssert([bundledAsset isKindOfClass:[NSString class]], @"bundledAssets must be an array of strings");
NSRange extensionStartRange = [bundledAsset rangeOfString:@"." options:NSBackwardsSearch];
NSUInteger prefixLength = [@"asset_" length];
NSString *filename;
NSString *hash;
NSString *type;
if (extensionStartRange.location == NSNotFound) {
filename = bundledAsset;
hash = [bundledAsset substringFromIndex:prefixLength];
type = @"";
} else {
filename = [bundledAsset substringToIndex:extensionStartRange.location];
NSRange hashRange = NSMakeRange(prefixLength, extensionStartRange.location - prefixLength);
hash = [bundledAsset substringWithRange:hashRange];
type = [bundledAsset substringFromIndex:extensionStartRange.location + 1];
}
NSURL *url = [bundledAssetBaseUrl URLByAppendingPathComponent:hash];
NSString *key = [NSString stringWithFormat:@"%@.%@", hash, type];
EXUpdatesAsset *asset = [[EXUpdatesAsset alloc] initWithKey:key type:(NSString *)type];
asset.url = url;
asset.mainBundleFilename = filename;
[processedAssets addObject:asset];
}
update.metadata = manifest;
update.keep = YES;
update.bundleUrl = bundleUrl;
update.assets = processedAssets;
return update;
}
+ (NSURL *)bundledAssetBaseUrlWithManifest:(NSDictionary *)manifest config:(EXUpdatesConfig *)config
{
NSURL *manifestUrl = config.updateUrl;
NSString *host = manifestUrl.host;
if (!host ||
[host containsString:EXUpdatesExpoIoDomain] ||
[host containsString:EXUpdatesExpHostDomain] ||
[host containsString:EXUpdatesExpoTestDomain]) {
return [NSURL URLWithString:EXUpdatesExpoAssetBaseUrl];
} else {
NSString *assetsPathOrUrl = manifest[@"assetUrlOverride"] ?: @"assets";
// assetUrlOverride may be an absolute or relative URL
// if relative, we should resolve with respect to the manifest URL
NSURL *maybeAssetsUrl = [NSURL URLWithString:assetsPathOrUrl];
if (maybeAssetsUrl && maybeAssetsUrl.scheme) {
return maybeAssetsUrl;
} else if (maybeAssetsUrl && maybeAssetsUrl.standardizedURL) {
return [manifestUrl.URLByDeletingLastPathComponent URLByAppendingPathComponent:maybeAssetsUrl.standardizedURL.relativeString];
} else {
return [manifestUrl.URLByDeletingLastPathComponent URLByAppendingPathComponent:assetsPathOrUrl];
}
}
}
+ (BOOL)isDevelopmentModeManifest:(NSDictionary *)manifest
{
NSDictionary *manifestPackagerOptsConfig = manifest[@"packagerOpts"];
return (manifest[@"developer"] != nil && manifestPackagerOptsConfig != nil && [@(YES) isEqualToNumber:manifestPackagerOptsConfig[@"dev"]]);
}
+ (BOOL)areDevToolsEnabledWithManifest:(NSDictionary *)manifest
{
NSDictionary *manifestDeveloperConfig = manifest[@"developer"];
BOOL isDeployedFromTool = (manifestDeveloperConfig && manifestDeveloperConfig[@"tool"] != nil);
return (isDeployedFromTool);
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,15 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesNewUpdate : NSObject
+ (EXUpdatesUpdate *)updateWithNewManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,93 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesEmbeddedAppLoader.h>
#import <EXUpdates/EXUpdatesNewUpdate.h>
#import <EXUpdates/EXUpdatesUpdate+Private.h>
#import <EXUpdates/EXUpdatesUtils.h>
NS_ASSUME_NONNULL_BEGIN
@implementation EXUpdatesNewUpdate
+ (EXUpdatesUpdate *)updateWithNewManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
{
EXUpdatesUpdate *update = [[EXUpdatesUpdate alloc] initWithRawManifest:manifest
config:config
database:database];
id updateId = manifest[@"id"];
id commitTime = manifest[@"commitTime"];
id runtimeVersion = manifest[@"runtimeVersion"];
id metadata = manifest[@"metadata"];
id bundleUrlString = manifest[@"bundleUrl"];
id assets = manifest[@"assets"];
NSAssert([updateId isKindOfClass:[NSString class]], @"update ID should be a string");
NSAssert([commitTime isKindOfClass:[NSNumber class]], @"commitTime should be a number");
NSAssert([runtimeVersion isKindOfClass:[NSString class]], @"runtimeVersion should be a string");
NSAssert(!metadata || [metadata isKindOfClass:[NSDictionary class]], @"metadata should be null or an object");
NSAssert([bundleUrlString isKindOfClass:[NSString class]], @"bundleUrl should be a string");
NSAssert(assets && [assets isKindOfClass:[NSArray class]], @"assets should be a nonnull array");
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:(NSString *)updateId];
NSAssert(uuid, @"update ID should be a valid UUID");
NSURL *bundleUrl = [NSURL URLWithString:bundleUrlString];
NSAssert(bundleUrl, @"bundleUrl should be a valid URL");
NSMutableArray<EXUpdatesAsset *> *processedAssets = [NSMutableArray new];
NSString *bundleKey = [NSString stringWithFormat:@"bundle-%@", commitTime];
EXUpdatesAsset *jsBundleAsset = [[EXUpdatesAsset alloc] initWithKey:bundleKey type:EXUpdatesEmbeddedBundleFileType];
jsBundleAsset.url = bundleUrl;
jsBundleAsset.isLaunchAsset = YES;
jsBundleAsset.mainBundleFilename = EXUpdatesEmbeddedBundleFilename;
[processedAssets addObject:jsBundleAsset];
for (NSDictionary *assetDict in (NSArray *)assets) {
NSAssert([assetDict isKindOfClass:[NSDictionary class]], @"assets must be objects");
id key = assetDict[@"key"];
id urlString = assetDict[@"url"];
id type = assetDict[@"type"];
id metadata = assetDict[@"metadata"];
id mainBundleFilename = assetDict[@"mainBundleFilename"];
NSAssert(key && [key isKindOfClass:[NSString class]], @"asset key should be a nonnull string");
NSAssert(urlString && [urlString isKindOfClass:[NSString class]], @"asset url should be a nonnull string");
NSAssert(type && [type isKindOfClass:[NSString class]], @"asset type should be a nonnull string");
NSURL *url = [NSURL URLWithString:(NSString *)urlString];
NSAssert(url, @"asset url should be a valid URL");
EXUpdatesAsset *asset = [[EXUpdatesAsset alloc] initWithKey:key type:(NSString *)type];
asset.url = url;
if (metadata) {
NSAssert([metadata isKindOfClass:[NSDictionary class]], @"asset metadata should be an object");
asset.metadata = (NSDictionary *)metadata;
}
if (mainBundleFilename) {
NSAssert([mainBundleFilename isKindOfClass:[NSString class]], @"asset localPath should be a string");
asset.mainBundleFilename = (NSString *)mainBundleFilename;
}
[processedAssets addObject:asset];
}
update.updateId = uuid;
update.commitTime = [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)commitTime doubleValue] / 1000];
update.runtimeVersion = (NSString *)runtimeVersion;
if (metadata) {
update.metadata = (NSDictionary *)metadata;
}
update.status = EXUpdatesUpdateStatusPending;
update.keep = YES;
update.bundleUrl = bundleUrl;
update.assets = processedAssets;
return update;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,29 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAsset.h>
#import <EXUpdates/EXUpdatesUpdate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesUpdate ()
@property (nonatomic, strong, readwrite) NSUUID *updateId;
@property (nonatomic, strong, readwrite) NSString *scopeKey;
@property (nonatomic, strong, readwrite) NSDate *commitTime;
@property (nonatomic, strong, readwrite) NSString *runtimeVersion;
@property (nonatomic, strong, readwrite, nullable) NSDictionary *metadata;
@property (nonatomic, assign, readwrite) BOOL keep;
@property (nonatomic, strong, readwrite) NSURL *bundleUrl;
@property (nonatomic, strong, readwrite) NSArray<EXUpdatesAsset *> *assets;
@property (nonatomic, assign, readwrite) BOOL isDevelopmentMode;
@property (nonatomic, strong) EXUpdatesConfig *config;
@property (nonatomic, strong, nullable) EXUpdatesDatabase *database;
- (instancetype)initWithRawManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(nullable EXUpdatesDatabase *)database;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,55 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesAsset.h>
#import <EXUpdates/EXUpdatesConfig.h>
@class EXUpdatesDatabase;
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, EXUpdatesUpdateStatus) {
EXUpdatesUpdateStatusFailed = 0,
EXUpdatesUpdateStatusReady = 1,
EXUpdatesUpdateStatusLaunchable = 2,
EXUpdatesUpdateStatusPending = 3,
EXUpdatesUpdateStatusUnused = 4,
EXUpdatesUpdateStatusEmbedded = 5,
EXUpdatesUpdateStatusDevelopment = 6
};
@interface EXUpdatesUpdate : NSObject
@property (nonatomic, strong, readonly) NSUUID *updateId;
@property (nonatomic, strong, readonly) NSString *scopeKey;
@property (nonatomic, strong, readonly) NSDate *commitTime;
@property (nonatomic, strong, readonly) NSString *runtimeVersion;
@property (nonatomic, strong, readonly, nullable) NSDictionary * metadata;
@property (nonatomic, assign, readonly) BOOL keep;
@property (nonatomic, strong, readonly) NSArray<EXUpdatesAsset *> *assets;
@property (nonatomic, assign, readonly) BOOL isDevelopmentMode;
@property (nonatomic, strong, readonly) NSDictionary *rawManifest;
@property (nonatomic, assign) EXUpdatesUpdateStatus status;
+ (instancetype)updateWithId:(NSUUID *)updateId
scopeKey:(NSString *)scopeKey
commitTime:(NSDate *)commitTime
runtimeVersion:(NSString *)runtimeVersion
metadata:(nullable NSDictionary *)metadata
status:(EXUpdatesUpdateStatus)status
keep:(BOOL)keep
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database;
+ (instancetype)updateWithManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database;
+ (instancetype)updateWithEmbeddedManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(nullable EXUpdatesDatabase *)database;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,113 @@
// Copyright © 2019 650 Industries. All rights reserved.
#import <EXUpdates/EXUpdatesBareUpdate.h>
#import <EXUpdates/EXUpdatesDatabase.h>
#import <EXUpdates/EXUpdatesLegacyUpdate.h>
#import <EXUpdates/EXUpdatesNewUpdate.h>
#import <EXUpdates/EXUpdatesUpdate+Private.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXUpdatesUpdate ()
@property (nonatomic, strong, readwrite) NSDictionary *rawManifest;
@end
@implementation EXUpdatesUpdate
- (instancetype)initWithRawManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(nullable EXUpdatesDatabase *)database
{
if (self = [super init]) {
_rawManifest = manifest;
_config = config;
_database = database;
_scopeKey = config.scopeKey;
_isDevelopmentMode = NO;
}
return self;
}
+ (instancetype)updateWithId:(NSUUID *)updateId
scopeKey:(NSString *)scopeKey
commitTime:(NSDate *)commitTime
runtimeVersion:(NSString *)runtimeVersion
metadata:(nullable NSDictionary *)metadata
status:(EXUpdatesUpdateStatus)status
keep:(BOOL)keep
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
{
// for now, we store the entire managed manifest in the metadata field
EXUpdatesUpdate *update = [[self alloc] initWithRawManifest:metadata ?: @{}
config:config
database:database];
update.updateId = updateId;
update.scopeKey = scopeKey;
update.commitTime = commitTime;
update.runtimeVersion = runtimeVersion;
update.metadata = metadata;
update.status = status;
update.keep = keep;
return update;
}
+ (instancetype)updateWithManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(EXUpdatesDatabase *)database
{
if (config.usesLegacyManifest) {
return [EXUpdatesLegacyUpdate updateWithLegacyManifest:manifest
config:config
database:database];
} else {
return [EXUpdatesNewUpdate updateWithNewManifest:manifest
config:config
database:database];
}
}
+ (instancetype)updateWithEmbeddedManifest:(NSDictionary *)manifest
config:(EXUpdatesConfig *)config
database:(nullable EXUpdatesDatabase *)database
{
if (config.usesLegacyManifest) {
if (manifest[@"releaseId"]) {
return [EXUpdatesLegacyUpdate updateWithLegacyManifest:manifest
config:config
database:database];
} else {
return [EXUpdatesBareUpdate updateWithBareManifest:manifest
config:config
database:database];
}
} else {
if (manifest[@"runtimeVersion"]) {
return [EXUpdatesNewUpdate updateWithNewManifest:manifest
config:config
database:database];
} else {
return [EXUpdatesBareUpdate updateWithBareManifest:manifest
config:config
database:database];
}
}
}
- (NSArray<EXUpdatesAsset *> *)assets
{
if (!_assets && _database) {
dispatch_sync(_database.databaseQueue, ^{
NSError *error;
self->_assets = [self->_database assetsWithUpdateId:self->_updateId error:&error];
NSAssert(self->_assets, @"Assets should be nonnull when selected from DB: %@", error.localizedDescription);
});
}
return _assets;
}
@end
NS_ASSUME_NONNULL_END