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