FBSimulatorControl/Commands/FBSimulatorApplicationCommands.m (426 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBSimulatorApplicationCommands.h" #import <CoreSimulator/SimDevice.h> #import <FBControlCore/FBControlCore.h> #import "FBSimulator+Private.h" #import "FBSimulator.h" #import "FBSimulatorError.h" #import "FBSimulatorLaunchCtlCommands.h" #import "FBSimulatorLaunchedApplication.h" #import "FBSimulatorProcessSpawnCommands.h" @interface FBSimulatorApplicationCommands () @property (nonatomic, weak, readonly) FBSimulator *simulator; @end @implementation FBSimulatorApplicationCommands + (instancetype)commandsWithTarget:(FBSimulator *)target { return [[self alloc] initWithSimulator:target]; } - (instancetype)initWithSimulator:(FBSimulator *)simulator { self = [super init]; if (!self) { return nil; } _simulator = simulator; return self; } #pragma mark - FBApplicationCommands Implementation - (FBFuture<FBInstalledApplication *> *)installApplicationWithPath:(NSString *)path { return [[self confirmCompatibilityOfApplicationAtPath:path] onQueue:self.simulator.workQueue fmap:^ FBFuture<FBInstalledApplication *> * (FBBundleDescriptor *appBundle) { NSDictionary *options = @{ @"CFBundleIdentifier": appBundle.identifier }; NSURL *appURL = [NSURL fileURLWithPath:appBundle.path]; NSError *error = nil; if ([self.simulator.device installApplication:appURL withOptions:options error:&error]) { return [self installedApplicationWithBundleID:appBundle.identifier]; } // Retry install if the first attempt failed with 'Failed to load Info.plist...'. // This is to mitagate an error where the first install of an app after uninstalling it // always fails. // See Apple bug report 46691107 if ([error.description containsString:@"Failed to load Info.plist from bundle at path"]) { [self.simulator.logger log:@"Retrying install due to reinstall bug"]; error = nil; if ([self.simulator.device installApplication:appURL withOptions:options error:&error]) { return [self installedApplicationWithBundleID:appBundle.identifier]; } } return [[[FBSimulatorError describeFormat:@"Failed to install Application %@ with options %@", appBundle, options] causedBy:error] failFuture]; }]; } - (FBFuture<FBSimulatorLaunchedApplication *> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration { FBSimulator *simulator = self.simulator; FBProcessIO *io = configuration.io; return [[[FBFuture futureWithFutures:@[ [self ensureApplicationIsInstalled:configuration.bundleID], [self confirmApplicationLaunchState:configuration.bundleID launchMode:configuration.launchMode waitForDebugger:configuration.waitForDebugger], ]] onQueue:simulator.workQueue fmap:^(id _) { return [io attachViaFile]; }] onQueue:simulator.workQueue fmap:^ FBFuture<FBSimulatorLaunchedApplication *> * (FBProcessFileAttachment *attachment) { FBFuture<NSNumber *> *launch = [self launchApplication:configuration stdOut:attachment.stdOut stdErr:attachment.stdErr]; return [FBSimulatorLaunchedApplication applicationWithSimulator:simulator configuration:configuration attachment:attachment launchFuture:launch]; }]; } - (FBFuture<NSNull *> *)killApplicationWithBundleID:(NSString *)bundleID { if (!bundleID) { return [[FBSimulatorError describe:@"Bundle ID was not provided"] failFuture]; } SimDevice *simDevice = self.simulator.device; return [FBFuture onQueue:self.simulator.workQueue resolveValue:^ NSNull * (NSError **error) { if (![simDevice terminateApplicationWithID:bundleID error:error]) { return nil; } return NSNull.null; }]; } - (FBFuture<NSArray<FBInstalledApplication *> *> *)installedApplications { return [[FBFuture onQueue:self.simulator.workQueue resolveValue:^ NSDictionary<NSString *, id> * (NSError **error) { return [self.simulator.device installedAppsWithError:error]; }] onQueue:self.simulator.asyncQueue map:^(NSDictionary<NSString *, id> *installedApps) { NSMutableArray<FBInstalledApplication *> *applications = [NSMutableArray array]; for (NSDictionary *appInfo in installedApps.allValues) { FBInstalledApplication *application = [FBSimulatorApplicationCommands installedApplicationFromInfo:appInfo error:nil]; if (!application) { continue; } [applications addObject:application]; } return applications; }]; } - (FBFuture<NSNull *> *)uninstallApplicationWithBundleID:(NSString *)bundleID { NSParameterAssert(bundleID); return [[[[self.simulator installedApplicationWithBundleID:bundleID] onQueue:self.simulator.asyncQueue fmap:^FBFuture *(FBInstalledApplication *installedApplication) { if (installedApplication.installType == FBApplicationInstallTypeSystem) { return [[FBSimulatorError describeFormat:@"Can't uninstall '%@' as it is a system Application", installedApplication] failFuture]; } return [FBFuture futureWithResult:installedApplication]; }] onQueue:self.simulator.workQueue fmap:^(FBInstalledApplication *_) { return [[self.simulator killApplicationWithBundleID:bundleID] fallback:NSNull.null]; }] onQueue:self.simulator.workQueue fmap:^ FBFuture<NSNull *> * (id _) { NSError *error = nil; if (![self.simulator.device uninstallApplication:bundleID withOptions:nil error:&error]) { return [[[FBSimulatorError describeFormat:@"Failed to uninstall '%@'", bundleID] causedBy:error] failFuture]; } return FBFuture.empty; }]; } - (FBFuture<FBInstalledApplication *> *)installedApplicationWithBundleID:(NSString *)bundleID { SimDevice *device = self.simulator.device; return [FBFuture onQueue:self.simulator.workQueue resolveValue:^ FBInstalledApplication * (NSError **error) { // -[SimDevice propertiesOfApplication:error:] will return in success if the app could not be found. // The dictionary only contains one element, which is the bundle id of the non-existant app. // This internal helper method understands this, so we can just re-use it here. NSString *applicationType = nil; BOOL applicationIsInstalled = [device applicationIsInstalled:bundleID type:&applicationType error:error]; if (!applicationIsInstalled) { return [[FBSimulatorError describeFormat:@"Cannot get app information for '%@', it is not installed", bundleID] fail:error]; } // appInfo is usually always returned, even if there is no app installed. NSDictionary<NSString *, id> *appInfo = [device propertiesOfApplication:bundleID error:error]; if (!appInfo) { return nil; } // Therefore we have to parse the app info to see that it is actually a real app. FBInstalledApplication *application = [FBSimulatorApplicationCommands installedApplicationFromInfo:appInfo error:error]; if (!application) { return nil; } return application; }]; } - (FBFuture<NSDictionary<NSString *, NSNumber *> *> *)runningApplications { static dispatch_once_t onceToken; static NSRegularExpression *regex; dispatch_once(&onceToken, ^{ NSError *error = nil; regex = [NSRegularExpression regularExpressionWithPattern:@"UIKitApplication:" options:0 error:&error]; NSCAssert(error == nil, @"Invalid regular expression"); }); return [[self.simulator serviceNamesAndProcessIdentifiersMatching:regex] onQueue:self.simulator.asyncQueue map:^(NSDictionary<NSString *, NSNumber *> *serviceNameToProcessIdentifier) { NSMutableDictionary<NSString *, NSNumber *> *mapping = [NSMutableDictionary dictionary]; for (NSString *serviceName in serviceNameToProcessIdentifier.allKeys) { NSString *bundleName = [FBSimulatorLaunchCtlCommands extractApplicationBundleIdentifierFromServiceName:serviceName]; if (!bundleName) { continue; } mapping[bundleName] = serviceNameToProcessIdentifier[serviceName]; } return mapping; }]; } - (FBFuture<NSNumber *> *)processIDWithBundleID:(NSString *)bundleID { NSError *error = nil; NSString *pattern = [NSString stringWithFormat:@"UIKitApplication:%@\\[|$",[NSRegularExpression escapedPatternForString:bundleID]]; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; if (error) { return [[FBSimulatorError describeFormat:@"Couldn't build search pattern for '%@'", bundleID] failFuture]; } return [[FBFuture onQueue:self.simulator.workQueue resolve:^{ return [self.simulator firstServiceNameAndProcessIdentifierMatching:regex]; }] onQueue:self.simulator.workQueue map:^(NSArray<id> *result) { return result[1]; }]; } #pragma mark Public + (FBFuture<NSDictionary<NSString *, NSURL *> *> *)groupContainerToPathMappingForSimulator:(FBSimulator *)simulator { FBSimulatorApplicationCommands *commands = [[FBSimulatorApplicationCommands alloc] initWithSimulator:simulator]; return [commands groupContainerToPathMapping]; } + (FBFuture<NSDictionary<NSString *, NSURL *> *> *)applicationContainerToPathMappingForSimulator:(FBSimulator *)simulator { FBSimulatorApplicationCommands *commands = [[FBSimulatorApplicationCommands alloc] initWithSimulator:simulator]; return [commands applicationContainerToPathMapping]; } #pragma mark Private - (FBFuture<NSDictionary<NSString *, NSURL *> *> *)groupContainerToPathMapping { return [[FBFuture onQueue:self.simulator.workQueue resolveValue:^ NSDictionary<NSString *, id> * (NSError **error) { return [self.simulator.device installedAppsWithError:error]; }] onQueue:self.simulator.asyncQueue map:^(NSDictionary<NSString *, id> *installedApps) { NSMutableDictionary<NSString *, NSURL *> *mapping = NSMutableDictionary.dictionary; for (NSString *key in installedApps.allKeys) { NSDictionary<NSString *, id> *app = installedApps[key]; NSDictionary<NSString *, id> *appContainers = app[@"GroupContainers"]; if (!appContainers) { continue; } [mapping addEntriesFromDictionary:appContainers]; } return [mapping copy]; }]; } - (FBFuture<NSDictionary<NSString *, NSURL *> *> *)applicationContainerToPathMapping { return [[FBFuture onQueue:self.simulator.workQueue resolveValue:^ NSDictionary<NSString *, id> * (NSError **error) { return [self.simulator.device installedAppsWithError:error]; }] onQueue:self.simulator.asyncQueue map:^(NSDictionary<NSString *, id> *installedApps) { NSMutableDictionary<NSString *, NSURL *> *mapping = NSMutableDictionary.dictionary; for (NSString *bundleID in installedApps.allKeys) { NSDictionary<NSString *, id> *app = installedApps[bundleID]; NSURL *dataContainer = app[KeyDataContainer]; if (!dataContainer) { continue; } mapping[bundleID] = dataContainer; } return [mapping copy]; }]; } - (FBFuture<NSNumber *> *)ensureApplicationIsInstalled:(NSString *)bundleID { return [[[self.simulator installedApplicationWithBundleID:bundleID] mapReplace:NSNull.null] onQueue:self.simulator.asyncQueue handleError:^(NSError *error) { return [[FBSimulatorError describeFormat:@"App %@ can't be launched as it isn't installed: %@", bundleID, error] failFuture]; }]; } - (FBFuture<NSNumber *> *)confirmApplicationLaunchState:(NSString *)bundleID launchMode:(FBApplicationLaunchMode)launchMode waitForDebugger:(BOOL)waitForDebugger { if (waitForDebugger && launchMode == FBApplicationLaunchModeForegroundIfRunning) { return [[FBSimulatorError describe:@"'Foreground if running' and 'wait for debugger cannot be applied simultaneously"] failFuture]; } FBSimulator *simulator = self.simulator; return [[simulator processIDWithBundleID:bundleID] onQueue:simulator.asyncQueue chain:^ FBFuture<NSNull *> * (FBFuture<NSNumber *> *processFuture) { NSNumber *processID = processFuture.result; if (!processID) { return FBFuture.empty; } if (launchMode == FBApplicationLaunchModeFailIfRunning) { return [[FBSimulatorError describeFormat:@"App %@ can't be launched as is running (PID=%@)", bundleID, processID] failFuture]; } else if (launchMode == FBApplicationLaunchModeRelaunchIfRunning) { return [self killApplicationWithBundleID:bundleID]; } return FBFuture.empty; }]; } - (FBFuture<NSNumber *> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration stdOut:(id<FBProcessFileOutput>)stdOut stdErr:(id<FBProcessFileOutput>)stdErr { // Start reading now, but don't block on the resolution, we will ensure that the read has started after the app has launched. FBFuture *readingFutures = [FBFuture futureWithFutures:@[ [stdOut startReading], [stdErr startReading], ]]; return [[self launchApplication:configuration stdOutPath:stdOut.filePath stdErrPath:stdErr.filePath] onQueue:self.simulator.workQueue fmap:^(NSNumber *result) { return [readingFutures mapReplace:result]; }]; } - (FBFuture<NSNumber *> *)isApplicationRunning:(NSString *)bundleID { return [[self.simulator processIDWithBundleID:bundleID] onQueue:self.simulator.workQueue chain:^(FBFuture<NSNumber *> *future){ NSNumber *processIdentifier = future.result; return processIdentifier ? [FBFuture futureWithResult:@YES] : [FBFuture futureWithResult:@NO]; }]; } - (FBFuture<NSNumber *> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration stdOutPath:(NSString *)stdOutPath stdErrPath:(NSString *)stdErrPath { FBSimulator *simulator = self.simulator; NSDictionary<NSString *, id> *options = [FBSimulatorApplicationCommands simDeviceLaunchOptionsForConfiguration:configuration stdOutPath:[self translateAbsolutePath:stdOutPath toPathRelativeTo:simulator.dataDirectory] stdErrPath:[self translateAbsolutePath:stdErrPath toPathRelativeTo:simulator.dataDirectory]]; FBMutableFuture<NSNumber *> *future = [FBMutableFuture future]; id<FBControlCoreLogger> logger = self.simulator.logger; [logger logFormat: @"Launching Application %@ with %@ %@", configuration.bundleID, [FBCollectionInformation oneLineDescriptionFromArray:configuration.arguments], [FBCollectionInformation oneLineDescriptionFromDictionary:configuration.environment] ]; [simulator.device launchApplicationAsyncWithID:configuration.bundleID options:options completionQueue:simulator.workQueue completionHandler:^(NSError *error, pid_t pid){ if (error) { [logger logFormat:@"Failed to launch Application %@ %@", configuration.bundleID, error]; [future resolveWithError:error]; } else { [logger logFormat:@"Launched Application %@ with pid %d", configuration.bundleID, pid]; [future resolveWithResult:@(pid)]; } }]; return future; } - (NSString *)translateAbsolutePath:(NSString *)absolutePath toPathRelativeTo:(NSString *)referencePath { if (![absolutePath hasPrefix:@"/"]) { return absolutePath; } // When launching an application with a custom stdout/stderr path, `SimDevice` uses the given path relative // to the Simulator's data directory. From the Framework's consumer point of view this might not be the // wanted behaviour. To work around it, we construct a path relative to the Simulator's data directory // using `..` until we end up in the absolute path outside the Simulator's data directory. NSString *translatedPath = @""; for (NSUInteger index = 0; index < referencePath.pathComponents.count; index++) { translatedPath = [translatedPath stringByAppendingPathComponent:@".."]; } return [translatedPath stringByAppendingPathComponent:absolutePath]; } + (NSDictionary<NSString *, id> *)simDeviceLaunchOptionsForConfiguration:(FBApplicationLaunchConfiguration *)configuration stdOutPath:(nullable NSString *)stdOutPath stdErrPath:(nullable NSString *)stdErrPath { NSMutableDictionary<NSString *, id> *options = [[FBSimulatorProcessSpawnCommands launchOptionsWithArguments:configuration.arguments environment:configuration.environment waitForDebugger:configuration.waitForDebugger] mutableCopy]; if (stdOutPath){ options[@"stdout"] = stdOutPath; } if (stdErrPath) { options[@"stderr"] = stdErrPath; } return [options copy]; } static NSString *const KeyDataContainer = @"DataContainer"; + (FBInstalledApplication *)installedApplicationFromInfo:(NSDictionary<NSString *, id> *)appInfo error:(NSError **)error { NSString *appName = appInfo[FBApplicationInstallInfoKeyBundleName]; if (![appName isKindOfClass:NSString.class]) { return [[FBControlCoreError describeFormat:@"Bundle Name %@ is not a String for %@ in %@", appName, FBApplicationInstallInfoKeyBundleName, appInfo] fail:error]; } NSString *bundleIdentifier = appInfo[FBApplicationInstallInfoKeyBundleIdentifier]; if (![bundleIdentifier isKindOfClass:NSString.class]) { return [[FBControlCoreError describeFormat:@"Bundle Identifier %@ is not a String for %@ in %@", bundleIdentifier, FBApplicationInstallInfoKeyBundleIdentifier, appInfo] fail:error]; } NSString *appPath = appInfo[FBApplicationInstallInfoKeyPath]; if (![appPath isKindOfClass:NSString.class]) { return [[FBControlCoreError describeFormat:@"App Path %@ is not a String for %@ in %@", appPath, FBApplicationInstallInfoKeyPath, appInfo] fail:error]; } NSString *typeString = appInfo[FBApplicationInstallInfoKeyApplicationType]; if (![typeString isKindOfClass:NSString.class]) { return [[FBControlCoreError describeFormat:@"Install Type %@ is not a String for %@ in %@", typeString, FBApplicationInstallInfoKeyApplicationType, appInfo] fail:error]; } NSURL *dataContainer = appInfo[KeyDataContainer]; if (dataContainer && ![dataContainer isKindOfClass:NSURL.class]) { return [[FBControlCoreError describeFormat:@"Data Container %@ is not a NSURL for %@ in %@", dataContainer, KeyDataContainer, appInfo] fail:error]; } FBBundleDescriptor *bundle = [FBBundleDescriptor bundleFromPath:appPath error:error]; if (!bundle) { return nil; } return [FBInstalledApplication installedApplicationWithBundle:bundle installTypeString:typeString signerIdentity:nil dataContainer:dataContainer.path]; } - (FBFuture<FBBundleDescriptor *> *)confirmCompatibilityOfApplicationAtPath:(NSString *)path { NSError *error = nil; FBBundleDescriptor *application = [FBBundleDescriptor bundleFromPath:path error:&error]; if (!application) { return [[[FBSimulatorError describeFormat:@"Could not determine Application information for path %@", path] causedBy:error] failFuture]; } return [[self.simulator installedApplicationWithBundleID:application.identifier] onQueue:self.simulator.workQueue chain:^FBFuture *(FBFuture<FBInstalledApplication *> *future) { FBInstalledApplication *installed = future.result; if (installed && installed.installType == FBApplicationInstallTypeSystem) { return [[FBSimulatorError describeFormat:@"Cannot install app as it is a system app %@", installed] failFuture]; } NSSet<NSString *> *binaryArchitectures = application.binary.architectures; NSSet<NSString *> *supportedArchitectures = FBiOSTargetConfiguration.baseArchToCompatibleArch[self.simulator.deviceType.simulatorArchitecture]; if (![binaryArchitectures intersectsSet:supportedArchitectures]) { return [[FBSimulatorError describeFormat: @"Simulator does not support any of the architectures (%@) of the executable at %@. Simulator Archs (%@)", [FBCollectionInformation oneLineDescriptionFromArray:binaryArchitectures.allObjects], application.binary.path, [FBCollectionInformation oneLineDescriptionFromArray:supportedArchitectures.allObjects]] failFuture]; } return [FBFuture futureWithResult:application]; }]; } @end