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