// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import #include #include #include #include // An executable (iossim) that runs an app in the iOS Simulator. // Run 'iossim -h' for usage information. // // For best results, the iOS Simulator application should not be running when // iossim is invoked. // // Headers for the iPhoneSimulatorRemoteClient framework used in this tool are // generated by class-dump, via GYP. // (class-dump is available at http://www.codethecode.com/projects/class-dump/) // // However, there are some forward declarations required to get things to // compile. Also, the DTiPhoneSimulatorSessionDelegate protocol is referenced // by the iPhoneSimulatorRemoteClient framework, but not defined in the object // file, so it must be defined here before importing the generated // iPhoneSimulatorRemoteClient.h file. @class DTiPhoneSimulatorApplicationSpecifier; @class DTiPhoneSimulatorSession; @class DTiPhoneSimulatorSessionConfig; @class DTiPhoneSimulatorSystemRoot; @protocol DTiPhoneSimulatorSessionDelegate - (void)session:(DTiPhoneSimulatorSession*)session didEndWithError:(NSError*)error; - (void)session:(DTiPhoneSimulatorSession*)session didStart:(BOOL)started withError:(NSError*)error; @end #import "iPhoneSimulatorRemoteClient.h" // An undocumented system log key included in messages from launchd. The value // is the PID of the process the message is about (as opposed to launchd's PID). #define ASL_KEY_REF_PID "RefPID" namespace { // Name of environment variables that control the user's home directory in the // simulator. const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME"; const char* const kHomeEnvVariable = "HOME"; // Device family codes for iPhone and iPad. const int kIPhoneFamily = 1; const int kIPadFamily = 2; // Max number of seconds to wait for the simulator session to start. // This timeout must allow time to start up iOS Simulator, install the app // and perform any other black magic that is encoded in the // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up // time is only a couple seconds but machine load, disk caches, etc., can all // affect startup time in the wild so the timeout needs to be fairly generous. // If this timeout occurs iossim will likely exit with non-zero status; the // exception being if the app is invoked and completes execution before the // session is started (this case is handled in session:didStart:withError). const NSTimeInterval kDefaultSessionStartTimeoutSeconds = 30; // While the simulated app is running, its stdout is redirected to a file which // is polled by iossim and written to iossim's stdout using the following // polling interval. const NSTimeInterval kOutputPollIntervalSeconds = 0.1; // The path within the developer dir of the private Simulator frameworks. NSString* const kSimulatorFrameworkRelativePath = @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/" @"iPhoneSimulatorRemoteClient.framework"; NSString* const kDevToolsFoundationRelativePath = @"../OtherFrameworks/DevToolsFoundation.framework"; NSString* const kSimulatorRelativePath = @"Platforms/iPhoneSimulator.platform/Developer/Applications/" @"iPhone Simulator.app"; // Simulator Error String Key. This can be found by looking in the Simulator's // Localizable.strings files. NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit."; const char* gToolName = "iossim"; // Exit status codes. const int kExitSuccess = EXIT_SUCCESS; const int kExitFailure = EXIT_FAILURE; const int kExitInvalidArguments = 2; const int kExitInitializationFailure = 3; const int kExitAppFailedToStart = 4; const int kExitAppCrashed = 5; void LogError(NSString* format, ...) { va_list list; va_start(list, format); NSString* message = [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]); fflush(stderr); va_end(list); } void LogWarning(NSString* format, ...) { va_list list; va_start(list, format); NSString* message = [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]); fflush(stderr); va_end(list); } } // namespace // A delegate that is called when the simulated app is started or ended in the // simulator. @interface SimulatorDelegate : NSObject { @private NSString* stdioPath_; NSString* developerDir_; NSThread* outputThread_; NSBundle* simulatorBundle_; BOOL appRunning_; } @end // An implementation that copies the simulated app's stdio to stdout of this // executable. While it would be nice to get stdout and stderr independently // from iOS Simulator, issues like I/O buffering and interleaved output // between iOS Simulator and the app would cause iossim to display things out // of order here. Printing all output to a single file keeps the order correct. // Instances of this classe should be initialized with the location of the // simulated app's output file. When the simulated app starts, a thread is // started which handles copying data from the simulated app's output file to // the stdout of this executable. @implementation SimulatorDelegate // Specifies the file locations of the simulated app's stdout and stderr. - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath developerDir:(NSString*)developerDir { self = [super init]; if (self) { stdioPath_ = [stdioPath copy]; developerDir_ = [developerDir copy]; } return self; } - (void)dealloc { [stdioPath_ release]; [developerDir_ release]; [simulatorBundle_ release]; [super dealloc]; } // Reads data from the simulated app's output and writes it to stdout. This // method blocks, so it should be called in a separate thread. The iOS // Simulator takes a file path for the simulated app's stdout and stderr, but // this path isn't always available (e.g. when the stdout is Xcode's build // window). As a workaround, iossim creates a temp file to hold output, which // this method reads and copies to stdout. - (void)tailOutput { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; // Copy data to stdout/stderr while the app is running. NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_]; NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput]; while (appRunning_) { NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init]; [standardOutput writeData:[simio readDataToEndOfFile]]; [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; [innerPool drain]; } // Once the app is no longer running, copy any data that was written during // the last sleep cycle. [standardOutput writeData:[simio readDataToEndOfFile]]; [pool drain]; } // Fetches a localized error string from the Simulator. - (NSString *)localizedSimulatorErrorString:(NSString*)stringKey { // Lazy load of the simulator bundle. if (simulatorBundle_ == nil) { NSString* simulatorPath = [developerDir_ stringByAppendingPathComponent:kSimulatorRelativePath]; simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath]; } NSString *localizedStr = [simulatorBundle_ localizedStringForKey:stringKey value:nil table:nil]; if ([localizedStr length]) return localizedStr; // Failed to get a value, follow Cocoa conventions and use the key as the // string. return stringKey; } - (void)session:(DTiPhoneSimulatorSession*)session didStart:(BOOL)started withError:(NSError*)error { if (!started) { // If the test executes very quickly (<30ms), the SimulatorDelegate may not // get the initial session:started:withError: message indicating successful // startup of the simulated app. Instead the delegate will get a // session:started:withError: message after the timeout has elapsed. To // account for this case, check if the simulated app's stdio file was // ever created and if it exists dump it to stdout and return success. NSFileManager* fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:stdioPath_]) { appRunning_ = NO; [self tailOutput]; // Note that exiting in this state leaves a process running // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will // prevent future simulator sessions from being started for 30 seconds // unless the iOS Simulator application is killed altogether. [self session:session didEndWithError:nil]; // session:didEndWithError should not return (because it exits) so // the execution path should never get here. exit(kExitFailure); } LogError(@"Simulator failed to start: \"%@\" (%@:%ld)", [error localizedDescription], [error domain], static_cast([error code])); exit(kExitAppFailedToStart); } // Start a thread to write contents of outputPath to stdout. appRunning_ = YES; outputThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(tailOutput) object:nil]; [outputThread_ start]; } - (void)session:(DTiPhoneSimulatorSession*)session didEndWithError:(NSError*)error { appRunning_ = NO; // Wait for the output thread to finish copying data to stdout. if (outputThread_) { while (![outputThread_ isFinished]) { [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; } [outputThread_ release]; outputThread_ = nil; } if (error) { // There appears to be a race condition where sometimes the simulator // framework will end with an error, but the error is that the simulated // app cleanly shut down; try to trap this error and don't fail the // simulator run. NSString* localizedDescription = [error localizedDescription]; NSString* ignorableErrorStr = [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey]; if ([ignorableErrorStr isEqual:localizedDescription]) { LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)", localizedDescription, [error domain], static_cast([error code])); } else { LogError(@"Simulator ended with error: \"%@\" (%@:%ld)", localizedDescription, [error domain], static_cast([error code])); exit(kExitFailure); } } // Check if the simulated app exited abnormally by looking for system log // messages from launchd that refer to the simulated app's PID. Limit query // to messages in the last minute since PIDs are cyclical. aslmsg query = asl_new(ASL_TYPE_QUERY); asl_set_query(query, ASL_KEY_SENDER, "launchd", ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING); asl_set_query(query, ASL_KEY_REF_PID, [[[session simulatedApplicationPID] stringValue] UTF8String], ASL_QUERY_OP_EQUAL); asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL); // Log any messages found, and take note of any messages that may indicate the // app crashed or did not exit cleanly. aslresponse response = asl_search(NULL, query); BOOL badEntryFound = NO; aslmsg entry; while ((entry = aslresponse_next(response)) != NULL) { const char* message = asl_get(entry, ASL_KEY_MSG); LogWarning(@"Console message: %s", message); // Some messages are harmless, so don't trigger a failure for them. if (strstr(message, "The following job tried to hijack the service")) continue; badEntryFound = YES; } // If the query returned any nasty-looking results, iossim should exit with // non-zero status. if (badEntryFound) { LogError(@"Simulated app crashed or exited with non-zero status"); exit(kExitAppCrashed); } exit(kExitSuccess); } @end namespace { // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment // variable. NSString* FindDeveloperDir() { // Check the env first. NSDictionary* env = [[NSProcessInfo processInfo] environment]; NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"]; if ([developerDir length] > 0) return developerDir; // Go look for it via xcode-select. NSTask* xcodeSelectTask = [[[NSTask alloc] init] autorelease]; [xcodeSelectTask setLaunchPath:@"/usr/bin/xcode-select"]; [xcodeSelectTask setArguments:[NSArray arrayWithObject:@"-print-path"]]; NSPipe* outputPipe = [NSPipe pipe]; [xcodeSelectTask setStandardOutput:outputPipe]; NSFileHandle* outputFile = [outputPipe fileHandleForReading]; [xcodeSelectTask launch]; NSData* outputData = [outputFile readDataToEndOfFile]; [xcodeSelectTask terminate]; NSString* output = [[[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding] autorelease]; output = [output stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ([output length] == 0) output = nil; return output; } // Loads the Simulator framework from the given developer dir. NSBundle* LoadSimulatorFramework(NSString* developerDir) { // The Simulator framework depends on some of the other Xcode private // frameworks; manually load them first so everything can be linked up. NSString* devToolsFoundationPath = [developerDir stringByAppendingPathComponent:kDevToolsFoundationRelativePath]; NSBundle* devToolsFoundationBundle = [NSBundle bundleWithPath:devToolsFoundationPath]; if (![devToolsFoundationBundle load]) return nil; NSString* simBundlePath = [developerDir stringByAppendingPathComponent:kSimulatorFrameworkRelativePath]; NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath]; if (![simBundle load]) return nil; return simBundle; } // Helper to find a class by name and die if it isn't found. Class FindClassByName(NSString* nameOfClass) { Class theClass = NSClassFromString(nameOfClass); if (!theClass) { LogError(@"Failed to find class %@ at runtime.", nameOfClass); exit(kExitInitializationFailure); } return theClass; } // Converts the given app path to an application spec, which requires an // absolute path. DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) { Class applicationSpecifierClass = FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier"); if (![appPath isAbsolutePath]) { NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath]; appPath = [cwd stringByAppendingPathComponent:appPath]; } appPath = [appPath stringByStandardizingPath]; return [applicationSpecifierClass specifierWithApplicationPath:appPath]; } // Returns the system root for the given SDK version. If sdkVersion is nil, the // default system root is returned. Will return nil if the sdkVersion is not // valid. DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) { Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot"); DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot]; if (sdkVersion) systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion]; return systemRoot; } // Builds a config object for starting the specified app. DTiPhoneSimulatorSessionConfig* BuildSessionConfig( DTiPhoneSimulatorApplicationSpecifier* appSpec, DTiPhoneSimulatorSystemRoot* systemRoot, NSString* stdoutPath, NSString* stderrPath, NSArray* appArgs, NSDictionary* appEnv, NSNumber* deviceFamily) { Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig"); DTiPhoneSimulatorSessionConfig* sessionConfig = [[[sessionConfigClass alloc] init] autorelease]; sessionConfig.applicationToSimulateOnStart = appSpec; sessionConfig.simulatedSystemRoot = systemRoot; sessionConfig.localizedClientName = @"chromium"; sessionConfig.simulatedApplicationStdErrPath = stderrPath; sessionConfig.simulatedApplicationStdOutPath = stdoutPath; sessionConfig.simulatedApplicationLaunchArgs = appArgs; sessionConfig.simulatedApplicationLaunchEnvironment = appEnv; sessionConfig.simulatedDeviceFamily = deviceFamily; return sessionConfig; } // Builds a simulator session that will use the given delegate. DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) { Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession"); DTiPhoneSimulatorSession* session = [[[sessionClass alloc] init] autorelease]; session.delegate = delegate; return session; } // Creates a temporary directory with a unique name based on the provided // template. The template should not contain any path separators and be suffixed // with X's, which will be substituted with a unique alphanumeric string (see // 'man mkdtemp' for details). The directory will be created as a subdirectory // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX', // this method would return something like '/path/to/tempdir/test-3n2'. // // Returns the absolute path of the newly-created directory, or nill if unable // to create a unique directory. NSString* CreateTempDirectory(NSString* dirNameTemplate) { NSString* fullPathTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate]; char* fullPath = mkdtemp(const_cast([fullPathTemplate UTF8String])); if (fullPath == NULL) return nil; return [NSString stringWithUTF8String:fullPath]; } // Creates the necessary directory structure under the given user home directory // path. // Returns YES if successful, NO if unable to create the directories. BOOL CreateHomeDirSubDirs(NSString* userHomePath) { NSFileManager* fileManager = [NSFileManager defaultManager]; // Create user home and subdirectories. NSArray* subDirsToCreate = [NSArray arrayWithObjects: @"Documents", @"Library/Caches", @"Library/Preferences", nil]; for (NSString* subDir in subDirsToCreate) { NSString* path = [userHomePath stringByAppendingPathComponent:subDir]; NSError* error; if (![fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]) { LogError(@"Unable to create directory: %@. Error: %@", path, [error localizedDescription]); return NO; } } return YES; } // Creates the necessary directory structure under the given user home directory // path, then sets the path in the appropriate environment variable. // Returns YES if successful, NO if unable to create or initialize the given // directory. BOOL InitializeSimulatorUserHome(NSString* userHomePath, NSString* deviceName) { if (!CreateHomeDirSubDirs(userHomePath)) return NO; // Set the device to simulate. Note that the iOS Simulator must not be running // for this setting to take effect. CFStringRef iPhoneSimulatorAppID = CFSTR("com.apple.iphonesimulator"); CFPreferencesSetAppValue(CFSTR("SimulateDevice"), deviceName, iPhoneSimulatorAppID); CFPreferencesAppSynchronize(iPhoneSimulatorAppID); // Update the environment to use the specified directory as the user home // directory. // Note: the third param of setenv specifies whether or not to overwrite the // variable's value if it has already been set. if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) || (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) { LogError(@"Unable to set environment variables for home directory."); return NO; } return YES; } // Performs a case-insensitive search to see if |stringToSearch| begins with // |prefixToFind|. Returns true if a match is found. BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch, NSString* prefixToFind) { NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch); NSRange range = [stringToSearch rangeOfString:prefixToFind options:options]; return range.location != NSNotFound; } // Prints the usage information to stderr. void PrintUsage() { fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] " "[-e envKey=value]* [-t startupTimeout] []\n" " where is the path to the .app directory and appArgs are any" " arguments to send the simulated app.\n" "\n" "Options:\n" " -d Specifies the device (must be one of the values from the iOS" " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n" " -s Specifies the SDK version to use (e.g '4.3')." " Will use system default if not specified.\n" " -u Specifies a user home directory for the simulator." " Will create a new directory if not specified.\n" " -e Specifies an environment key=value pair that will be" " set in the simulated application's environment.\n" " -t Specifies the session startup timeout (in seconds)." " Defaults to %d.\n", static_cast(kDefaultSessionStartTimeoutSeconds)); } } // namespace int main(int argc, char* const argv[]) { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; // basename() may modify the passed in string and it returns a pointer to an // internal buffer. Give it a copy to modify, and copy what it returns. char* worker = strdup(argv[0]); char* toolName = basename(worker); if (toolName != NULL) { toolName = strdup(toolName); if (toolName != NULL) gToolName = toolName; } if (worker != NULL) free(worker); NSString* appPath = nil; NSString* appName = nil; NSString* sdkVersion = nil; NSString* deviceName = @"iPhone"; NSString* simHomePath = nil; NSMutableArray* appArgs = [NSMutableArray array]; NSMutableDictionary* appEnv = [NSMutableDictionary dictionary]; NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds; // Parse the optional arguments int c; while ((c = getopt(argc, argv, "hs:d:u:e:t:")) != -1) { switch (c) { case 's': sdkVersion = [NSString stringWithUTF8String:optarg]; break; case 'd': deviceName = [NSString stringWithUTF8String:optarg]; break; case 'u': simHomePath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; break; case 'e': { NSString* envLine = [NSString stringWithUTF8String:optarg]; NSRange range = [envLine rangeOfString:@"="]; if (range.location == NSNotFound) { LogError(@"Invalid key=value argument for -e."); PrintUsage(); exit(kExitInvalidArguments); } NSString* key = [envLine substringToIndex:range.location]; NSString* value = [envLine substringFromIndex:(range.location + 1)]; [appEnv setObject:value forKey:key]; } break; case 't': { int timeout = atoi(optarg); if (timeout > 0) { sessionStartTimeout = static_cast(timeout); } else { LogError(@"Invalid startup timeout (%s).", optarg); PrintUsage(); exit(kExitInvalidArguments); } } break; case 'h': PrintUsage(); exit(kExitSuccess); default: PrintUsage(); exit(kExitInvalidArguments); } } // There should be at least one arg left, specifying the app path. Any // additional args are passed as arguments to the app. if (optind < argc) { appPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[optind] length:strlen(argv[optind])]; appName = [appPath lastPathComponent]; while (++optind < argc) { [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]]; } } else { LogError(@"Unable to parse command line arguments."); PrintUsage(); exit(kExitInvalidArguments); } NSString* developerDir = FindDeveloperDir(); if (!developerDir) { LogError(@"Unable to find developer directory."); exit(kExitInitializationFailure); } NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir); if (!simulatorFramework) { LogError(@"Failed to load the Simulator Framework."); exit(kExitInitializationFailure); } // Make sure the app path provided is legit. DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath); if (!appSpec) { LogError(@"Invalid app path: %@", appPath); exit(kExitInitializationFailure); } // Make sure the SDK path provided is legit (or nil). DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion); if (!systemRoot) { LogError(@"Invalid SDK version: %@", sdkVersion); exit(kExitInitializationFailure); } // Get the paths for stdout and stderr so the simulated app's output will show // up in the caller's stdout/stderr. NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX"); NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"]; // Determine the deviceFamily based on the deviceName NSNumber* deviceFamily = nil; if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) { deviceFamily = [NSNumber numberWithInt:kIPhoneFamily]; } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) { deviceFamily = [NSNumber numberWithInt:kIPadFamily]; } else { LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'", deviceName); exit(kExitInvalidArguments); } // Set up the user home directory for the simulator if (!simHomePath) { NSString* dirNameTemplate = [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName]; simHomePath = CreateTempDirectory(dirNameTemplate); if (!simHomePath) { LogError(@"Unable to create unique directory for template %@", dirNameTemplate); exit(kExitInitializationFailure); } } if (!InitializeSimulatorUserHome(simHomePath, deviceName)) { LogError(@"Unable to initialize home directory for simulator: %@", simHomePath); exit(kExitInitializationFailure); } // Create the config and simulator session. DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec, systemRoot, stdioPath, stdioPath, appArgs, appEnv, deviceFamily); SimulatorDelegate* delegate = [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath developerDir:developerDir] autorelease]; DTiPhoneSimulatorSession* session = BuildSession(delegate); // Start the simulator session. NSError* error; BOOL started = [session requestStartWithConfig:config timeout:sessionStartTimeout error:&error]; // Spin the runtime indefinitely. When the delegate gets the message that the // app has quit it will exit this program. if (started) { [[NSRunLoop mainRunLoop] run]; } else { LogError(@"Simulator failed request to start: \"%@\" (%@:%ld)", [error localizedDescription], [error domain], static_cast([error code])); } // Note that this code is only executed if the simulator fails to start // because once the main run loop is started, only the delegate calling // exit() will end the program. [pool drain]; return kExitFailure; }