1// Copyright 2020-present 650 Industries. All rights reserved.
2
3#import "EXAppFetcher.h"
4#import "EXDevelopmentHomeLoader.h"
5#import "EXClientReleaseType.h"
6#import "EXEnvironment.h"
7#import "EXErrorRecoveryManager.h"
8#import "EXFileDownloader.h"
9#import "EXKernel.h"
10#import "EXKernelLinkingManager.h"
11#import "EXManifestResource.h"
12#import "EXSession.h"
13#import "EXUpdatesDatabaseManager.h"
14#import "EXVersions.h"
15#import "EXBuildConstants.h"
16
17#if defined(EX_DETACHED)
18#import "ExpoKit-Swift.h"
19#else
20#import "Expo_Go-Swift.h"
21#endif // defined(EX_DETACHED)
22
23#import <React/RCTUtils.h>
24#import <sys/utsname.h>
25
26@import EXManifests;
27@import EXUpdates;
28
29NS_ASSUME_NONNULL_BEGIN
30
31@interface EXDevelopmentHomeLoader ()
32
33@property (nonatomic, strong, nullable) EXManifestAndAssetRequestHeaders *manifestAndAssetRequestHeaders;
34
35@property (nonatomic, strong, nullable) EXManifestsManifest *confirmedManifest;
36@property (nonatomic, strong, nullable) EXManifestsManifest *optimisticManifest;
37@property (nonatomic, strong, nullable) NSData *bundle;
38@property (nonatomic, assign) BOOL isUpToDate;
39
40/**
41 * Stateful variable to let us prevent multiple simultaneous fetches from the development server.
42 * This can happen when reloading a bundle with remote debugging enabled;
43 * RN requests the bundle multiple times for some reason.
44 */
45@property (nonatomic, assign) BOOL isLoadingDevelopmentJavaScriptResource;
46
47@property (nonatomic, strong, nullable) NSError *error;
48
49@property (nonatomic, strong) dispatch_queue_t appLoaderQueue;
50
51@end
52
53@implementation EXDevelopmentHomeLoader
54
55@synthesize bundle = _bundle;
56@synthesize isUpToDate = _isUpToDate;
57
58- (instancetype)init {
59  if (self = [super init]) {
60    _manifestAndAssetRequestHeaders = [EXDevelopmentHomeLoader bundledDevelopmentHomeManifestAndAssetRequestHeaders];
61    _appLoaderQueue = dispatch_queue_create("host.exp.exponent.LoaderQueue", DISPATCH_QUEUE_SERIAL);
62  }
63  return self;
64}
65
66#pragma mark - getters and lifecycle
67
68- (void)_reset
69{
70  _confirmedManifest = nil;
71  _optimisticManifest = nil;
72  _bundle = nil;
73  _error = nil;
74  _isUpToDate = NO;
75  _isLoadingDevelopmentJavaScriptResource = NO;
76}
77
78- (EXAppLoaderStatus)status
79{
80  if (_error) {
81    return kEXAppLoaderStatusError;
82  } else if (_bundle) {
83    return kEXAppLoaderStatusHasManifestAndBundle;
84  } else if (_optimisticManifest) {
85    return kEXAppLoaderStatusHasManifest;
86  }
87  return kEXAppLoaderStatusNew;
88}
89
90- (nullable EXManifestsManifest *)manifest
91{
92  if (_confirmedManifest) {
93    return _confirmedManifest;
94  }
95  if (_optimisticManifest) {
96    return _optimisticManifest;
97  }
98  return nil;
99}
100
101- (nullable NSData *)bundle
102{
103  if (_bundle) {
104    return _bundle;
105  }
106  return nil;
107}
108
109- (void)forceBundleReload
110{
111  if (self.status == kEXAppLoaderStatusNew) {
112    @throw [NSException exceptionWithName:NSInternalInconsistencyException
113                                   reason:@"Tried to load a bundle from an AppLoader with no manifest."
114                                 userInfo:@{}];
115  }
116  NSAssert([self supportsBundleReload], @"Tried to force a bundle reload on a non-development bundle");
117  if (self.isLoadingDevelopmentJavaScriptResource) {
118    // prevent multiple simultaneous fetches from the development server.
119    // this can happen when reloading a bundle with remote debugging enabled;
120    // RN requests the bundle multiple times for some reason.
121    // TODO: fix inside of RN
122    return;
123  }
124  [self _loadDevelopmentJavaScriptResource];
125}
126
127- (BOOL)supportsBundleReload
128{
129  if (_optimisticManifest) {
130    return _optimisticManifest.isUsingDeveloperTool;
131  }
132  return NO;
133}
134
135#pragma mark - public
136
137- (void)request
138{
139  [self _reset];
140  [self _beginRequest];
141}
142
143- (void)requestFromCache
144{
145  [self request];
146}
147
148#pragma mark - EXHomeAppLoaderTaskDelegate
149
150- (void)homeAppLoaderTask:(EXHomeAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher
151{
152  if (_error) {
153    return;
154  }
155
156  if (!_optimisticManifest) {
157    [self _setOptimisticManifest:launcher.launchedUpdate.manifest];
158  }
159
160  // HomeAppLoaderTask always sets this to true
161  _isUpToDate = true;
162
163  if (launcher.launchedUpdate.manifest.isUsingDeveloperTool) {
164    // in dev mode, we need to set an optimistic manifest but nothing else
165    return;
166  }
167  _confirmedManifest = launcher.launchedUpdate.manifest;
168  if (_confirmedManifest == nil) {
169    return;
170  }
171  _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl];
172
173  if (self.delegate) {
174    [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle];
175  }
176}
177
178- (void)homeAppLoaderTask:(EXHomeAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error
179{
180  _error = error;
181
182  if (self.delegate) {
183    [self.delegate appLoader:self didFailWithError:_error];
184  }
185}
186
187#pragma mark - internal
188
189- (BOOL)_initializeDatabase
190{
191  EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
192  BOOL success = updatesDatabaseManager.isDatabaseOpen;
193  if (!updatesDatabaseManager.isDatabaseOpen) {
194    success = [updatesDatabaseManager openDatabase];
195  }
196
197  if (!success) {
198    _error = updatesDatabaseManager.error;
199    if (self.delegate) {
200      [self.delegate appLoader:self didFailWithError:_error];
201    }
202    return NO;
203  } else {
204    return YES;
205  }
206}
207
208- (void)_beginRequest
209{
210  if (![self _initializeDatabase]) {
211    return;
212  }
213  [self _startLoaderTask];
214}
215
216- (void)_startLoaderTask
217{
218  EXUpdatesConfig *config = [EXUpdatesConfig configFromDictionary:@{
219    EXUpdatesConfig.EXUpdatesConfigHasEmbeddedUpdateKey: @NO,
220    EXUpdatesConfig.EXUpdatesConfigSDKVersionKey: [self _sdkVersions],
221    EXUpdatesConfig.EXUpdatesConfigScopeKeyKey: self.manifestAndAssetRequestHeaders.manifest.scopeKey,
222    EXUpdatesConfig.EXUpdatesConfigExpectsSignedManifestKey: @YES,
223    EXUpdatesConfig.EXUpdatesConfigRequestHeadersKey: [self _requestHeaders]
224  }];
225
226  EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
227
228  NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy];
229  [sdkVersions addObject:@"UNVERSIONED"];
230
231  NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count];
232  for (NSString *sdkVersion in sdkVersions) {
233    [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]];
234  }
235  [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"];
236  [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions];
237
238  EXUpdatesSelectionPolicy *selectionPolicy = [[EXUpdatesSelectionPolicy alloc]
239                                               initWithLauncherSelectionPolicy:[[EXExpoGoLauncherSelectionPolicyFilterAware alloc] initWithSdkVersions:sdkVersions]
240                                               loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new]
241                                               reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]];
242
243  EXHomeAppLoaderTask *loaderTask = [[EXHomeAppLoaderTask alloc] initWithManifestAndAssetRequestHeaders:self.manifestAndAssetRequestHeaders
244                                                                                                 config:config
245                                                                                               database:updatesDatabaseManager.database
246                                                                                              directory:updatesDatabaseManager.updatesDirectory
247                                                                                        selectionPolicy:selectionPolicy
248                                                                                          delegateQueue:_appLoaderQueue];
249  loaderTask.delegate = self;
250  [loaderTask start];
251}
252
253- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest
254{
255  _optimisticManifest = manifest;
256  if (self.delegate) {
257    [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest];
258  }
259}
260
261- (void)_loadDevelopmentJavaScriptResource
262{
263  _isLoadingDevelopmentJavaScriptResource = YES;
264  EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self];
265  [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) {
266    if (self.delegate) {
267      [self.delegate appLoader:self didLoadBundleWithProgress:progress];
268    }
269  } success:^(NSData *bundle) {
270    self.isUpToDate = YES;
271    self.bundle = bundle;
272    self.isLoadingDevelopmentJavaScriptResource = NO;
273    if (self.delegate) {
274      [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle];
275    }
276  } error:^(NSError *error) {
277    self.error = error;
278    self.isLoadingDevelopmentJavaScriptResource = NO;
279    if (self.delegate) {
280      [self.delegate appLoader:self didFailWithError:error];
281    }
282  }];
283}
284
285#pragma mark - headers
286
287- (NSDictionary *)_requestHeaders
288{
289  NSDictionary *requestHeaders = @{
290      @"Exponent-SDK-Version": [self _sdkVersions],
291      @"Exponent-Accept-Signature": @"true",
292      @"Exponent-Platform": @"ios",
293      @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
294      @"Expo-Client-Environment": [self _clientEnvironment],
295      @"Expo-Updates-Environment": [self _clientEnvironment],
296      @"User-Agent": [self _userAgentString],
297      @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType]
298  };
299
300  NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret];
301  if (sessionSecret) {
302    NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy];
303    requestHeadersMutable[@"Expo-Session"] = sessionSecret;
304    requestHeaders = requestHeadersMutable;
305  }
306
307  return requestHeaders;
308}
309
310- (NSString *)_userAgentString
311{
312  struct utsname systemInfo;
313  uname(&systemInfo);
314  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
315  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
316          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
317          deviceModel,
318          [UIDevice currentDevice].systemName,
319          [UIDevice currentDevice].systemVersion,
320          [UIScreen mainScreen].scale,
321          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
322}
323
324- (NSString *)_clientEnvironment
325{
326  if ([EXEnvironment sharedEnvironment].isDetached) {
327    return @"STANDALONE";
328  } else {
329    return @"EXPO_DEVICE";
330#if TARGET_IPHONE_SIMULATOR
331    return @"EXPO_SIMULATOR";
332#endif
333  }
334}
335
336- (NSString *)_sdkVersions
337{
338  NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"];
339  if (versionsAvailable) {
340    return [versionsAvailable componentsJoinedByString:@","];
341  } else {
342    return [EXVersions sharedInstance].temporarySdkVersion;
343  }
344}
345
346+ (EXManifestAndAssetRequestHeaders * _Nullable)bundledDevelopmentHomeManifestAndAssetRequestHeaders
347{
348  NSString *manifestAndAssetRequestHeadersJson = [EXBuildConstants sharedInstance].kernelManifestAndAssetRequestHeadersJsonString;
349  if (!manifestAndAssetRequestHeadersJson) {
350    return nil;
351  }
352
353  id manifestAndAssetRequestHeaders = RCTJSONParse(manifestAndAssetRequestHeadersJson, nil);
354  if ([manifestAndAssetRequestHeaders isKindOfClass:[NSDictionary class]]) {
355    id manifest = manifestAndAssetRequestHeaders[@"manifest"];
356    id assetRequestHeaders = manifestAndAssetRequestHeaders[@"assetRequestHeaders"];
357    if ([manifest isKindOfClass:[NSDictionary class]]) {
358      return [[EXManifestAndAssetRequestHeaders alloc] initWithManifest:[EXManifestsManifestFactory manifestForManifestJSON:manifest]
359                                                    assetRequestHeaders:assetRequestHeaders];
360    }
361  }
362
363  return nil;
364}
365
366@end
367
368NS_ASSUME_NONNULL_END
369