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