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