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