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