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>
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
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
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
33 @protocol OS_dispatch_source
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
44 @class SimServiceConnectionManager;
45 #import "CoreSimulator.h"
46 #endif // IOSSIM_USE_XCODE_6
48 @interface DVTPlatform : NSObject
49 + (BOOL)loadAllPlatformsReturningError:(id*)arg1;
51 @class DTiPhoneSimulatorApplicationSpecifier;
52 @class DTiPhoneSimulatorSession;
53 @class DTiPhoneSimulatorSessionConfig;
54 @class DTiPhoneSimulatorSystemRoot;
55 @class DVTConfinementServiceConnection;
56 @class DVTDispatchLock;
57 @class DVTiPhoneSimulatorMessenger;
58 @class DVTNotificationToken;
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;
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"
79 // Name of environment variables that control the user's home directory in the
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
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";
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, ...) {
141 va_start(list, format);
144 [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
146 fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
152 void LogWarning(NSString* format, ...) {
154 va_start(list, format);
157 [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
159 fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
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);
169 LogError(@"Failed to find class %@ at runtime.", nameOfClass);
170 exit(kExitInitializationFailure);
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");
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]);
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]);
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");
202 printf(" 'iPad Retina'\n");
203 printf(" 'iPad Retina (64-bit)'\n");
204 #endif // defined(IOSSIM_USE_XCODE_6)
208 // A delegate that is called when the simulated app is started or ended in the
210 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
212 NSString* stdioPath_;
213 NSString* developerDir_;
214 NSString* simulatorHome_;
215 NSThread* outputThread_;
216 NSBundle* simulatorBundle_;
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 {
238 stdioPath_ = [stdioPath copy];
239 developerDir_ = [developerDir copy];
240 simulatorHome_ = [simulatorHome copy];
247 [stdioPath_ release];
248 [developerDir_ release];
249 [simulatorBundle_ release];
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];
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];
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]];
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];
298 NSString *localizedStr =
299 [simulatorBundle_ localizedStringForKey:stringKey
302 if ([localizedStr length])
304 // Failed to get a value, follow Cocoa conventions and use the key as the
309 - (void)session:(DTiPhoneSimulatorSession*)session
310 didStart:(BOOL)started
311 withError:(NSError*)error {
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_]) {
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.
334 LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
335 [error localizedDescription],
336 [error domain], static_cast<long int>([error code]));
337 PrintSupportedDevices();
338 exit(kExitAppFailedToStart);
341 // Start a thread to write contents of outputPath to stdout.
344 [[NSThread alloc] initWithTarget:self
345 selector:@selector(tailOutputForSession:)
347 [outputThread_ start];
350 - (void)session:(DTiPhoneSimulatorSession*)session
351 didEndWithError:(NSError*)error {
353 // Wait for the output thread to finish copying data to stdout.
355 while (![outputThread_ isFinished]) {
356 [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
358 [outputThread_ release];
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
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]));
375 LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
376 localizedDescription, [error domain],
377 static_cast<long int>([error code]));
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);
400 if (snprintf(session_id, 20, "%d", [session simulatedApplicationPID]) < 0) {
401 LogError(@"Failed to get [session simulatedApplicationPID]");
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);
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"))
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;
427 [dataPath stringByAppendingPathComponent:@"Library/Logs/system.log"];
429 NSString* relativePathToSystemLog =
430 [NSString stringWithFormat:
431 @"Library/Logs/iOS Simulator/%@/system.log", versionString];
433 [simulatorHome_ stringByAppendingPathComponent:relativePathToSystemLog];
434 #endif // defined(IOSSIM_USE_XCODE_6)
435 NSFileManager* fileManager = [NSFileManager defaultManager];
436 if ([fileManager fileExistsAtPath:path]) {
438 [NSString stringWithContentsOfFile:path
439 encoding:NSUTF8StringEncoding
441 NSArray* lines = [content componentsSeparatedByCharactersInSet:
442 [NSCharacterSet newlineCharacterSet]];
443 for (NSString* line in lines) {
444 NSString* const kErrorString = @"Service exited with abnormal code:";
445 if ([line rangeOfString:kErrorString].location != NSNotFound) {
446 LogWarning(@"Console message: %@", line);
451 // Remove the log file so subsequent invocations of iossim won't be
452 // looking at stale logs.
453 remove([path fileSystemRepresentation]);
455 LogWarning(@"Unable to find system log at '%@'.", path);
459 // If the query returned any nasty-looking results, iossim should exit with
462 LogError(@"Simulated app crashed or exited with non-zero status");
463 exit(kExitAppCrashed);
471 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
473 NSString* FindDeveloperDir() {
474 // Check the env first.
475 NSDictionary* env = [[NSProcessInfo processInfo] environment];
476 NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"];
477 if ([developerDir length] > 0)
480 // Go look for it via xcode-select.
481 NSTask* xcodeSelectTask = [[[NSTask alloc] init] autorelease];
482 [xcodeSelectTask setLaunchPath:@"/usr/bin/xcode-select"];
483 [xcodeSelectTask setArguments:[NSArray arrayWithObject:@"-print-path"]];
485 NSPipe* outputPipe = [NSPipe pipe];
486 [xcodeSelectTask setStandardOutput:outputPipe];
487 NSFileHandle* outputFile = [outputPipe fileHandleForReading];
489 [xcodeSelectTask launch];
490 NSData* outputData = [outputFile readDataToEndOfFile];
491 [xcodeSelectTask terminate];
494 [[[NSString alloc] initWithData:outputData
495 encoding:NSUTF8StringEncoding] autorelease];
496 output = [output stringByTrimmingCharactersInSet:
497 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
498 if ([output length] == 0)
503 // Loads the Simulator framework from the given developer dir.
504 NSBundle* LoadSimulatorFramework(NSString* developerDir) {
505 // The Simulator framework depends on some of the other Xcode private
506 // frameworks; manually load them first so everything can be linked up.
507 NSString* dvtFoundationPath = [developerDir
508 stringByAppendingPathComponent:kDVTFoundationRelativePath];
509 NSBundle* dvtFoundationBundle =
510 [NSBundle bundleWithPath:dvtFoundationPath];
511 if (![dvtFoundationBundle load])
514 NSString* devToolsFoundationPath = [developerDir
515 stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
516 NSBundle* devToolsFoundationBundle =
517 [NSBundle bundleWithPath:devToolsFoundationPath];
518 if (![devToolsFoundationBundle load])
521 // Prime DVTPlatform.
523 Class DVTPlatformClass = FindClassByName(@"DVTPlatform");
524 if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) {
525 LogError(@"Unable to loadAllPlatformsReturningError. Error: %@",
526 [error localizedDescription]);
530 // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed
531 // (crbug.com/385030).
532 #if defined(IOSSIM_USE_XCODE_6)
533 NSString* coreSimulatorPath = [developerDir
534 stringByAppendingPathComponent:kCoreSimulatorRelativePath];
535 NSBundle* coreSimulatorBundle =
536 [NSBundle bundleWithPath:coreSimulatorPath];
537 if (![coreSimulatorBundle load])
539 #endif // defined(IOSSIM_USE_XCODE_6)
541 NSString* simBundlePath = [developerDir
542 stringByAppendingPathComponent:kSimulatorFrameworkRelativePath];
543 NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
544 if (![simBundle load])
549 // Converts the given app path to an application spec, which requires an
551 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) {
552 Class applicationSpecifierClass =
553 FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier");
554 if (![appPath isAbsolutePath]) {
555 NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath];
556 appPath = [cwd stringByAppendingPathComponent:appPath];
558 appPath = [appPath stringByStandardizingPath];
559 NSFileManager* fileManager = [NSFileManager defaultManager];
560 if (![fileManager fileExistsAtPath:appPath]) {
561 LogError(@"File not found: %@", appPath);
562 exit(kExitInvalidArguments);
564 return [applicationSpecifierClass specifierWithApplicationPath:appPath];
567 // Returns the system root for the given SDK version. If sdkVersion is nil, the
568 // default system root is returned. Will return nil if the sdkVersion is not
570 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
571 Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
572 DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
574 systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
579 // Builds a config object for starting the specified app.
580 DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
581 DTiPhoneSimulatorApplicationSpecifier* appSpec,
582 DTiPhoneSimulatorSystemRoot* systemRoot,
583 NSString* stdoutPath,
584 NSString* stderrPath,
586 NSDictionary* appEnv,
587 NSNumber* deviceFamily,
588 NSString* deviceName) {
589 Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig");
590 DTiPhoneSimulatorSessionConfig* sessionConfig =
591 [[[sessionConfigClass alloc] init] autorelease];
592 sessionConfig.applicationToSimulateOnStart = appSpec;
593 sessionConfig.simulatedSystemRoot = systemRoot;
594 sessionConfig.localizedClientName = @"chromium";
595 sessionConfig.simulatedApplicationStdErrPath = stderrPath;
596 sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
597 sessionConfig.simulatedApplicationLaunchArgs = appArgs;
598 sessionConfig.simulatedApplicationLaunchEnvironment = appEnv;
599 sessionConfig.simulatedDeviceInfoName = deviceName;
600 sessionConfig.simulatedDeviceFamily = deviceFamily;
602 // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed
603 // (crbug.com/385030).
604 #if defined(IOSSIM_USE_XCODE_6)
605 Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
607 [simDeviceTypeClass supportedDeviceTypesByName][deviceName];
608 Class simRuntimeClass = FindClassByName(@"SimRuntime");
609 NSString* identifier = systemRoot.runtime.identifier;
610 id simRuntime = [simRuntimeClass supportedRuntimesByIdentifier][identifier];
612 // Attempt to use an existing device, but create one if a suitable match can't
613 // be found. For example, if the simulator is running with a non-default home
614 // directory (e.g. via iossim's -u command line arg) then there won't be any
615 // devices so one will have to be created.
616 Class simDeviceSetClass = FindClassByName(@"SimDeviceSet");
618 [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]];
620 for (id device in [deviceSet availableDevices]) {
621 if ([device runtime] == simRuntime &&
622 [device deviceType] == simDeviceType) {
628 NSError* error = nil;
629 // n.b. only the device name is necessary because the iOS Simulator menu
630 // already splits devices by runtime version.
631 NSString* name = [NSString stringWithFormat:@"iossim - %@ ", deviceName];
632 simDevice = [deviceSet createDeviceWithType:simDeviceType
637 LogError(@"Failed to create device: %@", error);
638 exit(kExitInitializationFailure);
641 sessionConfig.device = simDevice;
643 return sessionConfig;
646 // Builds a simulator session that will use the given delegate.
647 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
648 Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
649 DTiPhoneSimulatorSession* session =
650 [[[sessionClass alloc] init] autorelease];
651 session.delegate = delegate;
655 // Creates a temporary directory with a unique name based on the provided
656 // template. The template should not contain any path separators and be suffixed
657 // with X's, which will be substituted with a unique alphanumeric string (see
658 // 'man mkdtemp' for details). The directory will be created as a subdirectory
659 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
660 // this method would return something like '/path/to/tempdir/test-3n2'.
662 // Returns the absolute path of the newly-created directory, or nill if unable
663 // to create a unique directory.
664 NSString* CreateTempDirectory(NSString* dirNameTemplate) {
665 NSString* fullPathTemplate =
666 [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
667 char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
668 if (fullPath == NULL)
671 return [NSString stringWithUTF8String:fullPath];
674 // Creates the necessary directory structure under the given user home directory
676 // Returns YES if successful, NO if unable to create the directories.
677 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
678 NSFileManager* fileManager = [NSFileManager defaultManager];
680 // Create user home and subdirectories.
681 NSArray* subDirsToCreate = [NSArray arrayWithObjects:
684 @"Library/Preferences",
686 for (NSString* subDir in subDirsToCreate) {
687 NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
689 if (![fileManager createDirectoryAtPath:path
690 withIntermediateDirectories:YES
693 LogError(@"Unable to create directory: %@. Error: %@",
694 path, [error localizedDescription]);
702 // Creates the necessary directory structure under the given user home directory
703 // path, then sets the path in the appropriate environment variable.
704 // Returns YES if successful, NO if unable to create or initialize the given
706 BOOL InitializeSimulatorUserHome(NSString* userHomePath) {
707 if (!CreateHomeDirSubDirs(userHomePath))
710 // Update the environment to use the specified directory as the user home
712 // Note: the third param of setenv specifies whether or not to overwrite the
713 // variable's value if it has already been set.
714 if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
715 (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
716 LogError(@"Unable to set environment variables for home directory.");
723 // Performs a case-insensitive search to see if |stringToSearch| begins with
724 // |prefixToFind|. Returns true if a match is found.
725 BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch,
726 NSString* prefixToFind) {
727 NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch);
728 NSRange range = [stringToSearch rangeOfString:prefixToFind
730 return range.location != NSNotFound;
733 // Prints the usage information to stderr.
735 fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
736 "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n"
737 " where <appPath> is the path to the .app directory and appArgs are any"
738 " arguments to send the simulated app.\n"
741 " -d Specifies the device (must be one of the values from the iOS"
742 " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n"
743 " -s Specifies the SDK version to use (e.g '4.3')."
744 " Will use system default if not specified.\n"
745 " -u Specifies a user home directory for the simulator."
746 " Will create a new directory if not specified.\n"
747 " -e Specifies an environment key=value pair that will be"
748 " set in the simulated application's environment.\n"
749 " -t Specifies the session startup timeout (in seconds)."
751 " -l List supported devices and iOS versions.\n",
752 static_cast<int>(kDefaultSessionStartTimeoutSeconds));
756 int main(int argc, char* const argv[]) {
757 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
759 // basename() may modify the passed in string and it returns a pointer to an
760 // internal buffer. Give it a copy to modify, and copy what it returns.
761 char* worker = strdup(argv[0]);
762 char* toolName = basename(worker);
763 if (toolName != NULL) {
764 toolName = strdup(toolName);
765 if (toolName != NULL)
766 gToolName = toolName;
771 NSString* appPath = nil;
772 NSString* appName = nil;
773 NSString* sdkVersion = nil;
774 // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed
775 // (crbug.com/385030).
776 #if defined(IOSSIM_USE_XCODE_6)
777 NSString* deviceName = @"iPhone 5";
779 NSString* deviceName = @"iPhone";
781 NSString* simHomePath = nil;
782 NSMutableArray* appArgs = [NSMutableArray array];
783 NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
784 NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
786 NSString* developerDir = FindDeveloperDir();
788 LogError(@"Unable to find developer directory.");
789 exit(kExitInitializationFailure);
792 NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
793 if (!simulatorFramework) {
794 LogError(@"Failed to load the Simulator Framework.");
795 exit(kExitInitializationFailure);
798 // Parse the optional arguments
800 while ((c = getopt(argc, argv, "hs:d:u:e:t:l")) != -1) {
803 sdkVersion = [NSString stringWithUTF8String:optarg];
806 deviceName = [NSString stringWithUTF8String:optarg];
809 simHomePath = [[NSFileManager defaultManager]
810 stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
813 NSString* envLine = [NSString stringWithUTF8String:optarg];
814 NSRange range = [envLine rangeOfString:@"="];
815 if (range.location == NSNotFound) {
816 LogError(@"Invalid key=value argument for -e.");
818 exit(kExitInvalidArguments);
820 NSString* key = [envLine substringToIndex:range.location];
821 NSString* value = [envLine substringFromIndex:(range.location + 1)];
822 [appEnv setObject:value forKey:key];
826 int timeout = atoi(optarg);
828 sessionStartTimeout = static_cast<NSTimeInterval>(timeout);
830 LogError(@"Invalid startup timeout (%s).", optarg);
832 exit(kExitInvalidArguments);
837 PrintSupportedDevices();
845 exit(kExitInvalidArguments);
849 // There should be at least one arg left, specifying the app path. Any
850 // additional args are passed as arguments to the app.
852 appPath = [[NSFileManager defaultManager]
853 stringWithFileSystemRepresentation:argv[optind]
854 length:strlen(argv[optind])];
855 appName = [appPath lastPathComponent];
856 while (++optind < argc) {
857 [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
860 LogError(@"Unable to parse command line arguments.");
862 exit(kExitInvalidArguments);
865 // Make sure the app path provided is legit.
866 DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
868 LogError(@"Invalid app path: %@", appPath);
869 exit(kExitInitializationFailure);
872 // Make sure the SDK path provided is legit (or nil).
873 DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
875 LogError(@"Invalid SDK version: %@", sdkVersion);
876 PrintSupportedDevices();
877 exit(kExitInitializationFailure);
880 // Get the paths for stdout and stderr so the simulated app's output will show
881 // up in the caller's stdout/stderr.
882 NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
883 NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
885 // Determine the deviceFamily based on the deviceName
886 NSNumber* deviceFamily = nil;
887 // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed
888 // (crbug.com/385030).
889 #if defined(IOSSIM_USE_XCODE_6)
890 Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
891 if ([simDeviceTypeClass supportedDeviceTypesByName][deviceName] == nil) {
892 LogError(@"Invalid device name: %@.", deviceName);
893 PrintSupportedDevices();
894 exit(kExitInvalidArguments);
897 if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) {
898 deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
899 } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) {
900 deviceFamily = [NSNumber numberWithInt:kIPadFamily];
903 LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'",
905 exit(kExitInvalidArguments);
907 #endif // !defined(IOSSIM_USE_XCODE_6)
909 // Set up the user home directory for the simulator only if a non-default
910 // value was specified.
912 if (!InitializeSimulatorUserHome(simHomePath)) {
913 LogError(@"Unable to initialize home directory for simulator: %@",
915 exit(kExitInitializationFailure);
918 simHomePath = NSHomeDirectory();
921 // Create the config and simulator session.
922 DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
930 SimulatorDelegate* delegate =
931 [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
932 developerDir:developerDir
933 simulatorHome:simHomePath] autorelease];
934 DTiPhoneSimulatorSession* session = BuildSession(delegate);
936 // Start the simulator session.
938 BOOL started = [session requestStartWithConfig:config
939 timeout:sessionStartTimeout
942 // Spin the runtime indefinitely. When the delegate gets the message that the
943 // app has quit it will exit this program.
945 [[NSRunLoop mainRunLoop] run];
947 LogError(@"Simulator failed request to start: \"%@\" (%@:%ld)",
948 [error localizedDescription],
949 [error domain], static_cast<long int>([error code]));
952 // Note that this code is only executed if the simulator fails to start
953 // because once the main run loop is started, only the delegate calling
954 // exit() will end the program.