This repository has been archived on 2022-03-12. You can view files and clone it, but cannot push or open issues or pull requests.
2021-04-02 02:24:13 +03:00

300 lines
11 KiB
Objective-C

// 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