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