// Copyright 2020-present 650 Industries. All rights reserved. #import "EXAppFetcher.h" #import "EXDevelopmentHomeLoader.h" #import "EXClientReleaseType.h" #import "EXEnvironment.h" #import "EXErrorRecoveryManager.h" #import "EXFileDownloader.h" #import "EXKernel.h" #import "EXKernelLinkingManager.h" #import "EXManifestResource.h" #import "EXSession.h" #import "EXUpdatesDatabaseManager.h" #import "EXVersions.h" #import "EXBuildConstants.h" #if defined(EX_DETACHED) #import "ExpoKit-Swift.h" #else #import "Expo_Go-Swift.h" #endif // defined(EX_DETACHED) #import #import @import EXManifests; @import EXUpdates; NS_ASSUME_NONNULL_BEGIN @interface EXDevelopmentHomeLoader () @property (nonatomic, strong, nullable) EXManifestAndAssetRequestHeaders *manifestAndAssetRequestHeaders; @property (nonatomic, strong, nullable) EXManifestsManifest *confirmedManifest; @property (nonatomic, strong, nullable) EXManifestsManifest *optimisticManifest; @property (nonatomic, strong, nullable) NSData *bundle; @property (nonatomic, assign) BOOL isUpToDate; /** * Stateful variable to let us prevent multiple simultaneous fetches from the development server. * This can happen when reloading a bundle with remote debugging enabled; * RN requests the bundle multiple times for some reason. */ @property (nonatomic, assign) BOOL isLoadingDevelopmentJavaScriptResource; @property (nonatomic, strong, nullable) NSError *error; @property (nonatomic, strong) dispatch_queue_t appLoaderQueue; @end @implementation EXDevelopmentHomeLoader @synthesize bundle = _bundle; @synthesize isUpToDate = _isUpToDate; - (instancetype)init { if (self = [super init]) { _manifestAndAssetRequestHeaders = [EXDevelopmentHomeLoader bundledDevelopmentHomeManifestAndAssetRequestHeaders]; _appLoaderQueue = dispatch_queue_create("host.exp.exponent.LoaderQueue", DISPATCH_QUEUE_SERIAL); } return self; } #pragma mark - getters and lifecycle - (void)_reset { _confirmedManifest = nil; _optimisticManifest = nil; _bundle = nil; _error = nil; _isUpToDate = NO; _isLoadingDevelopmentJavaScriptResource = NO; } - (EXAppLoaderStatus)status { if (_error) { return kEXAppLoaderStatusError; } else if (_bundle) { return kEXAppLoaderStatusHasManifestAndBundle; } else if (_optimisticManifest) { return kEXAppLoaderStatusHasManifest; } return kEXAppLoaderStatusNew; } - (nullable EXManifestsManifest *)manifest { if (_confirmedManifest) { return _confirmedManifest; } if (_optimisticManifest) { return _optimisticManifest; } return nil; } - (nullable NSData *)bundle { if (_bundle) { return _bundle; } return nil; } - (void)forceBundleReload { if (self.status == kEXAppLoaderStatusNew) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Tried to load a bundle from an AppLoader with no manifest." userInfo:@{}]; } NSAssert([self supportsBundleReload], @"Tried to force a bundle reload on a non-development bundle"); if (self.isLoadingDevelopmentJavaScriptResource) { // prevent multiple simultaneous fetches from the development server. // this can happen when reloading a bundle with remote debugging enabled; // RN requests the bundle multiple times for some reason. // TODO: fix inside of RN return; } [self _loadDevelopmentJavaScriptResource]; } - (BOOL)supportsBundleReload { if (_optimisticManifest) { return _optimisticManifest.isUsingDeveloperTool; } return NO; } #pragma mark - public - (void)request { [self _reset]; [self _beginRequest]; } - (void)requestFromCache { [self request]; } #pragma mark - EXHomeAppLoaderTaskDelegate - (void)homeAppLoaderTask:(EXHomeAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id)launcher { if (_error) { return; } if (!_optimisticManifest) { [self _setOptimisticManifest:launcher.launchedUpdate.manifest]; } // HomeAppLoaderTask always sets this to true _isUpToDate = true; if (launcher.launchedUpdate.manifest.isUsingDeveloperTool) { // in dev mode, we need to set an optimistic manifest but nothing else return; } _confirmedManifest = launcher.launchedUpdate.manifest; if (_confirmedManifest == nil) { return; } _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl]; if (self.delegate) { [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; } } - (void)homeAppLoaderTask:(EXHomeAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error { _error = error; if (self.delegate) { [self.delegate appLoader:self didFailWithError:_error]; } } #pragma mark - internal - (BOOL)_initializeDatabase { EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; BOOL success = updatesDatabaseManager.isDatabaseOpen; if (!updatesDatabaseManager.isDatabaseOpen) { success = [updatesDatabaseManager openDatabase]; } if (!success) { _error = updatesDatabaseManager.error; if (self.delegate) { [self.delegate appLoader:self didFailWithError:_error]; } return NO; } else { return YES; } } - (void)_beginRequest { if (![self _initializeDatabase]) { return; } [self _startLoaderTask]; } - (void)_startLoaderTask { EXUpdatesConfig *config = [EXUpdatesConfig configFromDictionary:@{ EXUpdatesConfig.EXUpdatesConfigHasEmbeddedUpdateKey: @NO, EXUpdatesConfig.EXUpdatesConfigSDKVersionKey: [self _sdkVersions], EXUpdatesConfig.EXUpdatesConfigScopeKeyKey: self.manifestAndAssetRequestHeaders.manifest.scopeKey, EXUpdatesConfig.EXUpdatesConfigExpectsSignedManifestKey: @YES, EXUpdatesConfig.EXUpdatesConfigRequestHeadersKey: [self _requestHeaders] }]; EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy]; [sdkVersions addObject:@"UNVERSIONED"]; NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count]; for (NSString *sdkVersion in sdkVersions) { [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]]; } [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"]; [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions]; EXUpdatesSelectionPolicy *selectionPolicy = [[EXUpdatesSelectionPolicy alloc] initWithLauncherSelectionPolicy:[[EXExpoGoLauncherSelectionPolicyFilterAware alloc] initWithSdkVersions:sdkVersions] loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new] reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]]; EXHomeAppLoaderTask *loaderTask = [[EXHomeAppLoaderTask alloc] initWithManifestAndAssetRequestHeaders:self.manifestAndAssetRequestHeaders config:config database:updatesDatabaseManager.database directory:updatesDatabaseManager.updatesDirectory selectionPolicy:selectionPolicy delegateQueue:_appLoaderQueue]; loaderTask.delegate = self; [loaderTask start]; } - (void)_setOptimisticManifest:(EXManifestsManifest *)manifest { _optimisticManifest = manifest; if (self.delegate) { [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest]; } } - (void)_loadDevelopmentJavaScriptResource { _isLoadingDevelopmentJavaScriptResource = YES; EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self]; [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) { if (self.delegate) { [self.delegate appLoader:self didLoadBundleWithProgress:progress]; } } success:^(NSData *bundle) { self.isUpToDate = YES; self.bundle = bundle; self.isLoadingDevelopmentJavaScriptResource = NO; if (self.delegate) { [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle]; } } error:^(NSError *error) { self.error = error; self.isLoadingDevelopmentJavaScriptResource = NO; if (self.delegate) { [self.delegate appLoader:self didFailWithError:error]; } }]; } #pragma mark - headers - (NSDictionary *)_requestHeaders { NSDictionary *requestHeaders = @{ @"Exponent-SDK-Version": [self _sdkVersions], @"Exponent-Accept-Signature": @"true", @"Exponent-Platform": @"ios", @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], @"Expo-Client-Environment": [self _clientEnvironment], @"Expo-Updates-Environment": [self _clientEnvironment], @"User-Agent": [self _userAgentString], @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType] }; NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; if (sessionSecret) { NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy]; requestHeadersMutable[@"Expo-Session"] = sessionSecret; requestHeaders = requestHeadersMutable; } return requestHeaders; } - (NSString *)_userAgentString { struct utsname systemInfo; uname(&systemInfo); NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], deviceModel, [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion, [UIScreen mainScreen].scale, [NSLocale autoupdatingCurrentLocale].localeIdentifier]; } - (NSString *)_clientEnvironment { if ([EXEnvironment sharedEnvironment].isDetached) { return @"STANDALONE"; } else { return @"EXPO_DEVICE"; #if TARGET_IPHONE_SIMULATOR return @"EXPO_SIMULATOR"; #endif } } - (NSString *)_sdkVersions { NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; if (versionsAvailable) { return [versionsAvailable componentsJoinedByString:@","]; } else { return [EXVersions sharedInstance].temporarySdkVersion; } } + (EXManifestAndAssetRequestHeaders * _Nullable)bundledDevelopmentHomeManifestAndAssetRequestHeaders { NSString *manifestAndAssetRequestHeadersJson = [EXBuildConstants sharedInstance].kernelManifestAndAssetRequestHeadersJsonString; if (!manifestAndAssetRequestHeadersJson) { return nil; } id manifestAndAssetRequestHeaders = RCTJSONParse(manifestAndAssetRequestHeadersJson, nil); if ([manifestAndAssetRequestHeaders isKindOfClass:[NSDictionary class]]) { id manifest = manifestAndAssetRequestHeaders[@"manifest"]; id assetRequestHeaders = manifestAndAssetRequestHeaders[@"assetRequestHeaders"]; if ([manifest isKindOfClass:[NSDictionary class]]) { return [[EXManifestAndAssetRequestHeaders alloc] initWithManifest:[EXManifestsManifestFactory manifestForManifestJSON:manifest] assetRequestHeaders:assetRequestHeaders]; } } return nil; } @end NS_ASSUME_NONNULL_END