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