Home | History | Annotate | Download | only in iossim
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import <Foundation/Foundation.h>
      6 #include <asl.h>
      7 #include <libgen.h>
      8 #include <stdarg.h>
      9 #include <stdio.h>
     10 
     11 // An executable (iossim) that runs an app in the iOS Simulator.
     12 // Run 'iossim -h' for usage information.
     13 //
     14 // For best results, the iOS Simulator application should not be running when
     15 // iossim is invoked.
     16 //
     17 // Headers for the iPhoneSimulatorRemoteClient framework used in this tool are
     18 // generated by class-dump, via GYP.
     19 // (class-dump is available at http://www.codethecode.com/projects/class-dump/)
     20 //
     21 // However, there are some forward declarations required to get things to
     22 // compile. Also, the DTiPhoneSimulatorSessionDelegate protocol is referenced
     23 // by the iPhoneSimulatorRemoteClient framework, but not defined in the object
     24 // file, so it must be defined here before importing the generated
     25 // iPhoneSimulatorRemoteClient.h file.
     26 
     27 @class DTiPhoneSimulatorApplicationSpecifier;
     28 @class DTiPhoneSimulatorSession;
     29 @class DTiPhoneSimulatorSessionConfig;
     30 @class DTiPhoneSimulatorSystemRoot;
     31 
     32 @protocol DTiPhoneSimulatorSessionDelegate
     33 - (void)session:(DTiPhoneSimulatorSession*)session
     34     didEndWithError:(NSError*)error;
     35 - (void)session:(DTiPhoneSimulatorSession*)session
     36        didStart:(BOOL)started
     37       withError:(NSError*)error;
     38 @end
     39 
     40 #import "iPhoneSimulatorRemoteClient.h"
     41 
     42 // An undocumented system log key included in messages from launchd. The value
     43 // is the PID of the process the message is about (as opposed to launchd's PID).
     44 #define ASL_KEY_REF_PID "RefPID"
     45 
     46 namespace {
     47 
     48 // Name of environment variables that control the user's home directory in the
     49 // simulator.
     50 const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME";
     51 const char* const kHomeEnvVariable = "HOME";
     52 
     53 // Device family codes for iPhone and iPad.
     54 const int kIPhoneFamily = 1;
     55 const int kIPadFamily = 2;
     56 
     57 // Max number of seconds to wait for the simulator session to start.
     58 // This timeout must allow time to start up iOS Simulator, install the app
     59 // and perform any other black magic that is encoded in the
     60 // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up
     61 // time is only a couple seconds but machine load, disk caches, etc., can all
     62 // affect startup time in the wild so the timeout needs to be fairly generous.
     63 // If this timeout occurs iossim will likely exit with non-zero status; the
     64 // exception being if the app is invoked and completes execution before the
     65 // session is started (this case is handled in session:didStart:withError).
     66 const NSTimeInterval kDefaultSessionStartTimeoutSeconds = 30;
     67 
     68 // While the simulated app is running, its stdout is redirected to a file which
     69 // is polled by iossim and written to iossim's stdout using the following
     70 // polling interval.
     71 const NSTimeInterval kOutputPollIntervalSeconds = 0.1;
     72 
     73 // The path within the developer dir of the private Simulator frameworks.
     74 NSString* const kSimulatorFrameworkRelativePath =
     75     @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/"
     76     @"iPhoneSimulatorRemoteClient.framework";
     77 NSString* const kDevToolsFoundationRelativePath =
     78     @"../OtherFrameworks/DevToolsFoundation.framework";
     79 NSString* const kSimulatorRelativePath =
     80     @"Platforms/iPhoneSimulator.platform/Developer/Applications/"
     81     @"iPhone Simulator.app";
     82 
     83 // Simulator Error String Key. This can be found by looking in the Simulator's
     84 // Localizable.strings files.
     85 NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit.";
     86 
     87 const char* gToolName = "iossim";
     88 
     89 // Exit status codes.
     90 const int kExitSuccess = EXIT_SUCCESS;
     91 const int kExitFailure = EXIT_FAILURE;
     92 const int kExitInvalidArguments = 2;
     93 const int kExitInitializationFailure = 3;
     94 const int kExitAppFailedToStart = 4;
     95 const int kExitAppCrashed = 5;
     96 
     97 void LogError(NSString* format, ...) {
     98   va_list list;
     99   va_start(list, format);
    100 
    101   NSString* message =
    102       [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
    103 
    104   fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
    105   fflush(stderr);
    106 
    107   va_end(list);
    108 }
    109 
    110 void LogWarning(NSString* format, ...) {
    111   va_list list;
    112   va_start(list, format);
    113 
    114   NSString* message =
    115       [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
    116 
    117   fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
    118   fflush(stderr);
    119 
    120   va_end(list);
    121 }
    122 
    123 }  // namespace
    124 
    125 // A delegate that is called when the simulated app is started or ended in the
    126 // simulator.
    127 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
    128  @private
    129   NSString* stdioPath_;
    130   NSString* developerDir_;
    131   NSThread* outputThread_;
    132   NSBundle* simulatorBundle_;
    133   BOOL appRunning_;
    134 }
    135 @end
    136 
    137 // An implementation that copies the simulated app's stdio to stdout of this
    138 // executable. While it would be nice to get stdout and stderr independently
    139 // from iOS Simulator, issues like I/O buffering and interleaved output
    140 // between iOS Simulator and the app would cause iossim to display things out
    141 // of order here. Printing all output to a single file keeps the order correct.
    142 // Instances of this classe should be initialized with the location of the
    143 // simulated app's output file. When the simulated app starts, a thread is
    144 // started which handles copying data from the simulated app's output file to
    145 // the stdout of this executable.
    146 @implementation SimulatorDelegate
    147 
    148 // Specifies the file locations of the simulated app's stdout and stderr.
    149 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath
    150                            developerDir:(NSString*)developerDir {
    151   self = [super init];
    152   if (self) {
    153     stdioPath_ = [stdioPath copy];
    154     developerDir_ = [developerDir copy];
    155   }
    156 
    157   return self;
    158 }
    159 
    160 - (void)dealloc {
    161   [stdioPath_ release];
    162   [developerDir_ release];
    163   [simulatorBundle_ release];
    164   [super dealloc];
    165 }
    166 
    167 // Reads data from the simulated app's output and writes it to stdout. This
    168 // method blocks, so it should be called in a separate thread. The iOS
    169 // Simulator takes a file path for the simulated app's stdout and stderr, but
    170 // this path isn't always available (e.g. when the stdout is Xcode's build
    171 // window). As a workaround, iossim creates a temp file to hold output, which
    172 // this method reads and copies to stdout.
    173 - (void)tailOutput {
    174   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    175 
    176   // Copy data to stdout/stderr while the app is running.
    177   NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_];
    178   NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput];
    179   while (appRunning_) {
    180     NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init];
    181     [standardOutput writeData:[simio readDataToEndOfFile]];
    182     [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
    183     [innerPool drain];
    184   }
    185 
    186   // Once the app is no longer running, copy any data that was written during
    187   // the last sleep cycle.
    188   [standardOutput writeData:[simio readDataToEndOfFile]];
    189 
    190   [pool drain];
    191 }
    192 
    193 // Fetches a localized error string from the Simulator.
    194 - (NSString *)localizedSimulatorErrorString:(NSString*)stringKey {
    195   // Lazy load of the simulator bundle.
    196   if (simulatorBundle_ == nil) {
    197     NSString* simulatorPath = [developerDir_
    198         stringByAppendingPathComponent:kSimulatorRelativePath];
    199     simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath];
    200   }
    201   NSString *localizedStr =
    202       [simulatorBundle_ localizedStringForKey:stringKey
    203                                         value:nil
    204                                         table:nil];
    205   if ([localizedStr length])
    206     return localizedStr;
    207   // Failed to get a value, follow Cocoa conventions and use the key as the
    208   // string.
    209   return stringKey;
    210 }
    211 
    212 - (void)session:(DTiPhoneSimulatorSession*)session
    213        didStart:(BOOL)started
    214       withError:(NSError*)error {
    215   if (!started) {
    216     // If the test executes very quickly (<30ms), the SimulatorDelegate may not
    217     // get the initial session:started:withError: message indicating successful
    218     // startup of the simulated app. Instead the delegate will get a
    219     // session:started:withError: message after the timeout has elapsed. To
    220     // account for this case, check if the simulated app's stdio file was
    221     // ever created and if it exists dump it to stdout and return success.
    222     NSFileManager* fileManager = [NSFileManager defaultManager];
    223     if ([fileManager fileExistsAtPath:stdioPath_]) {
    224       appRunning_ = NO;
    225       [self tailOutput];
    226       // Note that exiting in this state leaves a process running
    227       // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will
    228       // prevent future simulator sessions from being started for 30 seconds
    229       // unless the iOS Simulator application is killed altogether.
    230       [self session:session didEndWithError:nil];
    231 
    232       // session:didEndWithError should not return (because it exits) so
    233       // the execution path should never get here.
    234       exit(kExitFailure);
    235     }
    236 
    237     LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
    238              [error localizedDescription],
    239              [error domain], static_cast<long int>([error code]));
    240     exit(kExitAppFailedToStart);
    241   }
    242 
    243   // Start a thread to write contents of outputPath to stdout.
    244   appRunning_ = YES;
    245   outputThread_ = [[NSThread alloc] initWithTarget:self
    246                                           selector:@selector(tailOutput)
    247                                             object:nil];
    248   [outputThread_ start];
    249 }
    250 
    251 - (void)session:(DTiPhoneSimulatorSession*)session
    252     didEndWithError:(NSError*)error {
    253   appRunning_ = NO;
    254   // Wait for the output thread to finish copying data to stdout.
    255   if (outputThread_) {
    256     while (![outputThread_ isFinished]) {
    257       [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
    258     }
    259     [outputThread_ release];
    260     outputThread_ = nil;
    261   }
    262 
    263   if (error) {
    264     // There appears to be a race condition where sometimes the simulator
    265     // framework will end with an error, but the error is that the simulated
    266     // app cleanly shut down; try to trap this error and don't fail the
    267     // simulator run.
    268     NSString* localizedDescription = [error localizedDescription];
    269     NSString* ignorableErrorStr =
    270         [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey];
    271     if ([ignorableErrorStr isEqual:localizedDescription]) {
    272       LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)",
    273                  localizedDescription, [error domain],
    274                  static_cast<long int>([error code]));
    275     } else {
    276       LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
    277                localizedDescription, [error domain],
    278                static_cast<long int>([error code]));
    279       exit(kExitFailure);
    280     }
    281   }
    282 
    283   // Check if the simulated app exited abnormally by looking for system log
    284   // messages from launchd that refer to the simulated app's PID. Limit query
    285   // to messages in the last minute since PIDs are cyclical.
    286   aslmsg query = asl_new(ASL_TYPE_QUERY);
    287   asl_set_query(query, ASL_KEY_SENDER, "launchd",
    288                 ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING);
    289   asl_set_query(query, ASL_KEY_REF_PID,
    290                 [[[session simulatedApplicationPID] stringValue] UTF8String],
    291                 ASL_QUERY_OP_EQUAL);
    292   asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL);
    293 
    294   // Log any messages found, and take note of any messages that may indicate the
    295   // app crashed or did not exit cleanly.
    296   aslresponse response = asl_search(NULL, query);
    297   BOOL badEntryFound = NO;
    298   aslmsg entry;
    299   while ((entry = aslresponse_next(response)) != NULL) {
    300     const char* message = asl_get(entry, ASL_KEY_MSG);
    301     LogWarning(@"Console message: %s", message);
    302     // Some messages are harmless, so don't trigger a failure for them.
    303     if (strstr(message, "The following job tried to hijack the service"))
    304       continue;
    305     badEntryFound = YES;
    306   }
    307 
    308   // If the query returned any nasty-looking results, iossim should exit with
    309   // non-zero status.
    310   if (badEntryFound) {
    311     LogError(@"Simulated app crashed or exited with non-zero status");
    312     exit(kExitAppCrashed);
    313   }
    314   exit(kExitSuccess);
    315 }
    316 @end
    317 
    318 namespace {
    319 
    320 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
    321 // variable.
    322 NSString* FindDeveloperDir() {
    323   // Check the env first.
    324   NSDictionary* env = [[NSProcessInfo processInfo] environment];
    325   NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"];
    326   if ([developerDir length] > 0)
    327     return developerDir;
    328 
    329   // Go look for it via xcode-select.
    330   NSTask* xcodeSelectTask = [[[NSTask alloc] init] autorelease];
    331   [xcodeSelectTask setLaunchPath:@"/usr/bin/xcode-select"];
    332   [xcodeSelectTask setArguments:[NSArray arrayWithObject:@"-print-path"]];
    333 
    334   NSPipe* outputPipe = [NSPipe pipe];
    335   [xcodeSelectTask setStandardOutput:outputPipe];
    336   NSFileHandle* outputFile = [outputPipe fileHandleForReading];
    337 
    338   [xcodeSelectTask launch];
    339   NSData* outputData = [outputFile readDataToEndOfFile];
    340   [xcodeSelectTask terminate];
    341 
    342   NSString* output =
    343       [[[NSString alloc] initWithData:outputData
    344                              encoding:NSUTF8StringEncoding] autorelease];
    345   output = [output stringByTrimmingCharactersInSet:
    346       [NSCharacterSet whitespaceAndNewlineCharacterSet]];
    347   if ([output length] == 0)
    348     output = nil;
    349   return output;
    350 }
    351 
    352 // Loads the Simulator framework from the given developer dir.
    353 NSBundle* LoadSimulatorFramework(NSString* developerDir) {
    354   // The Simulator framework depends on some of the other Xcode private
    355   // frameworks; manually load them first so everything can be linked up.
    356   NSString* devToolsFoundationPath = [developerDir
    357       stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
    358   NSBundle* devToolsFoundationBundle =
    359       [NSBundle bundleWithPath:devToolsFoundationPath];
    360   if (![devToolsFoundationBundle load])
    361     return nil;
    362   NSString* simBundlePath = [developerDir
    363       stringByAppendingPathComponent:kSimulatorFrameworkRelativePath];
    364   NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
    365   if (![simBundle load])
    366     return nil;
    367   return simBundle;
    368 }
    369 
    370 // Helper to find a class by name and die if it isn't found.
    371 Class FindClassByName(NSString* nameOfClass) {
    372   Class theClass = NSClassFromString(nameOfClass);
    373   if (!theClass) {
    374     LogError(@"Failed to find class %@ at runtime.", nameOfClass);
    375     exit(kExitInitializationFailure);
    376   }
    377   return theClass;
    378 }
    379 
    380 // Converts the given app path to an application spec, which requires an
    381 // absolute path.
    382 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) {
    383   Class applicationSpecifierClass =
    384       FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier");
    385   if (![appPath isAbsolutePath]) {
    386     NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath];
    387     appPath = [cwd stringByAppendingPathComponent:appPath];
    388   }
    389   appPath = [appPath stringByStandardizingPath];
    390   return [applicationSpecifierClass specifierWithApplicationPath:appPath];
    391 }
    392 
    393 // Returns the system root for the given SDK version. If sdkVersion is nil, the
    394 // default system root is returned.  Will return nil if the sdkVersion is not
    395 // valid.
    396 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
    397   Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
    398   DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
    399   if (sdkVersion)
    400     systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
    401 
    402   return systemRoot;
    403 }
    404 
    405 // Builds a config object for starting the specified app.
    406 DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
    407     DTiPhoneSimulatorApplicationSpecifier* appSpec,
    408     DTiPhoneSimulatorSystemRoot* systemRoot,
    409     NSString* stdoutPath,
    410     NSString* stderrPath,
    411     NSArray* appArgs,
    412     NSDictionary* appEnv,
    413     NSNumber* deviceFamily) {
    414   Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig");
    415   DTiPhoneSimulatorSessionConfig* sessionConfig =
    416       [[[sessionConfigClass alloc] init] autorelease];
    417   sessionConfig.applicationToSimulateOnStart = appSpec;
    418   sessionConfig.simulatedSystemRoot = systemRoot;
    419   sessionConfig.localizedClientName = @"chromium";
    420   sessionConfig.simulatedApplicationStdErrPath = stderrPath;
    421   sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
    422   sessionConfig.simulatedApplicationLaunchArgs = appArgs;
    423   sessionConfig.simulatedApplicationLaunchEnvironment = appEnv;
    424   sessionConfig.simulatedDeviceFamily = deviceFamily;
    425   return sessionConfig;
    426 }
    427 
    428 // Builds a simulator session that will use the given delegate.
    429 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
    430   Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
    431   DTiPhoneSimulatorSession* session =
    432       [[[sessionClass alloc] init] autorelease];
    433   session.delegate = delegate;
    434   return session;
    435 }
    436 
    437 // Creates a temporary directory with a unique name based on the provided
    438 // template. The template should not contain any path separators and be suffixed
    439 // with X's, which will be substituted with a unique alphanumeric string (see
    440 // 'man mkdtemp' for details). The directory will be created as a subdirectory
    441 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
    442 // this method would return something like '/path/to/tempdir/test-3n2'.
    443 //
    444 // Returns the absolute path of the newly-created directory, or nill if unable
    445 // to create a unique directory.
    446 NSString* CreateTempDirectory(NSString* dirNameTemplate) {
    447   NSString* fullPathTemplate =
    448       [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
    449   char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
    450   if (fullPath == NULL)
    451     return nil;
    452 
    453   return [NSString stringWithUTF8String:fullPath];
    454 }
    455 
    456 // Creates the necessary directory structure under the given user home directory
    457 // path.
    458 // Returns YES if successful, NO if unable to create the directories.
    459 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
    460   NSFileManager* fileManager = [NSFileManager defaultManager];
    461 
    462   // Create user home and subdirectories.
    463   NSArray* subDirsToCreate = [NSArray arrayWithObjects:
    464                               @"Documents",
    465                               @"Library/Caches",
    466                               @"Library/Preferences",
    467                               nil];
    468   for (NSString* subDir in subDirsToCreate) {
    469     NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
    470     NSError* error;
    471     if (![fileManager createDirectoryAtPath:path
    472                 withIntermediateDirectories:YES
    473                                  attributes:nil
    474                                       error:&error]) {
    475       LogError(@"Unable to create directory: %@. Error: %@",
    476                path, [error localizedDescription]);
    477       return NO;
    478     }
    479   }
    480 
    481   return YES;
    482 }
    483 
    484 // Creates the necessary directory structure under the given user home directory
    485 // path, then sets the path in the appropriate environment variable.
    486 // Returns YES if successful, NO if unable to create or initialize the given
    487 // directory.
    488 BOOL InitializeSimulatorUserHome(NSString* userHomePath, NSString* deviceName) {
    489   if (!CreateHomeDirSubDirs(userHomePath))
    490     return NO;
    491 
    492   // Set the device to simulate. Note that the iOS Simulator must not be running
    493   // for this setting to take effect.
    494   CFStringRef iPhoneSimulatorAppID = CFSTR("com.apple.iphonesimulator");
    495   CFPreferencesSetAppValue(CFSTR("SimulateDevice"),
    496                            deviceName,
    497                            iPhoneSimulatorAppID);
    498   CFPreferencesAppSynchronize(iPhoneSimulatorAppID);
    499 
    500   // Update the environment to use the specified directory as the user home
    501   // directory.
    502   // Note: the third param of setenv specifies whether or not to overwrite the
    503   // variable's value if it has already been set.
    504   if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
    505       (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
    506     LogError(@"Unable to set environment variables for home directory.");
    507     return NO;
    508   }
    509 
    510   return YES;
    511 }
    512 
    513 // Performs a case-insensitive search to see if |stringToSearch| begins with
    514 // |prefixToFind|. Returns true if a match is found.
    515 BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch,
    516                                  NSString* prefixToFind) {
    517   NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch);
    518   NSRange range = [stringToSearch rangeOfString:prefixToFind
    519                                         options:options];
    520   return range.location != NSNotFound;
    521 }
    522 
    523 // Prints the usage information to stderr.
    524 void PrintUsage() {
    525   fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
    526       "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n"
    527       "  where <appPath> is the path to the .app directory and appArgs are any"
    528       " arguments to send the simulated app.\n"
    529       "\n"
    530       "Options:\n"
    531       "  -d  Specifies the device (must be one of the values from the iOS"
    532       " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n"
    533       "  -s  Specifies the SDK version to use (e.g '4.3')."
    534       " Will use system default if not specified.\n"
    535       "  -u  Specifies a user home directory for the simulator."
    536       " Will create a new directory if not specified.\n"
    537       "  -e  Specifies an environment key=value pair that will be"
    538       " set in the simulated application's environment.\n"
    539       "  -t  Specifies the session startup timeout (in seconds)."
    540       " Defaults to %d.\n",
    541       static_cast<int>(kDefaultSessionStartTimeoutSeconds));
    542 }
    543 
    544 }  // namespace
    545 
    546 int main(int argc, char* const argv[]) {
    547   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    548 
    549   // basename() may modify the passed in string and it returns a pointer to an
    550   // internal buffer. Give it a copy to modify, and copy what it returns.
    551   char* worker = strdup(argv[0]);
    552   char* toolName = basename(worker);
    553   if (toolName != NULL) {
    554     toolName = strdup(toolName);
    555     if (toolName != NULL)
    556       gToolName = toolName;
    557   }
    558   if (worker != NULL)
    559     free(worker);
    560 
    561   NSString* appPath = nil;
    562   NSString* appName = nil;
    563   NSString* sdkVersion = nil;
    564   NSString* deviceName = @"iPhone";
    565   NSString* simHomePath = nil;
    566   NSMutableArray* appArgs = [NSMutableArray array];
    567   NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
    568   NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
    569 
    570   // Parse the optional arguments
    571   int c;
    572   while ((c = getopt(argc, argv, "hs:d:u:e:t:")) != -1) {
    573     switch (c) {
    574       case 's':
    575         sdkVersion = [NSString stringWithUTF8String:optarg];
    576         break;
    577       case 'd':
    578         deviceName = [NSString stringWithUTF8String:optarg];
    579         break;
    580       case 'u':
    581         simHomePath = [[NSFileManager defaultManager]
    582             stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
    583         break;
    584       case 'e': {
    585         NSString* envLine = [NSString stringWithUTF8String:optarg];
    586         NSRange range = [envLine rangeOfString:@"="];
    587         if (range.location == NSNotFound) {
    588           LogError(@"Invalid key=value argument for -e.");
    589           PrintUsage();
    590           exit(kExitInvalidArguments);
    591         }
    592         NSString* key = [envLine substringToIndex:range.location];
    593         NSString* value = [envLine substringFromIndex:(range.location + 1)];
    594         [appEnv setObject:value forKey:key];
    595       }
    596         break;
    597       case 't': {
    598         int timeout = atoi(optarg);
    599         if (timeout > 0) {
    600           sessionStartTimeout = static_cast<NSTimeInterval>(timeout);
    601         } else {
    602           LogError(@"Invalid startup timeout (%s).", optarg);
    603           PrintUsage();
    604           exit(kExitInvalidArguments);
    605         }
    606       }
    607         break;
    608       case 'h':
    609         PrintUsage();
    610         exit(kExitSuccess);
    611       default:
    612         PrintUsage();
    613         exit(kExitInvalidArguments);
    614     }
    615   }
    616 
    617   // There should be at least one arg left, specifying the app path. Any
    618   // additional args are passed as arguments to the app.
    619   if (optind < argc) {
    620     appPath = [[NSFileManager defaultManager]
    621         stringWithFileSystemRepresentation:argv[optind]
    622                                     length:strlen(argv[optind])];
    623     appName = [appPath lastPathComponent];
    624     while (++optind < argc) {
    625       [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
    626     }
    627   } else {
    628     LogError(@"Unable to parse command line arguments.");
    629     PrintUsage();
    630     exit(kExitInvalidArguments);
    631   }
    632 
    633   NSString* developerDir = FindDeveloperDir();
    634   if (!developerDir) {
    635     LogError(@"Unable to find developer directory.");
    636     exit(kExitInitializationFailure);
    637   }
    638 
    639   NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
    640   if (!simulatorFramework) {
    641     LogError(@"Failed to load the Simulator Framework.");
    642     exit(kExitInitializationFailure);
    643   }
    644 
    645   // Make sure the app path provided is legit.
    646   DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
    647   if (!appSpec) {
    648     LogError(@"Invalid app path: %@", appPath);
    649     exit(kExitInitializationFailure);
    650   }
    651 
    652   // Make sure the SDK path provided is legit (or nil).
    653   DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
    654   if (!systemRoot) {
    655     LogError(@"Invalid SDK version: %@", sdkVersion);
    656     exit(kExitInitializationFailure);
    657   }
    658 
    659   // Get the paths for stdout and stderr so the simulated app's output will show
    660   // up in the caller's stdout/stderr.
    661   NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
    662   NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
    663 
    664   // Determine the deviceFamily based on the deviceName
    665   NSNumber* deviceFamily = nil;
    666   if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) {
    667     deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
    668   } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) {
    669     deviceFamily = [NSNumber numberWithInt:kIPadFamily];
    670   } else {
    671     LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'",
    672              deviceName);
    673     exit(kExitInvalidArguments);
    674   }
    675 
    676   // Set up the user home directory for the simulator
    677   if (!simHomePath) {
    678     NSString* dirNameTemplate =
    679         [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName];
    680     simHomePath = CreateTempDirectory(dirNameTemplate);
    681     if (!simHomePath) {
    682       LogError(@"Unable to create unique directory for template %@",
    683                dirNameTemplate);
    684       exit(kExitInitializationFailure);
    685     }
    686   }
    687   if (!InitializeSimulatorUserHome(simHomePath, deviceName)) {
    688     LogError(@"Unable to initialize home directory for simulator: %@",
    689              simHomePath);
    690     exit(kExitInitializationFailure);
    691   }
    692 
    693   // Create the config and simulator session.
    694   DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
    695                                                               systemRoot,
    696                                                               stdioPath,
    697                                                               stdioPath,
    698                                                               appArgs,
    699                                                               appEnv,
    700                                                               deviceFamily);
    701   SimulatorDelegate* delegate =
    702       [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
    703                                        developerDir:developerDir] autorelease];
    704   DTiPhoneSimulatorSession* session = BuildSession(delegate);
    705 
    706   // Start the simulator session.
    707   NSError* error;
    708   BOOL started = [session requestStartWithConfig:config
    709                                          timeout:sessionStartTimeout
    710                                            error:&error];
    711 
    712   // Spin the runtime indefinitely. When the delegate gets the message that the
    713   // app has quit it will exit this program.
    714   if (started) {
    715     [[NSRunLoop mainRunLoop] run];
    716   } else {
    717     LogError(@"Simulator failed request to start:  \"%@\" (%@:%ld)",
    718              [error localizedDescription],
    719              [error domain], static_cast<long int>([error code]));
    720   }
    721 
    722   // Note that this code is only executed if the simulator fails to start
    723   // because once the main run loop is started, only the delegate calling
    724   // exit() will end the program.
    725   [pool drain];
    726   return kExitFailure;
    727 }
    728