Update .DEPS.git
[chromium-blink-merge.git] / testing / iossim / iossim.mm
blob152f2bea4c74d43143aa805ceafa16d4059a899a
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   NSString* simulatorHome_;
132   NSThread* outputThread_;
133   NSBundle* simulatorBundle_;
134   BOOL appRunning_;
136 @end
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
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   }
160   return self;
163 - (void)dealloc {
164   [stdioPath_ release];
165   [developerDir_ release];
166   [simulatorBundle_ release];
167   [super dealloc];
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];
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   }
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]];
193   [pool drain];
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;
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];
235       // session:didEndWithError should not return (because it exits) so
236       // the execution path should never get here.
237       exit(kExitFailure);
238     }
240     LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
241              [error localizedDescription],
242              [error domain], static_cast<long int>([error code]));
243     exit(kExitAppFailedToStart);
244   }
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];
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   }
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   }
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);
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   }
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);
360 @end
362 namespace {
364 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
365 // variable.
366 NSString* 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;
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"]];
378   NSPipe* outputPipe = [NSPipe pipe];
379   [xcodeSelectTask setStandardOutput:outputPipe];
380   NSFileHandle* outputFile = [outputPipe fileHandleForReading];
382   [xcodeSelectTask launch];
383   NSData* outputData = [outputFile readDataToEndOfFile];
384   [xcodeSelectTask terminate];
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;
396 // Loads the Simulator framework from the given developer dir.
397 NSBundle* 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;
414 // Helper to find a class by name and die if it isn't found.
415 Class 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;
424 // Converts the given app path to an application spec, which requires an
425 // absolute path.
426 DTiPhoneSimulatorApplicationSpecifier* 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];
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.
440 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
441   Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
442   DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
443   if (sdkVersion)
444     systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
446   return systemRoot;
449 // Builds a config object for starting the specified app.
450 DTiPhoneSimulatorSessionConfig* 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;
472 // Builds a simulator session that will use the given delegate.
473 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
474   Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
475   DTiPhoneSimulatorSession* session =
476       [[[sessionClass alloc] init] autorelease];
477   session.delegate = delegate;
478   return session;
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'.
488 // Returns the absolute path of the newly-created directory, or nill if unable
489 // to create a unique directory.
490 NSString* 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;
497   return [NSString stringWithUTF8String:fullPath];
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.
503 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
504   NSFileManager* fileManager = [NSFileManager defaultManager];
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   }
525   return YES;
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.
532 BOOL InitializeSimulatorUserHome(NSString* userHomePath, NSString* deviceName) {
533   if (!CreateHomeDirSubDirs(userHomePath))
534     return NO;
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);
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   }
554   return YES;
557 // Performs a case-insensitive search to see if |stringToSearch| begins with
558 // |prefixToFind|. Returns true if a match is found.
559 BOOL 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;
567 // Prints the usage information to stderr.
568 void 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));
588 }  // namespace
590 int main(int argc, char* const argv[]) {
591   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
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);
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;
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   }
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   }
677   NSString* developerDir = FindDeveloperDir();
678   if (!developerDir) {
679     LogError(@"Unable to find developer directory.");
680     exit(kExitInitializationFailure);
681   }
683   NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
684   if (!simulatorFramework) {
685     LogError(@"Failed to load the Simulator Framework.");
686     exit(kExitInitializationFailure);
687   }
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   }
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   }
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"];
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   }
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   }
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);
751   // Start the simulator session.
752   NSError* error;
753   BOOL started = [session requestStartWithConfig:config
754                                          timeout:sessionStartTimeout
755                                            error:&error];
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   }
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;