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