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