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