1// Copyright © 2019 650 Industries. All rights reserved. 2 3#import <ABI48_0_0EXUpdates/ABI48_0_0EXUpdatesAppLauncherWithDatabase+Tests.h> 4#import <ABI48_0_0EXUpdates/ABI48_0_0EXUpdatesEmbeddedAppLoader.h> 5#import <ABI48_0_0EXUpdates/ABI48_0_0EXUpdatesDatabase.h> 6#import <ABI48_0_0EXUpdates/ABI48_0_0EXUpdatesFileDownloader.h> 7#import <ABI48_0_0EXUpdates/ABI48_0_0EXUpdatesUtils.h> 8 9NS_ASSUME_NONNULL_BEGIN 10 11@interface ABI48_0_0EXUpdatesAppLauncherWithDatabase () 12 13@property (nullable, nonatomic, strong, readwrite) ABI48_0_0EXUpdatesUpdate *launchedUpdate; 14@property (nullable, nonatomic, strong, readwrite) NSURL *launchAssetUrl; 15@property (nullable, nonatomic, strong, readwrite) NSMutableDictionary *assetFilesMap; 16 17@property (nonatomic, strong) ABI48_0_0EXUpdatesConfig *config; 18@property (nonatomic, strong) ABI48_0_0EXUpdatesDatabase *database; 19@property (nonatomic, strong) NSURL *directory; 20@property (nonatomic, strong) ABI48_0_0EXUpdatesFileDownloader *downloader; 21@property (nonatomic, copy) ABI48_0_0EXUpdatesAppLauncherCompletionBlock completion; 22@property (nonatomic, strong) dispatch_queue_t completionQueue; 23 24@property (nonatomic, strong) dispatch_queue_t launcherQueue; 25@property (nonatomic, assign) NSUInteger completedAssets; 26 27@property (nonatomic, strong) NSError *launchAssetError; 28 29@end 30 31static NSString * const ABI48_0_0EXUpdatesAppLauncherErrorDomain = @"AppLauncher"; 32 33/** 34 * Implementation of ABI48_0_0EXUpdatesAppLauncher that uses the SQLite database and expo-updates file store 35 * as the source of updates. 36 * 37 * Uses the ABI48_0_0EXUpdatesSelectionPolicy to choose an update from SQLite to launch, then ensures that 38 * the update is safe and ready to launch (i.e. all the assets that SQLite expects to be stored on 39 * disk are actually there). 40 * 41 * This class also includes failsafe code to attempt to re-download any assets unexpectedly missing 42 * from disk (since it isn't necessarily safe to just revert to an older update in this case). 43 * Distinct from the ABI48_0_0EXUpdatesAppLoader classes, though, this class does *not* make any major 44 * modifications to the database; its role is mostly to read the database and ensure integrity with 45 * the file system. 46 * 47 * It's important that the update to launch is selected *before* any other checks, e.g. the above 48 * check for assets on disk. This is to preserve the invariant that no older update should ever be 49 * launched after a newer one has been launched. 50 */ 51@implementation ABI48_0_0EXUpdatesAppLauncherWithDatabase 52 53- (instancetype)initWithConfig:(ABI48_0_0EXUpdatesConfig *)config 54 database:(ABI48_0_0EXUpdatesDatabase *)database 55 directory:(NSURL *)directory 56 completionQueue:(dispatch_queue_t)completionQueue 57{ 58 if (self = [super init]) { 59 _launcherQueue = dispatch_queue_create("expo.launcher.LauncherQueue", DISPATCH_QUEUE_SERIAL); 60 _completedAssets = 0; 61 _config = config; 62 _database = database; 63 _directory = directory; 64 _completionQueue = completionQueue; 65 } 66 return self; 67} 68 69+ (void)launchableUpdateWithConfig:(ABI48_0_0EXUpdatesConfig *)config 70 database:(ABI48_0_0EXUpdatesDatabase *)database 71 selectionPolicy:(ABI48_0_0EXUpdatesSelectionPolicy *)selectionPolicy 72 completion:(ABI48_0_0EXUpdatesAppLauncherUpdateCompletionBlock)completion 73 completionQueue:(dispatch_queue_t)completionQueue 74{ 75 dispatch_async(database.databaseQueue, ^{ 76 NSError *error; 77 NSArray<ABI48_0_0EXUpdatesUpdate *> *launchableUpdates = [database launchableUpdatesWithConfig:config error:&error]; 78 NSError *manifestFiltersError; 79 NSDictionary *manifestFilters = [database manifestFiltersWithScopeKey:config.scopeKey error:&manifestFiltersError]; 80 dispatch_async(completionQueue, ^{ 81 if (!launchableUpdates) { 82 completion(error, nil); 83 return; 84 } 85 if (manifestFiltersError) { 86 completion(manifestFiltersError, nil); 87 return; 88 } 89 90 // We can only run an update marked as embedded if it's actually the update embedded in the 91 // current binary. We might have an older update from a previous binary still listed in the 92 // database with Embedded status so we need to filter that out here. 93 ABI48_0_0EXUpdatesUpdate *embeddedManifest = [ABI48_0_0EXUpdatesEmbeddedAppLoader embeddedManifestWithConfig:config database:database]; 94 NSMutableArray<ABI48_0_0EXUpdatesUpdate *>*filteredLaunchableUpdates = [NSMutableArray new]; 95 for (ABI48_0_0EXUpdatesUpdate *update in launchableUpdates) { 96 if (update.status == ABI48_0_0EXUpdatesUpdateStatusEmbedded) { 97 if (embeddedManifest && ![update.updateId isEqual:embeddedManifest.updateId]) { 98 continue; 99 } 100 } 101 [filteredLaunchableUpdates addObject:update]; 102 } 103 104 completion(nil, [selectionPolicy launchableUpdateFromUpdates:filteredLaunchableUpdates filters:manifestFilters]); 105 }); 106 }); 107} 108 109- (void)launchUpdateWithSelectionPolicy:(ABI48_0_0EXUpdatesSelectionPolicy *)selectionPolicy 110 completion:(ABI48_0_0EXUpdatesAppLauncherCompletionBlock)completion 111{ 112 NSAssert(!_completion, @"ABI48_0_0EXUpdatesAppLauncher:launchUpdateWithSelectionPolicy:successBlock should not be called twice on the same instance"); 113 _completion = completion; 114 115 if (!_launchedUpdate) { 116 [[self class] launchableUpdateWithConfig:_config database:_database selectionPolicy:selectionPolicy completion:^(NSError * _Nullable error, ABI48_0_0EXUpdatesUpdate * _Nullable launchableUpdate) { 117 if (error || !launchableUpdate) { 118 if (self->_completion) { 119 dispatch_async(self->_completionQueue, ^{ 120 NSMutableDictionary *userInfo = [NSMutableDictionary new]; 121 userInfo[NSLocalizedDescriptionKey] = @"No launchable updates found in database"; 122 if (error) { 123 userInfo[NSUnderlyingErrorKey] = error; 124 } 125 self->_completion([NSError errorWithDomain:ABI48_0_0EXUpdatesAppLauncherErrorDomain code:1011 userInfo:userInfo], NO); 126 }); 127 } 128 } else { 129 self->_launchedUpdate = launchableUpdate; 130 [self _finishLaunch]; 131 } 132 } completionQueue:_launcherQueue]; 133 } else { 134 [self _finishLaunch]; 135 } 136} 137 138+ (void)storedUpdateIdsInDatabase:(ABI48_0_0EXUpdatesDatabase *)database 139 completion:(ABI48_0_0EXUpdatesAppLauncherQueryCompletionBlock)completionBlock 140{ 141 dispatch_async(database.databaseQueue,^{ 142 NSArray<NSUUID *> *readyUpdateIds; 143 NSError *dbError = nil; 144 readyUpdateIds = [database allUpdateIdsWithStatus:ABI48_0_0EXUpdatesUpdateStatusReady error:&dbError]; 145 if (dbError != nil) { 146 completionBlock(dbError, @[]); 147 } else { 148 completionBlock(nil, readyUpdateIds); 149 } 150 }); 151} 152 153- (BOOL)isUsingEmbeddedAssets 154{ 155 return _assetFilesMap == nil; 156} 157 158- (void)_finishLaunch 159{ 160 [self _markUpdateAccessed]; 161 [self _ensureAllAssetsExist]; 162} 163 164- (void)_markUpdateAccessed 165{ 166 NSAssert(_launchedUpdate, @"launchedUpdate should be nonnull before calling markUpdateAccessed"); 167 dispatch_async(_database.databaseQueue, ^{ 168 NSError *error; 169 [self->_database markUpdateAccessed:self->_launchedUpdate error:&error]; 170 if (error) { 171 NSLog(@"Failed to mark update as recently accessed: %@", error.localizedDescription); 172 } 173 }); 174} 175 176- (void)_ensureAllAssetsExist 177{ 178 if (_launchedUpdate.status == ABI48_0_0EXUpdatesUpdateStatusEmbedded) { 179 NSAssert(_assetFilesMap == nil, @"assetFilesMap should be null for embedded updates"); 180 _launchAssetUrl = [[NSBundle mainBundle] URLForResource:ABI48_0_0EXUpdatesBareEmbeddedBundleFilename withExtension:ABI48_0_0EXUpdatesBareEmbeddedBundleFileType]; 181 182 dispatch_async(self->_completionQueue, ^{ 183 self->_completion(self->_launchAssetError, self->_launchAssetUrl != nil); 184 self->_completion = nil; 185 }); 186 return; 187 } else if (_launchedUpdate.status == ABI48_0_0EXUpdatesUpdateStatusDevelopment) { 188 dispatch_async(self->_completionQueue, ^{ 189 self->_completion(nil, YES); 190 self->_completion = nil; 191 }); 192 return; 193 } 194 195 _assetFilesMap = [NSMutableDictionary new]; 196 197 if (_launchedUpdate) { 198 NSUInteger totalAssetCount = _launchedUpdate.assets.count; 199 for (ABI48_0_0EXUpdatesAsset *asset in _launchedUpdate.assets) { 200 NSURL *assetLocalUrl = [_directory URLByAppendingPathComponent:asset.filename]; 201 [self _ensureAssetExists:asset withLocalUrl:assetLocalUrl completion:^(BOOL exists) { 202 dispatch_assert_queue(self->_launcherQueue); 203 self->_completedAssets++; 204 205 if (exists) { 206 if (asset.isLaunchAsset) { 207 self->_launchAssetUrl = assetLocalUrl; 208 } else { 209 if (asset.key) { 210 self->_assetFilesMap[asset.key] = assetLocalUrl.absoluteString; 211 } 212 } 213 } 214 215 if (self->_completedAssets == totalAssetCount) { 216 dispatch_async(self->_completionQueue, ^{ 217 self->_completion(self->_launchAssetError, self->_launchAssetUrl != nil); 218 self->_completion = nil; 219 }); 220 } 221 }]; 222 } 223 } 224} 225 226- (void)_ensureAssetExists:(ABI48_0_0EXUpdatesAsset *)asset withLocalUrl:(NSURL *)assetLocalUrl completion:(void (^)(BOOL exists))completion 227{ 228 [self _checkExistenceOfAsset:asset withLocalUrl:assetLocalUrl completion:^(BOOL exists) { 229 if (exists) { 230 completion(YES); 231 return; 232 } 233 234 [self _maybeCopyAssetFromMainBundle:asset withLocalUrl:assetLocalUrl completion:^(BOOL success, NSError * _Nullable error) { 235 if (success) { 236 completion(YES); 237 return; 238 } 239 240 if (error) { 241 NSLog(@"Error copying embedded asset %@: %@", asset.key, error.localizedDescription); 242 } 243 244 [self _downloadAsset:asset withLocalUrl:assetLocalUrl completion:^(NSError * _Nullable error, ABI48_0_0EXUpdatesAsset *asset, NSURL *assetLocalUrl) { 245 if (error) { 246 if (asset.isLaunchAsset) { 247 // save the error -- since this is the launch asset, the launcher will fail 248 // so we want to propagate this error 249 self->_launchAssetError = error; 250 } 251 NSLog(@"Failed to load missing asset %@: %@", asset.key, error.localizedDescription); 252 completion(NO); 253 } else { 254 // attempt to update the database record to match the newly downloaded asset 255 // but don't block launching on this 256 dispatch_async(self->_database.databaseQueue, ^{ 257 NSError *error; 258 [self->_database updateAsset:asset error:&error]; 259 if (error) { 260 NSLog(@"Could not write data for downloaded asset to database: %@", error.localizedDescription); 261 } 262 }); 263 264 completion(YES); 265 } 266 }]; 267 }]; 268 }]; 269} 270 271- (void)_checkExistenceOfAsset:(ABI48_0_0EXUpdatesAsset *)asset withLocalUrl:(NSURL *)assetLocalUrl completion:(void (^)(BOOL exists))completion 272{ 273 dispatch_async([ABI48_0_0EXUpdatesFileDownloader assetFilesQueue], ^{ 274 BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:[assetLocalUrl path]]; 275 dispatch_async(self->_launcherQueue, ^{ 276 completion(exists); 277 }); 278 }); 279} 280 281- (void)_maybeCopyAssetFromMainBundle:(ABI48_0_0EXUpdatesAsset *)asset 282 withLocalUrl:(NSURL *)assetLocalUrl 283 completion:(void (^)(BOOL success, NSError * _Nullable error))completion 284{ 285 ABI48_0_0EXUpdatesUpdate *embeddedManifest = [ABI48_0_0EXUpdatesEmbeddedAppLoader embeddedManifestWithConfig:_config database:_database]; 286 if (embeddedManifest) { 287 ABI48_0_0EXUpdatesAsset *matchingAsset; 288 for (ABI48_0_0EXUpdatesAsset *embeddedAsset in embeddedManifest.assets) { 289 if (embeddedAsset.key && [embeddedAsset.key isEqualToString:asset.key]) { 290 matchingAsset = embeddedAsset; 291 break; 292 } 293 } 294 295 if (matchingAsset && matchingAsset.mainBundleFilename) { 296 dispatch_async([ABI48_0_0EXUpdatesFileDownloader assetFilesQueue], ^{ 297 NSString *bundlePath = [ABI48_0_0EXUpdatesUtils pathForBundledAsset:matchingAsset]; 298 if (bundlePath == nil) { 299 dispatch_async(self->_launcherQueue, ^{ 300 completion(NO, [NSError errorWithDomain:ABI48_0_0EXUpdatesAppLauncherErrorDomain code:1013 userInfo:@{NSLocalizedDescriptionKey: @"Asset bundlePath was unexpectedly nil"}]); 301 }); 302 return; 303 } 304 NSError *error; 305 BOOL success = [NSFileManager.defaultManager copyItemAtPath:bundlePath toPath:[assetLocalUrl path] error:&error]; 306 dispatch_async(self->_launcherQueue, ^{ 307 completion(success, error); 308 }); 309 }); 310 return; 311 } 312 } 313 314 completion(NO, nil); 315} 316 317- (void)_downloadAsset:(ABI48_0_0EXUpdatesAsset *)asset 318 withLocalUrl:(NSURL *)assetLocalUrl 319 completion:(void (^)(NSError * _Nullable error, ABI48_0_0EXUpdatesAsset *asset, NSURL *assetLocalUrl))completion 320{ 321 if (!asset.url) { 322 completion([NSError errorWithDomain:ABI48_0_0EXUpdatesAppLauncherErrorDomain code:1007 userInfo:@{NSLocalizedDescriptionKey: @"Failed to download asset with no URL provided"}], asset, assetLocalUrl); 323 } 324 dispatch_async([ABI48_0_0EXUpdatesFileDownloader assetFilesQueue], ^{ 325 [self.downloader downloadFileFromURL:asset.url 326 verifyingHash:asset.expectedHash 327 toPath:[assetLocalUrl path] 328 extraHeaders:asset.extraRequestHeaders ?: @{} 329 successBlock:^(NSData *data, NSURLResponse *response, NSString *base64URLEncodedSHA256Hash) { 330 dispatch_async(self->_launcherQueue, ^{ 331 if ([response isKindOfClass:[NSHTTPURLResponse class]]) { 332 asset.headers = ((NSHTTPURLResponse *)response).allHeaderFields; 333 } 334 asset.contentHash = base64URLEncodedSHA256Hash; 335 asset.downloadTime = [NSDate date]; 336 completion(nil, asset, assetLocalUrl); 337 }); 338 } 339 errorBlock:^(NSError *error) { 340 dispatch_async(self->_launcherQueue, ^{ 341 completion(error, asset, assetLocalUrl); 342 }); 343 }]; 344 }); 345} 346 347- (ABI48_0_0EXUpdatesFileDownloader *)downloader 348{ 349 if (!_downloader) { 350 _downloader = [[ABI48_0_0EXUpdatesFileDownloader alloc] initWithUpdatesConfig:_config]; 351 } 352 return _downloader; 353} 354 355@end 356 357NS_ASSUME_NONNULL_END 358