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 NSString* const kDVTFoundationRelativePath =
    105     @"../SharedFrameworks/DVTFoundation.framework";
    106 NSString* const kDevToolsFoundationRelativePath =
    107     @"../OtherFrameworks/DevToolsFoundation.framework";
    108 NSString* const kSimulatorRelativePath =
    109     @"Platforms/iPhoneSimulator.platform/Developer/Applications/"
    110     @"iPhone Simulator.app";
    111 
    112 // Simulator Error String Key. This can be found by looking in the Simulator's
    113 // Localizable.strings files.
    114 NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit.";
    115 
    116 const char* gToolName = "iossim";
    117 
    118 // Exit status codes.
    119 const int kExitSuccess = EXIT_SUCCESS;
    120 const int kExitFailure = EXIT_FAILURE;
    121 const int kExitInvalidArguments = 2;
    122 const int kExitInitializationFailure = 3;
    123 const int kExitAppFailedToStart = 4;
    124 const int kExitAppCrashed = 5;
    125 const int kExitUnsupportedXcodeVersion = 6;
    126 
    127 void LogError(NSString* format, ...) {
    128   va_list list;
    129   va_start(list, format);
    130 
    131   NSString* message =
    132       [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
    133 
    134   fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
    135   fflush(stderr);
    136 
    137   va_end(list);
    138 }
    139 
    140 void LogWarning(NSString* format, ...) {
    141   va_list list;
    142   va_start(list, format);
    143 
    144   NSString* message =
    145       [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
    146 
    147   fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
    148   fflush(stderr);
    149 
    150   va_end(list);
    151 }
    152 
    153 // Helper to find a class by name and die if it isn't found.
    154 Class FindClassByName(NSString* nameOfClass) {
    155   Class theClass = NSClassFromString(nameOfClass);
    156   if (!theClass) {
    157     LogError(@"Failed to find class %@ at runtime.", nameOfClass);
    158     exit(kExitInitializationFailure);
    159   }
    160   return theClass;
    161 }
    162 
    163 // Returns the a NSString containing the stdout from running an NSTask that
    164 // launches |toolPath| with th given command line |args|.
    165 NSString* GetOutputFromTask(NSString* toolPath, NSArray* args) {
    166   NSTask* task = [[[NSTask alloc] init] autorelease];
    167   [task setLaunchPath:toolPath];
    168   [task setArguments:args];
    169   NSPipe* outputPipe = [NSPipe pipe];
    170   [task setStandardOutput:outputPipe];
    171   NSFileHandle* outputFile = [outputPipe fileHandleForReading];
    172 
    173   [task launch];
    174   NSData* outputData = [outputFile readDataToEndOfFile];
    175   [task waitUntilExit];
    176   if ([task isRunning]) {
    177     LogError(@"Task '%@ %@' is still running.",
    178         toolPath,
    179         [args componentsJoinedByString:@" "]);
    180     return nil;
    181   } else if ([task terminationStatus]) {
    182     LogError(@"Task '%@ %@' exited with return code %d.",
    183         toolPath,
    184         [args componentsJoinedByString:@" "],
    185         [task terminationStatus]);
    186     return nil;
    187   }
    188   return [[[NSString alloc] initWithData:outputData
    189                                 encoding:NSUTF8StringEncoding] autorelease];
    190 }
    191 
    192 // Finds the Xcode version via xcodebuild -version. Output from xcodebuild is
    193 // expected to look like:
    194 //   Xcode <version>
    195 //   Build version 5B130a
    196 // where <version> is the string returned by this function (e.g. 6.0).
    197 NSString* FindXcodeVersion() {
    198   NSString* output = GetOutputFromTask(@"/usr/bin/xcodebuild",
    199                                        @[ @"-version" ]);
    200   // Scan past the "Xcode ", then scan the rest of the line into |version|.
    201   NSScanner* scanner = [NSScanner scannerWithString:output];
    202   BOOL valid = [scanner scanString:@"Xcode " intoString:NULL];
    203   NSString* version;
    204   valid =
    205       [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
    206                               intoString:&version];
    207   if (!valid) {
    208     LogError(@"Unable to find Xcode version. 'xcodebuild -version' "
    209              @"returned \n%@", output);
    210     return nil;
    211   }
    212   return version;
    213 }
    214 
    215 // Returns true if iossim is running with Xcode 6 or later installed on the
    216 // host.
    217 BOOL IsRunningWithXcode6OrLater() {
    218   static NSString* xcodeVersion = FindXcodeVersion();
    219   if (!xcodeVersion) {
    220     return false;
    221   }
    222   NSArray* components = [xcodeVersion componentsSeparatedByString:@"."];
    223   if ([components count] < 1) {
    224     return false;
    225   }
    226   NSInteger majorVersion = [[components objectAtIndex:0] integerValue];
    227   return majorVersion >= 6;
    228 }
    229 
    230 // Prints supported devices and SDKs.
    231 void PrintSupportedDevices() {
    232   if (IsRunningWithXcode6OrLater()) {
    233 #if defined(IOSSIM_USE_XCODE_6)
    234     printf("Supported device/SDK combinations:\n");
    235     Class simDeviceSetClass = FindClassByName(@"SimDeviceSet");
    236     id deviceSet =
    237         [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]];
    238     for (id simDevice in [deviceSet availableDevices]) {
    239       NSString* deviceInfo =
    240           [NSString stringWithFormat:@"  -d '%@' -s '%@'\n",
    241               [simDevice name], [[simDevice runtime] versionString]];
    242       printf("%s", [deviceInfo UTF8String]);
    243     }
    244 #endif  // IOSSIM_USE_XCODE_6
    245   } else {
    246     printf("Supported SDK versions:\n");
    247     Class rootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
    248     for (id root in [rootClass knownRoots]) {
    249       printf("  '%s'\n", [[root sdkVersion] UTF8String]);
    250     }
    251     // This is the list of devices supported on Xcode 5.1.x.
    252     printf("Supported devices:\n");
    253     printf("  'iPhone'\n");
    254     printf("  'iPhone Retina (3.5-inch)'\n");
    255     printf("  'iPhone Retina (4-inch)'\n");
    256     printf("  'iPhone Retina (4-inch 64-bit)'\n");
    257     printf("  'iPad'\n");
    258     printf("  'iPad Retina'\n");
    259     printf("  'iPad Retina (64-bit)'\n");
    260   }
    261 }
    262 }  // namespace
    263 
    264 // A delegate that is called when the simulated app is started or ended in the
    265 // simulator.
    266 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
    267  @private
    268   NSString* stdioPath_;
    269   NSString* developerDir_;
    270   NSString* simulatorHome_;
    271   NSThread* outputThread_;
    272   NSBundle* simulatorBundle_;
    273   BOOL appRunning_;
    274 }
    275 @end
    276 
    277 // An implementation that copies the simulated app's stdio to stdout of this
    278 // executable. While it would be nice to get stdout and stderr independently
    279 // from iOS Simulator, issues like I/O buffering and interleaved output
    280 // between iOS Simulator and the app would cause iossim to display things out
    281 // of order here. Printing all output to a single file keeps the order correct.
    282 // Instances of this classe should be initialized with the location of the
    283 // simulated app's output file. When the simulated app starts, a thread is
    284 // started which handles copying data from the simulated app's output file to
    285 // the stdout of this executable.
    286 @implementation SimulatorDelegate
    287 
    288 // Specifies the file locations of the simulated app's stdout and stderr.
    289 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath
    290                            developerDir:(NSString*)developerDir
    291                           simulatorHome:(NSString*)simulatorHome {
    292   self = [super init];
    293   if (self) {
    294     stdioPath_ = [stdioPath copy];
    295     developerDir_ = [developerDir copy];
    296     simulatorHome_ = [simulatorHome copy];
    297   }
    298 
    299   return self;
    300 }
    301 
    302 - (void)dealloc {
    303   [stdioPath_ release];
    304   [developerDir_ release];
    305   [simulatorBundle_ release];
    306   [super dealloc];
    307 }
    308 
    309 // Reads data from the simulated app's output and writes it to stdout. This
    310 // method blocks, so it should be called in a separate thread. The iOS
    311 // Simulator takes a file path for the simulated app's stdout and stderr, but
    312 // this path isn't always available (e.g. when the stdout is Xcode's build
    313 // window). As a workaround, iossim creates a temp file to hold output, which
    314 // this method reads and copies to stdout.
    315 - (void)tailOutputForSession:(DTiPhoneSimulatorSession*)session {
    316   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    317 
    318   NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_];
    319   if (IsRunningWithXcode6OrLater()) {
    320 #if defined(IOSSIM_USE_XCODE_6)
    321     // With iOS 8 simulators on Xcode 6, the app output is relative to the
    322     // simulator's data directory.
    323     if ([session.sessionConfig.simulatedSystemRoot.sdkVersion isEqual:@"8.0"]) {
    324       NSString* dataPath = session.sessionConfig.device.dataPath;
    325       NSString* appOutput =
    326           [dataPath stringByAppendingPathComponent:stdioPath_];
    327       simio = [NSFileHandle fileHandleForReadingAtPath:appOutput];
    328     }
    329 #endif  // IOSSIM_USE_XCODE_6
    330   }
    331   NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput];
    332   // Copy data to stdout/stderr while the app is running.
    333   while (appRunning_) {
    334     NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init];
    335     [standardOutput writeData:[simio readDataToEndOfFile]];
    336     [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
    337     [innerPool drain];
    338   }
    339 
    340   // Once the app is no longer running, copy any data that was written during
    341   // the last sleep cycle.
    342   [standardOutput writeData:[simio readDataToEndOfFile]];
    343 
    344   [pool drain];
    345 }
    346 
    347 // Fetches a localized error string from the Simulator.
    348 - (NSString *)localizedSimulatorErrorString:(NSString*)stringKey {
    349   // Lazy load of the simulator bundle.
    350   if (simulatorBundle_ == nil) {
    351     NSString* simulatorPath = [developerDir_
    352         stringByAppendingPathComponent:kSimulatorRelativePath];
    353     simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath];
    354   }
    355   NSString *localizedStr =
    356       [simulatorBundle_ localizedStringForKey:stringKey
    357                                         value:nil
    358                                         table:nil];
    359   if ([localizedStr length])
    360     return localizedStr;
    361   // Failed to get a value, follow Cocoa conventions and use the key as the
    362   // string.
    363   return stringKey;
    364 }
    365 
    366 - (void)session:(DTiPhoneSimulatorSession*)session
    367        didStart:(BOOL)started
    368       withError:(NSError*)error {
    369   if (!started) {
    370     // If the test executes very quickly (<30ms), the SimulatorDelegate may not
    371     // get the initial session:started:withError: message indicating successful
    372     // startup of the simulated app. Instead the delegate will get a
    373     // session:started:withError: message after the timeout has elapsed. To
    374     // account for this case, check if the simulated app's stdio file was
    375     // ever created and if it exists dump it to stdout and return success.
    376     NSFileManager* fileManager = [NSFileManager defaultManager];
    377     if ([fileManager fileExistsAtPath:stdioPath_]) {
    378       appRunning_ = NO;
    379       [self tailOutputForSession:session];
    380       // Note that exiting in this state leaves a process running
    381       // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will
    382       // prevent future simulator sessions from being started for 30 seconds
    383       // unless the iOS Simulator application is killed altogether.
    384       [self session:session didEndWithError:nil];
    385 
    386       // session:didEndWithError should not return (because it exits) so
    387       // the execution path should never get here.
    388       exit(kExitFailure);
    389     }
    390 
    391     LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
    392              [error localizedDescription],
    393              [error domain], static_cast<long int>([error code]));
    394     PrintSupportedDevices();
    395     exit(kExitAppFailedToStart);
    396   }
    397 
    398   // Start a thread to write contents of outputPath to stdout.
    399   appRunning_ = YES;
    400   outputThread_ =
    401       [[NSThread alloc] initWithTarget:self
    402                               selector:@selector(tailOutputForSession:)
    403                                 object:session];
    404   [outputThread_ start];
    405 }
    406 
    407 - (void)session:(DTiPhoneSimulatorSession*)session
    408     didEndWithError:(NSError*)error {
    409   appRunning_ = NO;
    410   // Wait for the output thread to finish copying data to stdout.
    411   if (outputThread_) {
    412     while (![outputThread_ isFinished]) {
    413       [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
    414     }
    415     [outputThread_ release];
    416     outputThread_ = nil;
    417   }
    418 
    419   if (error) {
    420     // There appears to be a race condition where sometimes the simulator
    421     // framework will end with an error, but the error is that the simulated
    422     // app cleanly shut down; try to trap this error and don't fail the
    423     // simulator run.
    424     NSString* localizedDescription = [error localizedDescription];
    425     NSString* ignorableErrorStr =
    426         [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey];
    427     if ([ignorableErrorStr isEqual:localizedDescription]) {
    428       LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)",
    429                  localizedDescription, [error domain],
    430                  static_cast<long int>([error code]));
    431     } else {
    432       LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
    433                localizedDescription, [error domain],
    434                static_cast<long int>([error code]));
    435       exit(kExitFailure);
    436     }
    437   }
    438 
    439   // Try to determine if the simulated app crashed or quit with a non-zero
    440   // status code. iOS Simluator handles things a bit differently depending on
    441   // the version, so first determine the iOS version being used.
    442   BOOL badEntryFound = NO;
    443   NSString* versionString =
    444       [[[session sessionConfig] simulatedSystemRoot] sdkVersion];
    445   NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."]
    446       objectAtIndex:0] intValue];
    447   if (majorVersion <= 6) {
    448     // In iOS 6 and before, logging from the simulated apps went to the main
    449     // system logs, so use ASL to check if the simulated app exited abnormally
    450     // by looking for system log messages from launchd that refer to the
    451     // simulated app's PID. Limit query to messages in the last minute since
    452     // PIDs are cyclical.
    453     aslmsg query = asl_new(ASL_TYPE_QUERY);
    454     asl_set_query(query, ASL_KEY_SENDER, "launchd",
    455                   ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING);
    456     char session_id[20];
    457     if (snprintf(session_id, 20, "%d", [session simulatedApplicationPID]) < 0) {
    458       LogError(@"Failed to get [session simulatedApplicationPID]");
    459       exit(kExitFailure);
    460     }
    461     asl_set_query(query, ASL_KEY_REF_PID, session_id, ASL_QUERY_OP_EQUAL);
    462     asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL);
    463 
    464     // Log any messages found, and take note of any messages that may indicate
    465     // the app crashed or did not exit cleanly.
    466     aslresponse response = asl_search(NULL, query);
    467     aslmsg entry;
    468     while ((entry = aslresponse_next(response)) != NULL) {
    469       const char* message = asl_get(entry, ASL_KEY_MSG);
    470       LogWarning(@"Console message: %s", message);
    471       // Some messages are harmless, so don't trigger a failure for them.
    472       if (strstr(message, "The following job tried to hijack the service"))
    473         continue;
    474       badEntryFound = YES;
    475     }
    476   } else {
    477     // Otherwise, the iOS Simulator's system logging is sandboxed, so parse the
    478     // sandboxed system.log file for known errors.
    479     NSString* path;
    480     if (IsRunningWithXcode6OrLater()) {
    481 #if defined(IOSSIM_USE_XCODE_6)
    482       NSString* dataPath = session.sessionConfig.device.dataPath;
    483       path =
    484           [dataPath stringByAppendingPathComponent:@"Library/Logs/system.log"];
    485 #endif  // IOSSIM_USE_XCODE_6
    486     } else {
    487       NSString* relativePathToSystemLog =
    488           [NSString stringWithFormat:
    489               @"Library/Logs/iOS Simulator/%@/system.log", versionString];
    490       path = [simulatorHome_
    491           stringByAppendingPathComponent:relativePathToSystemLog];
    492     }
    493     NSFileManager* fileManager = [NSFileManager defaultManager];
    494     if ([fileManager fileExistsAtPath:path]) {
    495       NSString* content =
    496           [NSString stringWithContentsOfFile:path
    497                                     encoding:NSUTF8StringEncoding
    498                                        error:NULL];
    499       NSArray* lines = [content componentsSeparatedByCharactersInSet:
    500           [NSCharacterSet newlineCharacterSet]];
    501       NSString* simulatedAppPID =
    502           [NSString stringWithFormat:@"%d", session.simulatedApplicationPID];
    503       for (NSString* line in lines) {
    504         NSString* const kErrorString = @"Service exited with abnormal code:";
    505         if ([line rangeOfString:kErrorString].location != NSNotFound &&
    506             [line rangeOfString:simulatedAppPID].location != NSNotFound) {
    507           LogWarning(@"Console message: %@", line);
    508           badEntryFound = YES;
    509           break;
    510         }
    511       }
    512       // Remove the log file so subsequent invocations of iossim won't be
    513       // looking at stale logs.
    514       remove([path fileSystemRepresentation]);
    515     } else {
    516         LogWarning(@"Unable to find system log at '%@'.", path);
    517     }
    518   }
    519 
    520   // If the query returned any nasty-looking results, iossim should exit with
    521   // non-zero status.
    522   if (badEntryFound) {
    523     LogError(@"Simulated app crashed or exited with non-zero status");
    524     exit(kExitAppCrashed);
    525   }
    526   exit(kExitSuccess);
    527 }
    528 @end
    529 
    530 namespace {
    531 
    532 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
    533 // variable.
    534 NSString* FindDeveloperDir() {
    535   // Check the env first.
    536   NSDictionary* env = [[NSProcessInfo processInfo] environment];
    537   NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"];
    538   if ([developerDir length] > 0)
    539     return developerDir;
    540 
    541   // Go look for it via xcode-select.
    542   NSString* output = GetOutputFromTask(@"/usr/bin/xcode-select",
    543                                        @[ @"-print-path" ]);
    544   output = [output stringByTrimmingCharactersInSet:
    545       [NSCharacterSet whitespaceAndNewlineCharacterSet]];
    546   if ([output length] == 0)
    547     output = nil;
    548   return output;
    549 }
    550 
    551 // Loads the Simulator framework from the given developer dir.
    552 NSBundle* LoadSimulatorFramework(NSString* developerDir) {
    553   // The Simulator framework depends on some of the other Xcode private
    554   // frameworks; manually load them first so everything can be linked up.
    555   NSString* dvtFoundationPath = [developerDir
    556       stringByAppendingPathComponent:kDVTFoundationRelativePath];
    557   NSBundle* dvtFoundationBundle =
    558       [NSBundle bundleWithPath:dvtFoundationPath];
    559   if (![dvtFoundationBundle load])
    560     return nil;
    561 
    562   NSString* devToolsFoundationPath = [developerDir
    563       stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
    564   NSBundle* devToolsFoundationBundle =
    565       [NSBundle bundleWithPath:devToolsFoundationPath];
    566   if (![devToolsFoundationBundle load])
    567     return nil;
    568 
    569   // Prime DVTPlatform.
    570   NSError* error;
    571   Class DVTPlatformClass = FindClassByName(@"DVTPlatform");
    572   if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) {
    573     LogError(@"Unable to loadAllPlatformsReturningError. Error: %@",
    574          [error localizedDescription]);
    575     return nil;
    576   }
    577 
    578   // The path within the developer dir of the private Simulator frameworks.
    579   NSString* simulatorFrameworkRelativePath;
    580   if (IsRunningWithXcode6OrLater()) {
    581     simulatorFrameworkRelativePath =
    582         @"../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework";
    583     NSString* const kCoreSimulatorRelativePath =
    584        @"Library/PrivateFrameworks/CoreSimulator.framework";
    585     NSString* coreSimulatorPath = [developerDir
    586         stringByAppendingPathComponent:kCoreSimulatorRelativePath];
    587     NSBundle* coreSimulatorBundle =
    588         [NSBundle bundleWithPath:coreSimulatorPath];
    589     if (![coreSimulatorBundle load])
    590       return nil;
    591   } else {
    592     simulatorFrameworkRelativePath =
    593       @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/"
    594       @"DVTiPhoneSimulatorRemoteClient.framework";
    595   }
    596   NSString* simBundlePath = [developerDir
    597       stringByAppendingPathComponent:simulatorFrameworkRelativePath];
    598   NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
    599   if (![simBundle load])
    600     return nil;
    601   return simBundle;
    602 }
    603 
    604 // Converts the given app path to an application spec, which requires an
    605 // absolute path.
    606 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) {
    607   Class applicationSpecifierClass =
    608       FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier");
    609   if (![appPath isAbsolutePath]) {
    610     NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath];
    611     appPath = [cwd stringByAppendingPathComponent:appPath];
    612   }
    613   appPath = [appPath stringByStandardizingPath];
    614   NSFileManager* fileManager = [NSFileManager defaultManager];
    615   if (![fileManager fileExistsAtPath:appPath]) {
    616     LogError(@"File not found: %@", appPath);
    617     exit(kExitInvalidArguments);
    618   }
    619   return [applicationSpecifierClass specifierWithApplicationPath:appPath];
    620 }
    621 
    622 // Returns the system root for the given SDK version. If sdkVersion is nil, the
    623 // default system root is returned.  Will return nil if the sdkVersion is not
    624 // valid.
    625 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
    626   Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
    627   DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
    628   if (sdkVersion)
    629     systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
    630 
    631   return systemRoot;
    632 }
    633 
    634 // Builds a config object for starting the specified app.
    635 DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
    636     DTiPhoneSimulatorApplicationSpecifier* appSpec,
    637     DTiPhoneSimulatorSystemRoot* systemRoot,
    638     NSString* stdoutPath,
    639     NSString* stderrPath,
    640     NSArray* appArgs,
    641     NSDictionary* appEnv,
    642     NSNumber* deviceFamily,
    643     NSString* deviceName) {
    644   Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig");
    645   DTiPhoneSimulatorSessionConfig* sessionConfig =
    646       [[[sessionConfigClass alloc] init] autorelease];
    647   sessionConfig.applicationToSimulateOnStart = appSpec;
    648   sessionConfig.simulatedSystemRoot = systemRoot;
    649   sessionConfig.localizedClientName = @"chromium";
    650   sessionConfig.simulatedApplicationStdErrPath = stderrPath;
    651   sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
    652   sessionConfig.simulatedApplicationLaunchArgs = appArgs;
    653   sessionConfig.simulatedApplicationLaunchEnvironment = appEnv;
    654   sessionConfig.simulatedDeviceInfoName = deviceName;
    655   sessionConfig.simulatedDeviceFamily = deviceFamily;
    656 
    657   if (IsRunningWithXcode6OrLater()) {
    658 #if defined(IOSSIM_USE_XCODE_6)
    659     Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
    660     id simDeviceType =
    661         [simDeviceTypeClass supportedDeviceTypesByName][deviceName];
    662     Class simRuntimeClass = FindClassByName(@"SimRuntime");
    663     NSString* identifier = systemRoot.runtime.identifier;
    664     id simRuntime = [simRuntimeClass supportedRuntimesByIdentifier][identifier];
    665 
    666     // Attempt to use an existing device, but create one if a suitable match
    667     // can't be found. For example, if the simulator is running with a
    668     // non-default home directory (e.g. via iossim's -u command line arg) then
    669     // there won't be any devices so one will have to be created.
    670     Class simDeviceSetClass = FindClassByName(@"SimDeviceSet");
    671     id deviceSet =
    672         [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]];
    673     id simDevice = nil;
    674     for (id device in [deviceSet availableDevices]) {
    675       if ([device runtime] == simRuntime &&
    676           [device deviceType] == simDeviceType) {
    677         simDevice = device;
    678         break;
    679       }
    680     }
    681     if (!simDevice) {
    682       NSError* error = nil;
    683       // n.b. only the device name is necessary because the iOS Simulator menu
    684       // already splits devices by runtime version.
    685       NSString* name = [NSString stringWithFormat:@"iossim - %@ ", deviceName];
    686       simDevice = [deviceSet createDeviceWithType:simDeviceType
    687                                           runtime:simRuntime
    688                                              name:name
    689                                             error:&error];
    690       if (error) {
    691         LogError(@"Failed to create device: %@", error);
    692         exit(kExitInitializationFailure);
    693       }
    694     }
    695     sessionConfig.device = simDevice;
    696 #endif  // IOSSIM_USE_XCODE_6
    697   }
    698   return sessionConfig;
    699 }
    700 
    701 // Builds a simulator session that will use the given delegate.
    702 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
    703   Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
    704   DTiPhoneSimulatorSession* session =
    705       [[[sessionClass alloc] init] autorelease];
    706   session.delegate = delegate;
    707   return session;
    708 }
    709 
    710 // Creates a temporary directory with a unique name based on the provided
    711 // template. The template should not contain any path separators and be suffixed
    712 // with X's, which will be substituted with a unique alphanumeric string (see
    713 // 'man mkdtemp' for details). The directory will be created as a subdirectory
    714 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
    715 // this method would return something like '/path/to/tempdir/test-3n2'.
    716 //
    717 // Returns the absolute path of the newly-created directory, or nill if unable
    718 // to create a unique directory.
    719 NSString* CreateTempDirectory(NSString* dirNameTemplate) {
    720   NSString* fullPathTemplate =
    721       [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
    722   char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
    723   if (fullPath == NULL)
    724     return nil;
    725 
    726   return [NSString stringWithUTF8String:fullPath];
    727 }
    728 
    729 // Creates the necessary directory structure under the given user home directory
    730 // path.
    731 // Returns YES if successful, NO if unable to create the directories.
    732 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
    733   NSFileManager* fileManager = [NSFileManager defaultManager];
    734 
    735   // Create user home and subdirectories.
    736   NSArray* subDirsToCreate = [NSArray arrayWithObjects:
    737                               @"Documents",
    738                               @"Library/Caches",
    739                               @"Library/Preferences",
    740                               nil];
    741   for (NSString* subDir in subDirsToCreate) {
    742     NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
    743     NSError* error;
    744     if (![fileManager createDirectoryAtPath:path
    745                 withIntermediateDirectories:YES
    746                                  attributes:nil
    747                                       error:&error]) {
    748       LogError(@"Unable to create directory: %@. Error: %@",
    749                path, [error localizedDescription]);
    750       return NO;
    751     }
    752   }
    753 
    754   return YES;
    755 }
    756 
    757 // Creates the necessary directory structure under the given user home directory
    758 // path, then sets the path in the appropriate environment variable.
    759 // Returns YES if successful, NO if unable to create or initialize the given
    760 // directory.
    761 BOOL InitializeSimulatorUserHome(NSString* userHomePath) {
    762   if (!CreateHomeDirSubDirs(userHomePath))
    763     return NO;
    764 
    765   // Update the environment to use the specified directory as the user home
    766   // directory.
    767   // Note: the third param of setenv specifies whether or not to overwrite the
    768   // variable's value if it has already been set.
    769   if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
    770       (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
    771     LogError(@"Unable to set environment variables for home directory.");
    772     return NO;
    773   }
    774 
    775   return YES;
    776 }
    777 
    778 // Performs a case-insensitive search to see if |stringToSearch| begins with
    779 // |prefixToFind|. Returns true if a match is found.
    780 BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch,
    781                                  NSString* prefixToFind) {
    782   NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch);
    783   NSRange range = [stringToSearch rangeOfString:prefixToFind
    784                                         options:options];
    785   return range.location != NSNotFound;
    786 }
    787 
    788 // Prints the usage information to stderr.
    789 void PrintUsage() {
    790   fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
    791       "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n"
    792       "  where <appPath> is the path to the .app directory and appArgs are any"
    793       " arguments to send the simulated app.\n"
    794       "\n"
    795       "Options:\n"
    796       "  -d  Specifies the device (must be one of the values from the iOS"
    797       " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n"
    798       "  -s  Specifies the SDK version to use (e.g '4.3')."
    799       " Will use system default if not specified.\n"
    800       "  -u  Specifies a user home directory for the simulator."
    801       " Will create a new directory if not specified.\n"
    802       "  -e  Specifies an environment key=value pair that will be"
    803       " set in the simulated application's environment.\n"
    804       "  -t  Specifies the session startup timeout (in seconds)."
    805       " Defaults to %d.\n"
    806       "  -l  List supported devices and iOS versions.\n",
    807       static_cast<int>(kDefaultSessionStartTimeoutSeconds));
    808 }
    809 }  // namespace
    810 
    811 void EnsureSupportForCurrentXcodeVersion() {
    812   if (IsRunningWithXcode6OrLater()) {
    813 #if !IOSSIM_USE_XCODE_6
    814     LogError(@"Running on Xcode 6, but Xcode 6 support was not compiled in.");
    815     exit(kExitUnsupportedXcodeVersion);
    816 #endif  // IOSSIM_USE_XCODE_6
    817   }
    818 }
    819 
    820 int main(int argc, char* const argv[]) {
    821   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    822 
    823   EnsureSupportForCurrentXcodeVersion();
    824 
    825   // basename() may modify the passed in string and it returns a pointer to an
    826   // internal buffer. Give it a copy to modify, and copy what it returns.
    827   char* worker = strdup(argv[0]);
    828   char* toolName = basename(worker);
    829   if (toolName != NULL) {
    830     toolName = strdup(toolName);
    831     if (toolName != NULL)
    832       gToolName = toolName;
    833   }
    834   if (worker != NULL)
    835     free(worker);
    836 
    837   NSString* appPath = nil;
    838   NSString* appName = nil;
    839   NSString* sdkVersion = nil;
    840   NSString* deviceName = IsRunningWithXcode6OrLater() ? @"iPhone 5" : @"iPhone";
    841   NSString* simHomePath = nil;
    842   NSMutableArray* appArgs = [NSMutableArray array];
    843   NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
    844   NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
    845 
    846   NSString* developerDir = FindDeveloperDir();
    847   if (!developerDir) {
    848     LogError(@"Unable to find developer directory.");
    849     exit(kExitInitializationFailure);
    850   }
    851 
    852   NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
    853   if (!simulatorFramework) {
    854     LogError(@"Failed to load the Simulator Framework.");
    855     exit(kExitInitializationFailure);
    856   }
    857 
    858   // Parse the optional arguments
    859   int c;
    860   while ((c = getopt(argc, argv, "hs:d:u:e:t:l")) != -1) {
    861     switch (c) {
    862       case 's':
    863         sdkVersion = [NSString stringWithUTF8String:optarg];
    864         break;
    865       case 'd':
    866         deviceName = [NSString stringWithUTF8String:optarg];
    867         break;
    868       case 'u':
    869         simHomePath = [[NSFileManager defaultManager]
    870             stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
    871         break;
    872       case 'e': {
    873         NSString* envLine = [NSString stringWithUTF8String:optarg];
    874         NSRange range = [envLine rangeOfString:@"="];
    875         if (range.location == NSNotFound) {
    876           LogError(@"Invalid key=value argument for -e.");
    877           PrintUsage();
    878           exit(kExitInvalidArguments);
    879         }
    880         NSString* key = [envLine substringToIndex:range.location];
    881         NSString* value = [envLine substringFromIndex:(range.location + 1)];
    882         [appEnv setObject:value forKey:key];
    883       }
    884         break;
    885       case 't': {
    886         int timeout = atoi(optarg);
    887         if (timeout > 0) {
    888           sessionStartTimeout = static_cast<NSTimeInterval>(timeout);
    889         } else {
    890           LogError(@"Invalid startup timeout (%s).", optarg);
    891           PrintUsage();
    892           exit(kExitInvalidArguments);
    893         }
    894       }
    895         break;
    896       case 'l':
    897         PrintSupportedDevices();
    898         exit(kExitSuccess);
    899         break;
    900       case 'h':
    901         PrintUsage();
    902         exit(kExitSuccess);
    903       default:
    904         PrintUsage();
    905         exit(kExitInvalidArguments);
    906     }
    907   }
    908 
    909   // There should be at least one arg left, specifying the app path. Any
    910   // additional args are passed as arguments to the app.
    911   if (optind < argc) {
    912     appPath = [[NSFileManager defaultManager]
    913         stringWithFileSystemRepresentation:argv[optind]
    914                                     length:strlen(argv[optind])];
    915     appName = [appPath lastPathComponent];
    916     while (++optind < argc) {
    917       [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
    918     }
    919   } else {
    920     LogError(@"Unable to parse command line arguments.");
    921     PrintUsage();
    922     exit(kExitInvalidArguments);
    923   }
    924 
    925   // Make sure the app path provided is legit.
    926   DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
    927   if (!appSpec) {
    928     LogError(@"Invalid app path: %@", appPath);
    929     exit(kExitInitializationFailure);
    930   }
    931 
    932   // Make sure the SDK path provided is legit (or nil).
    933   DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
    934   if (!systemRoot) {
    935     LogError(@"Invalid SDK version: %@", sdkVersion);
    936     PrintSupportedDevices();
    937     exit(kExitInitializationFailure);
    938   }
    939 
    940   // Get the paths for stdout and stderr so the simulated app's output will show
    941   // up in the caller's stdout/stderr.
    942   NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
    943   NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
    944 
    945   // Determine the deviceFamily based on the deviceName
    946   NSNumber* deviceFamily = nil;
    947   if (IsRunningWithXcode6OrLater()) {
    948 #if defined(IOSSIM_USE_XCODE_6)
    949     Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
    950     if ([simDeviceTypeClass supportedDeviceTypesByName][deviceName] == nil) {
    951       LogError(@"Invalid device name: %@.", deviceName);
    952       PrintSupportedDevices();
    953       exit(kExitInvalidArguments);
    954     }
    955 #endif  // IOSSIM_USE_XCODE_6
    956   } else {
    957     if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) {
    958       deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
    959     } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) {
    960       deviceFamily = [NSNumber numberWithInt:kIPadFamily];
    961     }
    962     else {
    963       LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'",
    964                deviceName);
    965       exit(kExitInvalidArguments);
    966     }
    967   }
    968 
    969   // Set up the user home directory for the simulator only if a non-default
    970   // value was specified.
    971   if (simHomePath) {
    972     if (!InitializeSimulatorUserHome(simHomePath)) {
    973       LogError(@"Unable to initialize home directory for simulator: %@",
    974                simHomePath);
    975       exit(kExitInitializationFailure);
    976     }
    977   } else {
    978     simHomePath = NSHomeDirectory();
    979   }
    980 
    981   // Create the config and simulator session.
    982   DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
    983                                                               systemRoot,
    984                                                               stdioPath,
    985                                                               stdioPath,
    986                                                               appArgs,
    987                                                               appEnv,
    988                                                               deviceFamily,
    989                                                               deviceName);
    990   SimulatorDelegate* delegate =
    991       [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
    992                                        developerDir:developerDir
    993                                       simulatorHome:simHomePath] autorelease];
    994   DTiPhoneSimulatorSession* session = BuildSession(delegate);
    995 
    996   // Start the simulator session.
    997   NSError* error;
    998   BOOL started = [session requestStartWithConfig:config
    999                                          timeout:sessionStartTimeout
   1000                                            error:&error];
   1001 
   1002   // Spin the runtime indefinitely. When the delegate gets the message that the
   1003   // app has quit it will exit this program.
   1004   if (started) {
   1005     [[NSRunLoop mainRunLoop] run];
   1006   } else {
   1007     LogError(@"Simulator failed request to start:  \"%@\" (%@:%ld)",
   1008              [error localizedDescription],
   1009              [error domain], static_cast<long int>([error code]));
   1010   }
   1011 
   1012   // Note that this code is only executed if the simulator fails to start
   1013   // because once the main run loop is started, only the delegate calling
   1014   // exit() will end the program.
   1015   [pool drain];
   1016   return kExitFailure;
   1017 }
   1018