cygprofile: increase timeouts to allow showing web contents
[chromium-blink-merge.git] / testing / iossim / iossim.mm
blobd66296f0a3e7e4284303123a53a9a23a7ad9b13f
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 #include <asl.h>
6 #import <Foundation/Foundation.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 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/)
21 // However, there are some forward declarations required to get things to
22 // compile.
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
31 // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed
32 // (crbug.com/385030).
33 #if defined(IOSSIM_USE_XCODE_6)
34 @protocol SimBridge;
35 @class DVTSimulatorApplication;
36 @class SimDeviceSet;
37 @class SimDeviceType;
38 @class SimProfilesPathMonitor;
39 @class SimRuntime;
40 @class SimServiceConnectionManager;
41 #import "CoreSimulator.h"
42 #endif  // IOSSIM_USE_XCODE_6
44 @interface DVTPlatform : NSObject
45 + (BOOL)loadAllPlatformsReturningError:(id*)arg1;
46 @end
47 @class DTiPhoneSimulatorApplicationSpecifier;
48 @class DTiPhoneSimulatorSession;
49 @class DTiPhoneSimulatorSessionConfig;
50 @class DTiPhoneSimulatorSystemRoot;
51 @class DVTConfinementServiceConnection;
52 @class DVTDispatchLock;
53 @class DVTiPhoneSimulatorMessenger;
54 @class DVTNotificationToken;
55 @class DVTTask;
56 // The DTiPhoneSimulatorSessionDelegate protocol is referenced
57 // by the iPhoneSimulatorRemoteClient framework, but not defined in the object
58 // file, so it must be defined here before importing the generated
59 // iPhoneSimulatorRemoteClient.h file.
60 @protocol DTiPhoneSimulatorSessionDelegate
61 - (void)session:(DTiPhoneSimulatorSession*)session
62     didEndWithError:(NSError*)error;
63 - (void)session:(DTiPhoneSimulatorSession*)session
64        didStart:(BOOL)started
65       withError:(NSError*)error;
66 @end
67 #import "DVTiPhoneSimulatorRemoteClient.h"
69 // An undocumented system log key included in messages from launchd. The value
70 // is the PID of the process the message is about (as opposed to launchd's PID).
71 #define ASL_KEY_REF_PID "RefPID"
73 namespace {
75 // Name of environment variables that control the user's home directory in the
76 // simulator.
77 const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME";
78 const char* const kHomeEnvVariable = "HOME";
80 // Device family codes for iPhone and iPad.
81 const int kIPhoneFamily = 1;
82 const int kIPadFamily = 2;
84 // Max number of seconds to wait for the simulator session to start.
85 // This timeout must allow time to start up iOS Simulator, install the app
86 // and perform any other black magic that is encoded in the
87 // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up
88 // time is only a couple seconds but machine load, disk caches, etc., can all
89 // affect startup time in the wild so the timeout needs to be fairly generous.
90 // If this timeout occurs iossim will likely exit with non-zero status; the
91 // exception being if the app is invoked and completes execution before the
92 // session is started (this case is handled in session:didStart:withError).
93 const NSTimeInterval kDefaultSessionStartTimeoutSeconds = 30;
95 // While the simulated app is running, its stdout is redirected to a file which
96 // is polled by iossim and written to iossim's stdout using the following
97 // polling interval.
98 const NSTimeInterval kOutputPollIntervalSeconds = 0.1;
100 NSString* const kDVTFoundationRelativePath =
101     @"../SharedFrameworks/DVTFoundation.framework";
102 NSString* const kDevToolsFoundationRelativePath =
103     @"../OtherFrameworks/DevToolsFoundation.framework";
104 NSString* const kSimulatorRelativePath =
105     @"Platforms/iPhoneSimulator.platform/Developer/Applications/"
106     @"iPhone Simulator.app";
108 // Simulator Error String Key. This can be found by looking in the Simulator's
109 // Localizable.strings files.
110 NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit.";
112 const char* gToolName = "iossim";
114 // Exit status codes.
115 const int kExitSuccess = EXIT_SUCCESS;
116 const int kExitFailure = EXIT_FAILURE;
117 const int kExitInvalidArguments = 2;
118 const int kExitInitializationFailure = 3;
119 const int kExitAppFailedToStart = 4;
120 const int kExitAppCrashed = 5;
121 const int kExitUnsupportedXcodeVersion = 6;
123 void LogError(NSString* format, ...) {
124   va_list list;
125   va_start(list, format);
127   NSString* message =
128       [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
130   fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
131   fflush(stderr);
133   va_end(list);
136 void LogWarning(NSString* format, ...) {
137   va_list list;
138   va_start(list, format);
140   NSString* message =
141       [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
143   fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
144   fflush(stderr);
146   va_end(list);
149 // Helper to find a class by name and die if it isn't found.
150 Class FindClassByName(NSString* nameOfClass) {
151   Class theClass = NSClassFromString(nameOfClass);
152   if (!theClass) {
153     LogError(@"Failed to find class %@ at runtime.", nameOfClass);
154     exit(kExitInitializationFailure);
155   }
156   return theClass;
159 // Returns the a NSString containing the stdout from running an NSTask that
160 // launches |toolPath| with th given command line |args|.
161 NSString* GetOutputFromTask(NSString* toolPath, NSArray* args) {
162   NSTask* task = [[[NSTask alloc] init] autorelease];
163   [task setLaunchPath:toolPath];
164   [task setArguments:args];
165   NSPipe* outputPipe = [NSPipe pipe];
166   [task setStandardOutput:outputPipe];
167   NSFileHandle* outputFile = [outputPipe fileHandleForReading];
169   [task launch];
170   NSData* outputData = [outputFile readDataToEndOfFile];
171   [task waitUntilExit];
172   if ([task isRunning]) {
173     LogError(@"Task '%@ %@' is still running.",
174         toolPath,
175         [args componentsJoinedByString:@" "]);
176     return nil;
177   } else if ([task terminationStatus]) {
178     LogError(@"Task '%@ %@' exited with return code %d.",
179         toolPath,
180         [args componentsJoinedByString:@" "],
181         [task terminationStatus]);
182     return nil;
183   }
184   return [[[NSString alloc] initWithData:outputData
185                                 encoding:NSUTF8StringEncoding] autorelease];
188 // Finds the Xcode version via xcodebuild -version. Output from xcodebuild is
189 // expected to look like:
190 //   Xcode <version>
191 //   Build version 5B130a
192 // where <version> is the string returned by this function (e.g. 6.0).
193 NSString* FindXcodeVersion() {
194   NSString* output = GetOutputFromTask(@"/usr/bin/xcodebuild",
195                                        @[ @"-version" ]);
196   // Scan past the "Xcode ", then scan the rest of the line into |version|.
197   NSScanner* scanner = [NSScanner scannerWithString:output];
198   BOOL valid = [scanner scanString:@"Xcode " intoString:NULL];
199   NSString* version;
200   valid =
201       [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
202                               intoString:&version];
203   if (!valid) {
204     LogError(@"Unable to find Xcode version. 'xcodebuild -version' "
205              @"returned \n%@", output);
206     return nil;
207   }
208   return version;
211 // Returns true if iossim is running with Xcode 6 or later installed on the
212 // host.
213 BOOL IsRunningWithXcode6OrLater() {
214   static NSString* xcodeVersion = FindXcodeVersion();
215   if (!xcodeVersion) {
216     return false;
217   }
218   NSArray* components = [xcodeVersion componentsSeparatedByString:@"."];
219   if ([components count] < 1) {
220     return false;
221   }
222   NSInteger majorVersion = [[components objectAtIndex:0] integerValue];
223   return majorVersion >= 6;
226 // Prints supported devices and SDKs.
227 void PrintSupportedDevices() {
228   if (IsRunningWithXcode6OrLater()) {
229 #if defined(IOSSIM_USE_XCODE_6)
230     printf("Supported device/SDK combinations:\n");
231     Class simDeviceSetClass = FindClassByName(@"SimDeviceSet");
232     id deviceSet =
233         [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]];
234     for (id simDevice in [deviceSet availableDevices]) {
235       NSString* deviceInfo =
236           [NSString stringWithFormat:@"  -d '%@' -s '%@'\n",
237               [simDevice name], [[simDevice runtime] versionString]];
238       printf("%s", [deviceInfo UTF8String]);
239     }
240 #endif  // IOSSIM_USE_XCODE_6
241   } else {
242     printf("Supported SDK versions:\n");
243     Class rootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
244     for (id root in [rootClass knownRoots]) {
245       printf("  '%s'\n", [[root sdkVersion] UTF8String]);
246     }
247     // This is the list of devices supported on Xcode 5.1.x.
248     printf("Supported devices:\n");
249     printf("  'iPhone'\n");
250     printf("  'iPhone Retina (3.5-inch)'\n");
251     printf("  'iPhone Retina (4-inch)'\n");
252     printf("  'iPhone Retina (4-inch 64-bit)'\n");
253     printf("  'iPad'\n");
254     printf("  'iPad Retina'\n");
255     printf("  'iPad Retina (64-bit)'\n");
256   }
258 }  // namespace
260 // A delegate that is called when the simulated app is started or ended in the
261 // simulator.
262 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
263  @private
264   NSString* stdioPath_;
265   NSString* developerDir_;
266   NSString* simulatorHome_;
267   NSThread* outputThread_;
268   NSBundle* simulatorBundle_;
269   BOOL appRunning_;
271 @end
273 // An implementation that copies the simulated app's stdio to stdout of this
274 // executable. While it would be nice to get stdout and stderr independently
275 // from iOS Simulator, issues like I/O buffering and interleaved output
276 // between iOS Simulator and the app would cause iossim to display things out
277 // of order here. Printing all output to a single file keeps the order correct.
278 // Instances of this classe should be initialized with the location of the
279 // simulated app's output file. When the simulated app starts, a thread is
280 // started which handles copying data from the simulated app's output file to
281 // the stdout of this executable.
282 @implementation SimulatorDelegate
284 // Specifies the file locations of the simulated app's stdout and stderr.
285 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath
286                            developerDir:(NSString*)developerDir
287                           simulatorHome:(NSString*)simulatorHome {
288   self = [super init];
289   if (self) {
290     stdioPath_ = [stdioPath copy];
291     developerDir_ = [developerDir copy];
292     simulatorHome_ = [simulatorHome copy];
293   }
295   return self;
298 - (void)dealloc {
299   [stdioPath_ release];
300   [developerDir_ release];
301   [simulatorBundle_ release];
302   [super dealloc];
305 // Reads data from the simulated app's output and writes it to stdout. This
306 // method blocks, so it should be called in a separate thread. The iOS
307 // Simulator takes a file path for the simulated app's stdout and stderr, but
308 // this path isn't always available (e.g. when the stdout is Xcode's build
309 // window). As a workaround, iossim creates a temp file to hold output, which
310 // this method reads and copies to stdout.
311 - (void)tailOutputForSession:(DTiPhoneSimulatorSession*)session {
312   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
314   NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_];
315   if (IsRunningWithXcode6OrLater()) {
316 #if defined(IOSSIM_USE_XCODE_6)
317     // With iOS 8 simulators on Xcode 6, the app output is relative to the
318     // simulator's data directory.
319     NSString* versionString =
320         [[[session sessionConfig] simulatedSystemRoot] sdkVersion];
321     NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."]
322         objectAtIndex:0] intValue];
323     if (majorVersion >= 8) {
324       NSString* dataPath = session.sessionConfig.device.dataPath;
325       NSString* appOutput =
326           [dataPath stringByAppendingPathComponent:stdioPath_];
327       simio = [NSFileHandle fileHandleForReadingAtPath:appOutput];
328     }
329 #endif  // IOSSIM_USE_XCODE_6
330   }
331   NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput];
332   // Copy data to stdout/stderr while the app is running.
333   while (appRunning_) {
334     NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init];
335     [standardOutput writeData:[simio readDataToEndOfFile]];
336     [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
337     [innerPool drain];
338   }
340   // Once the app is no longer running, copy any data that was written during
341   // the last sleep cycle.
342   [standardOutput writeData:[simio readDataToEndOfFile]];
344   [pool drain];
347 // Fetches a localized error string from the Simulator.
348 - (NSString *)localizedSimulatorErrorString:(NSString*)stringKey {
349   // Lazy load of the simulator bundle.
350   if (simulatorBundle_ == nil) {
351     NSString* simulatorPath = [developerDir_
352         stringByAppendingPathComponent:kSimulatorRelativePath];
353     simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath];
354   }
355   NSString *localizedStr =
356       [simulatorBundle_ localizedStringForKey:stringKey
357                                         value:nil
358                                         table:nil];
359   if ([localizedStr length])
360     return localizedStr;
361   // Failed to get a value, follow Cocoa conventions and use the key as the
362   // string.
363   return stringKey;
366 - (void)session:(DTiPhoneSimulatorSession*)session
367        didStart:(BOOL)started
368       withError:(NSError*)error {
369   if (!started) {
370     // If the test executes very quickly (<30ms), the SimulatorDelegate may not
371     // get the initial session:started:withError: message indicating successful
372     // startup of the simulated app. Instead the delegate will get a
373     // session:started:withError: message after the timeout has elapsed. To
374     // account for this case, check if the simulated app's stdio file was
375     // ever created and if it exists dump it to stdout and return success.
376     NSFileManager* fileManager = [NSFileManager defaultManager];
377     if ([fileManager fileExistsAtPath:stdioPath_]) {
378       appRunning_ = NO;
379       [self tailOutputForSession:session];
380       // Note that exiting in this state leaves a process running
381       // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will
382       // prevent future simulator sessions from being started for 30 seconds
383       // unless the iOS Simulator application is killed altogether.
384       [self session:session didEndWithError:nil];
386       // session:didEndWithError should not return (because it exits) so
387       // the execution path should never get here.
388       exit(kExitFailure);
389     }
391     LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
392              [error localizedDescription],
393              [error domain], static_cast<long int>([error code]));
394     PrintSupportedDevices();
395     exit(kExitAppFailedToStart);
396   }
398   // Start a thread to write contents of outputPath to stdout.
399   appRunning_ = YES;
400   outputThread_ =
401       [[NSThread alloc] initWithTarget:self
402                               selector:@selector(tailOutputForSession:)
403                                 object:session];
404   [outputThread_ start];
407 - (void)session:(DTiPhoneSimulatorSession*)session
408     didEndWithError:(NSError*)error {
409   appRunning_ = NO;
410   // Wait for the output thread to finish copying data to stdout.
411   if (outputThread_) {
412     while (![outputThread_ isFinished]) {
413       [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
414     }
415     [outputThread_ release];
416     outputThread_ = nil;
417   }
419   if (error) {
420     // There appears to be a race condition where sometimes the simulator
421     // framework will end with an error, but the error is that the simulated
422     // app cleanly shut down; try to trap this error and don't fail the
423     // simulator run.
424     NSString* localizedDescription = [error localizedDescription];
425     NSString* ignorableErrorStr =
426         [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey];
427     if ([ignorableErrorStr isEqual:localizedDescription]) {
428       LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)",
429                  localizedDescription, [error domain],
430                  static_cast<long int>([error code]));
431     } else {
432       LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
433                localizedDescription, [error domain],
434                static_cast<long int>([error code]));
435       exit(kExitFailure);
436     }
437   }
439   // Try to determine if the simulated app crashed or quit with a non-zero
440   // status code. iOS Simluator handles things a bit differently depending on
441   // the version, so first determine the iOS version being used.
442   BOOL badEntryFound = NO;
443   NSString* versionString =
444       [[[session sessionConfig] simulatedSystemRoot] sdkVersion];
445   NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."]
446       objectAtIndex:0] intValue];
447   if (majorVersion <= 6) {
448     // In iOS 6 and before, logging from the simulated apps went to the main
449     // system logs, so use ASL to check if the simulated app exited abnormally
450     // by looking for system log messages from launchd that refer to the
451     // simulated app's PID. Limit query to messages in the last minute since
452     // PIDs are cyclical.
453     aslmsg query = asl_new(ASL_TYPE_QUERY);
454     asl_set_query(query, ASL_KEY_SENDER, "launchd",
455                   ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING);
456     char session_id[20];
457     if (snprintf(session_id, 20, "%d", [session simulatedApplicationPID]) < 0) {
458       LogError(@"Failed to get [session simulatedApplicationPID]");
459       exit(kExitFailure);
460     }
461     asl_set_query(query, ASL_KEY_REF_PID, session_id, ASL_QUERY_OP_EQUAL);
462     asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL);
464     // Log any messages found, and take note of any messages that may indicate
465     // the app crashed or did not exit cleanly.
466     aslresponse response = asl_search(NULL, query);
467     aslmsg entry;
468     while ((entry = aslresponse_next(response)) != NULL) {
469       const char* message = asl_get(entry, ASL_KEY_MSG);
470       LogWarning(@"Console message: %s", message);
471       // Some messages are harmless, so don't trigger a failure for them.
472       if (strstr(message, "The following job tried to hijack the service"))
473         continue;
474       badEntryFound = YES;
475     }
476   } else {
477     // Otherwise, the iOS Simulator's system logging is sandboxed, so parse the
478     // sandboxed system.log file for known errors.
479     NSString* path;
480     if (IsRunningWithXcode6OrLater()) {
481 #if defined(IOSSIM_USE_XCODE_6)
482       NSString* dataPath = session.sessionConfig.device.dataPath;
483       path =
484           [dataPath stringByAppendingPathComponent:@"Library/Logs/system.log"];
485 #endif  // IOSSIM_USE_XCODE_6
486     } else {
487       NSString* relativePathToSystemLog =
488           [NSString stringWithFormat:
489               @"Library/Logs/iOS Simulator/%@/system.log", versionString];
490       path = [simulatorHome_
491           stringByAppendingPathComponent:relativePathToSystemLog];
492     }
493     NSFileManager* fileManager = [NSFileManager defaultManager];
494     if ([fileManager fileExistsAtPath:path]) {
495       NSString* content =
496           [NSString stringWithContentsOfFile:path
497                                     encoding:NSUTF8StringEncoding
498                                        error:NULL];
499       NSArray* lines = [content componentsSeparatedByCharactersInSet:
500           [NSCharacterSet newlineCharacterSet]];
501       NSString* simulatedAppPID =
502           [NSString stringWithFormat:@"%d", session.simulatedApplicationPID];
503       NSArray* kErrorStrings = @[
504         @"Service exited with abnormal code:",
505         @"Service exited due to signal:",
506       ];
507       for (NSString* line in lines) {
508         if ([line rangeOfString:simulatedAppPID].location != NSNotFound) {
509           for (NSString* errorString in kErrorStrings) {
510             if ([line rangeOfString:errorString].location != NSNotFound) {
511               LogWarning(@"Console message: %@", line);
512               badEntryFound = YES;
513               break;
514             }
515           }
516           if (badEntryFound) {
517             break;
518           }
519         }
520       }
521       // Remove the log file so subsequent invocations of iossim won't be
522       // looking at stale logs.
523       remove([path fileSystemRepresentation]);
524     } else {
525         LogWarning(@"Unable to find system log at '%@'.", path);
526     }
527   }
529   // If the query returned any nasty-looking results, iossim should exit with
530   // non-zero status.
531   if (badEntryFound) {
532     LogError(@"Simulated app crashed or exited with non-zero status");
533     exit(kExitAppCrashed);
534   }
535   exit(kExitSuccess);
537 @end
539 namespace {
541 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
542 // variable.
543 NSString* FindDeveloperDir() {
544   // Check the env first.
545   NSDictionary* env = [[NSProcessInfo processInfo] environment];
546   NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"];
547   if ([developerDir length] > 0)
548     return developerDir;
550   // Go look for it via xcode-select.
551   NSString* output = GetOutputFromTask(@"/usr/bin/xcode-select",
552                                        @[ @"-print-path" ]);
553   output = [output stringByTrimmingCharactersInSet:
554       [NSCharacterSet whitespaceAndNewlineCharacterSet]];
555   if ([output length] == 0)
556     output = nil;
557   return output;
560 // Loads the Simulator framework from the given developer dir.
561 NSBundle* LoadSimulatorFramework(NSString* developerDir) {
562   // The Simulator framework depends on some of the other Xcode private
563   // frameworks; manually load them first so everything can be linked up.
564   NSString* dvtFoundationPath = [developerDir
565       stringByAppendingPathComponent:kDVTFoundationRelativePath];
566   NSBundle* dvtFoundationBundle =
567       [NSBundle bundleWithPath:dvtFoundationPath];
568   if (![dvtFoundationBundle load])
569     return nil;
571   NSString* devToolsFoundationPath = [developerDir
572       stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
573   NSBundle* devToolsFoundationBundle =
574       [NSBundle bundleWithPath:devToolsFoundationPath];
575   if (![devToolsFoundationBundle load])
576     return nil;
578   // Prime DVTPlatform.
579   NSError* error;
580   Class DVTPlatformClass = FindClassByName(@"DVTPlatform");
581   if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) {
582     LogError(@"Unable to loadAllPlatformsReturningError. Error: %@",
583          [error localizedDescription]);
584     return nil;
585   }
587   // The path within the developer dir of the private Simulator frameworks.
588   NSString* simulatorFrameworkRelativePath;
589   if (IsRunningWithXcode6OrLater()) {
590     simulatorFrameworkRelativePath =
591         @"../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework";
592     NSString* const kCoreSimulatorRelativePath =
593        @"Library/PrivateFrameworks/CoreSimulator.framework";
594     NSString* coreSimulatorPath = [developerDir
595         stringByAppendingPathComponent:kCoreSimulatorRelativePath];
596     NSBundle* coreSimulatorBundle =
597         [NSBundle bundleWithPath:coreSimulatorPath];
598     if (![coreSimulatorBundle load])
599       return nil;
600   } else {
601     simulatorFrameworkRelativePath =
602       @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/"
603       @"DVTiPhoneSimulatorRemoteClient.framework";
604   }
605   NSString* simBundlePath = [developerDir
606       stringByAppendingPathComponent:simulatorFrameworkRelativePath];
607   NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
608   if (![simBundle load])
609     return nil;
610   return simBundle;
613 // Converts the given app path to an application spec, which requires an
614 // absolute path.
615 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) {
616   Class applicationSpecifierClass =
617       FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier");
618   if (![appPath isAbsolutePath]) {
619     NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath];
620     appPath = [cwd stringByAppendingPathComponent:appPath];
621   }
622   appPath = [appPath stringByStandardizingPath];
623   NSFileManager* fileManager = [NSFileManager defaultManager];
624   if (![fileManager fileExistsAtPath:appPath]) {
625     LogError(@"File not found: %@", appPath);
626     exit(kExitInvalidArguments);
627   }
628   return [applicationSpecifierClass specifierWithApplicationPath:appPath];
631 // Returns the system root for the given SDK version. If sdkVersion is nil, the
632 // default system root is returned.  Will return nil if the sdkVersion is not
633 // valid.
634 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
635   Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
636 #if defined(IOSSIM_USE_XCODE_6)
637   Class simRuntimeClass = FindClassByName(@"SimRuntime");
638   NSArray* sorted =
639       [[simRuntimeClass supportedRuntimes] sortedArrayUsingDescriptors:@[
640         [NSSortDescriptor sortDescriptorWithKey:@"version" ascending:YES]
641       ]];
642   NSString* versionString = [[sorted lastObject] versionString];
643   DTiPhoneSimulatorSystemRoot* systemRoot =
644       [systemRootClass rootWithSDKVersion:versionString];
645 #else
646   DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
647 #endif
648   if (sdkVersion)
649     systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
651   return systemRoot;
654 // Builds a config object for starting the specified app.
655 DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
656     DTiPhoneSimulatorApplicationSpecifier* appSpec,
657     DTiPhoneSimulatorSystemRoot* systemRoot,
658     NSString* stdoutPath,
659     NSString* stderrPath,
660     NSArray* appArgs,
661     NSDictionary* appEnv,
662     NSNumber* deviceFamily,
663     NSString* deviceName) {
664   Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig");
665   DTiPhoneSimulatorSessionConfig* sessionConfig =
666       [[[sessionConfigClass alloc] init] autorelease];
667   sessionConfig.applicationToSimulateOnStart = appSpec;
668   sessionConfig.simulatedSystemRoot = systemRoot;
669   sessionConfig.localizedClientName = @"chromium";
670   sessionConfig.simulatedApplicationStdErrPath = stderrPath;
671   sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
672   sessionConfig.simulatedApplicationLaunchArgs = appArgs;
673   sessionConfig.simulatedApplicationLaunchEnvironment = appEnv;
674   sessionConfig.simulatedDeviceInfoName = deviceName;
675   sessionConfig.simulatedDeviceFamily = deviceFamily;
677   if (IsRunningWithXcode6OrLater()) {
678 #if defined(IOSSIM_USE_XCODE_6)
679     Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
680     id simDeviceType =
681         [simDeviceTypeClass supportedDeviceTypesByAlias][deviceName];
682     Class simRuntimeClass = FindClassByName(@"SimRuntime");
683     NSString* identifier = systemRoot.runtime.identifier;
684     id simRuntime = [simRuntimeClass supportedRuntimesByIdentifier][identifier];
686     // Attempt to use an existing device, but create one if a suitable match
687     // can't be found. For example, if the simulator is running with a
688     // non-default home directory (e.g. via iossim's -u command line arg) then
689     // there won't be any devices so one will have to be created.
690     Class simDeviceSetClass = FindClassByName(@"SimDeviceSet");
691     id deviceSet =
692         [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]];
693     id simDevice = nil;
694     for (id device in [deviceSet availableDevices]) {
695       if ([device runtime] == simRuntime &&
696           [device deviceType] == simDeviceType) {
697         simDevice = device;
698         break;
699       }
700     }
701     if (!simDevice) {
702       NSError* error = nil;
703       // n.b. only the device name is necessary because the iOS Simulator menu
704       // already splits devices by runtime version.
705       NSString* name = [NSString stringWithFormat:@"iossim - %@ ", deviceName];
706       simDevice = [deviceSet createDeviceWithType:simDeviceType
707                                           runtime:simRuntime
708                                              name:name
709                                             error:&error];
710       if (error) {
711         LogError(@"Failed to create device: %@", error);
712         exit(kExitInitializationFailure);
713       }
714     }
715     sessionConfig.device = simDevice;
716 #endif  // IOSSIM_USE_XCODE_6
717   }
718   return sessionConfig;
721 // Builds a simulator session that will use the given delegate.
722 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
723   Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
724   DTiPhoneSimulatorSession* session =
725       [[[sessionClass alloc] init] autorelease];
726   session.delegate = delegate;
727   return session;
730 // Creates a temporary directory with a unique name based on the provided
731 // template. The template should not contain any path separators and be suffixed
732 // with X's, which will be substituted with a unique alphanumeric string (see
733 // 'man mkdtemp' for details). The directory will be created as a subdirectory
734 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
735 // this method would return something like '/path/to/tempdir/test-3n2'.
737 // Returns the absolute path of the newly-created directory, or nill if unable
738 // to create a unique directory.
739 NSString* CreateTempDirectory(NSString* dirNameTemplate) {
740   NSString* fullPathTemplate =
741       [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
742   char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
743   if (fullPath == NULL)
744     return nil;
746   return [NSString stringWithUTF8String:fullPath];
749 // Creates the necessary directory structure under the given user home directory
750 // path.
751 // Returns YES if successful, NO if unable to create the directories.
752 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
753   NSFileManager* fileManager = [NSFileManager defaultManager];
755   // Create user home and subdirectories.
756   NSArray* subDirsToCreate = [NSArray arrayWithObjects:
757                               @"Documents",
758                               @"Library/Caches",
759                               @"Library/Preferences",
760                               nil];
761   for (NSString* subDir in subDirsToCreate) {
762     NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
763     NSError* error;
764     if (![fileManager createDirectoryAtPath:path
765                 withIntermediateDirectories:YES
766                                  attributes:nil
767                                       error:&error]) {
768       LogError(@"Unable to create directory: %@. Error: %@",
769                path, [error localizedDescription]);
770       return NO;
771     }
772   }
774   return YES;
777 // Creates the necessary directory structure under the given user home directory
778 // path, then sets the path in the appropriate environment variable.
779 // Returns YES if successful, NO if unable to create or initialize the given
780 // directory.
781 BOOL InitializeSimulatorUserHome(NSString* userHomePath) {
782   if (!CreateHomeDirSubDirs(userHomePath))
783     return NO;
785   // Update the environment to use the specified directory as the user home
786   // directory.
787   // Note: the third param of setenv specifies whether or not to overwrite the
788   // variable's value if it has already been set.
789   if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
790       (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
791     LogError(@"Unable to set environment variables for home directory.");
792     return NO;
793   }
795   return YES;
798 // Performs a case-insensitive search to see if |stringToSearch| begins with
799 // |prefixToFind|. Returns true if a match is found.
800 BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch,
801                                  NSString* prefixToFind) {
802   NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch);
803   NSRange range = [stringToSearch rangeOfString:prefixToFind
804                                         options:options];
805   return range.location != NSNotFound;
808 // Prints the usage information to stderr.
809 void PrintUsage() {
810   fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
811       "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n"
812       "  where <appPath> is the path to the .app directory and appArgs are any"
813       " arguments to send the simulated app.\n"
814       "\n"
815       "Options:\n"
816       "  -d  Specifies the device (must be one of the values from the iOS"
817       " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n"
818       "  -s  Specifies the SDK version to use (e.g '4.3')."
819       " Will use system default if not specified.\n"
820       "  -u  Specifies a user home directory for the simulator."
821       " Will create a new directory if not specified.\n"
822       "  -e  Specifies an environment key=value pair that will be"
823       " set in the simulated application's environment.\n"
824       "  -t  Specifies the session startup timeout (in seconds)."
825       " Defaults to %d.\n"
826       "  -l  List supported devices and iOS versions.\n",
827       static_cast<int>(kDefaultSessionStartTimeoutSeconds));
829 }  // namespace
831 void EnsureSupportForCurrentXcodeVersion() {
832   if (IsRunningWithXcode6OrLater()) {
833 #if !IOSSIM_USE_XCODE_6
834     LogError(@"Running on Xcode 6, but Xcode 6 support was not compiled in.");
835     exit(kExitUnsupportedXcodeVersion);
836 #endif  // IOSSIM_USE_XCODE_6
837   }
840 int main(int argc, char* const argv[]) {
841   NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
843   EnsureSupportForCurrentXcodeVersion();
845   // basename() may modify the passed in string and it returns a pointer to an
846   // internal buffer. Give it a copy to modify, and copy what it returns.
847   char* worker = strdup(argv[0]);
848   char* toolName = basename(worker);
849   if (toolName != NULL) {
850     toolName = strdup(toolName);
851     if (toolName != NULL)
852       gToolName = toolName;
853   }
854   if (worker != NULL)
855     free(worker);
857   NSString* appPath = nil;
858   NSString* appName = nil;
859   NSString* sdkVersion = nil;
860   NSString* deviceName =
861       IsRunningWithXcode6OrLater() ? @"iPhone 5s" : @"iPhone";
862   NSString* simHomePath = nil;
863   NSMutableArray* appArgs = [NSMutableArray array];
864   NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
865   NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
867   NSString* developerDir = FindDeveloperDir();
868   if (!developerDir) {
869     LogError(@"Unable to find developer directory.");
870     exit(kExitInitializationFailure);
871   }
873   NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
874   if (!simulatorFramework) {
875     LogError(@"Failed to load the Simulator Framework.");
876     exit(kExitInitializationFailure);
877   }
879   // Parse the optional arguments
880   int c;
881   while ((c = getopt(argc, argv, "hs:d:u:e:t:l")) != -1) {
882     switch (c) {
883       case 's':
884         sdkVersion = [NSString stringWithUTF8String:optarg];
885         break;
886       case 'd':
887         deviceName = [NSString stringWithUTF8String:optarg];
888         break;
889       case 'u':
890         simHomePath = [[NSFileManager defaultManager]
891             stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
892         break;
893       case 'e': {
894         NSString* envLine = [NSString stringWithUTF8String:optarg];
895         NSRange range = [envLine rangeOfString:@"="];
896         if (range.location == NSNotFound) {
897           LogError(@"Invalid key=value argument for -e.");
898           PrintUsage();
899           exit(kExitInvalidArguments);
900         }
901         NSString* key = [envLine substringToIndex:range.location];
902         NSString* value = [envLine substringFromIndex:(range.location + 1)];
903         [appEnv setObject:value forKey:key];
904       }
905         break;
906       case 't': {
907         int timeout = atoi(optarg);
908         if (timeout > 0) {
909           sessionStartTimeout = static_cast<NSTimeInterval>(timeout);
910         } else {
911           LogError(@"Invalid startup timeout (%s).", optarg);
912           PrintUsage();
913           exit(kExitInvalidArguments);
914         }
915       }
916         break;
917       case 'l':
918         PrintSupportedDevices();
919         exit(kExitSuccess);
920         break;
921       case 'h':
922         PrintUsage();
923         exit(kExitSuccess);
924       default:
925         PrintUsage();
926         exit(kExitInvalidArguments);
927     }
928   }
930   // There should be at least one arg left, specifying the app path. Any
931   // additional args are passed as arguments to the app.
932   if (optind < argc) {
933     appPath = [[NSFileManager defaultManager]
934         stringWithFileSystemRepresentation:argv[optind]
935                                     length:strlen(argv[optind])];
936     appName = [appPath lastPathComponent];
937     while (++optind < argc) {
938       [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
939     }
940   } else {
941     LogError(@"Unable to parse command line arguments.");
942     PrintUsage();
943     exit(kExitInvalidArguments);
944   }
946   // Make sure the app path provided is legit.
947   DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
948   if (!appSpec) {
949     LogError(@"Invalid app path: %@", appPath);
950     exit(kExitInitializationFailure);
951   }
953   // Make sure the SDK path provided is legit (or nil).
954   DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
955   if (!systemRoot) {
956     LogError(@"Invalid SDK version: %@", sdkVersion);
957     PrintSupportedDevices();
958     exit(kExitInitializationFailure);
959   }
961   // Get the paths for stdout and stderr so the simulated app's output will show
962   // up in the caller's stdout/stderr.
963   NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
964   NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
966   // Determine the deviceFamily based on the deviceName
967   NSNumber* deviceFamily = nil;
968   if (IsRunningWithXcode6OrLater()) {
969 #if defined(IOSSIM_USE_XCODE_6)
970     Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
971     if ([simDeviceTypeClass supportedDeviceTypesByAlias][deviceName] == nil) {
972       LogError(@"Invalid device name: %@.", deviceName);
973       PrintSupportedDevices();
974       exit(kExitInvalidArguments);
975     }
976 #endif  // IOSSIM_USE_XCODE_6
977   } else {
978     if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) {
979       deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
980     } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) {
981       deviceFamily = [NSNumber numberWithInt:kIPadFamily];
982     }
983     else {
984       LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'",
985                deviceName);
986       exit(kExitInvalidArguments);
987     }
988   }
990   // Set up the user home directory for the simulator only if a non-default
991   // value was specified.
992   if (simHomePath) {
993     if (!InitializeSimulatorUserHome(simHomePath)) {
994       LogError(@"Unable to initialize home directory for simulator: %@",
995                simHomePath);
996       exit(kExitInitializationFailure);
997     }
998   } else {
999     simHomePath = NSHomeDirectory();
1000   }
1002   // Create the config and simulator session.
1003   DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
1004                                                               systemRoot,
1005                                                               stdioPath,
1006                                                               stdioPath,
1007                                                               appArgs,
1008                                                               appEnv,
1009                                                               deviceFamily,
1010                                                               deviceName);
1011   SimulatorDelegate* delegate =
1012       [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
1013                                        developerDir:developerDir
1014                                       simulatorHome:simHomePath] autorelease];
1015   DTiPhoneSimulatorSession* session = BuildSession(delegate);
1017   // Start the simulator session.
1018   NSError* error;
1019   BOOL started = [session requestStartWithConfig:config
1020                                          timeout:sessionStartTimeout
1021                                            error:&error];
1023   // Spin the runtime indefinitely. When the delegate gets the message that the
1024   // app has quit it will exit this program.
1025   if (started) {
1026     [[NSRunLoop mainRunLoop] run];
1027   } else {
1028     LogError(@"Simulator failed request to start:  \"%@\" (%@:%ld)",
1029              [error localizedDescription],
1030              [error domain], static_cast<long int>([error code]));
1031   }
1033   // Note that this code is only executed if the simulator fails to start
1034   // because once the main run loop is started, only the delegate calling
1035   // exit() will end the program.
1036   [pool drain];
1037   return kExitFailure;