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