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