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