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.
6 #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 // 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)
35 @class DVTSimulatorApplication;
39 @class SimServiceConnectionManager;
40 #import "CoreSimulator.h"
41 #endif // IOSSIM_USE_XCODE_6
43 @interface DVTPlatform : NSObject
44 + (BOOL)loadAllPlatformsReturningError:(id*)arg1;
46 @class DTiPhoneSimulatorApplicationSpecifier;
47 @class DTiPhoneSimulatorSession;
48 @class DTiPhoneSimulatorSessionConfig;
49 @class DTiPhoneSimulatorSystemRoot;
50 @class DVTConfinementServiceConnection;
51 @class DVTDispatchLock;
52 @class DVTiPhoneSimulatorMessenger;
53 @class DVTNotificationToken;
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;
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"
74 // Name of environment variables that control the user's home directory in the
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
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, ...) {
124 va_start(list, format);
127 [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
129 fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
135 void LogWarning(NSString* format, ...) {
137 va_start(list, format);
140 [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
142 fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
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);
152 LogError(@"Failed to find class %@ at runtime.", nameOfClass);
153 exit(kExitInitializationFailure);
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];
169 NSData* outputData = [outputFile readDataToEndOfFile];
170 [task waitUntilExit];
171 if ([task isRunning]) {
172 LogError(@"Task '%@ %@' is still running.",
174 [args componentsJoinedByString:@" "]);
176 } else if ([task terminationStatus]) {
177 LogError(@"Task '%@ %@' exited with return code %d.",
179 [args componentsJoinedByString:@" "],
180 [task terminationStatus]);
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:
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",
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];
200 [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
201 intoString:&version];
203 LogError(@"Unable to find Xcode version. 'xcodebuild -version' "
204 @"returned \n%@", output);
210 // Returns true if iossim is running with Xcode 6 or later installed on the
212 BOOL IsRunningWithXcode6OrLater() {
213 static NSString* xcodeVersion = FindXcodeVersion();
217 NSArray* components = [xcodeVersion componentsSeparatedByString:@"."];
218 if ([components count] < 1) {
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");
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]);
239 #endif // IOSSIM_USE_XCODE_6
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]);
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");
253 printf(" 'iPad Retina'\n");
254 printf(" 'iPad Retina (64-bit)'\n");
259 // A delegate that is called when the simulated app is started or ended in the
261 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
263 NSString* stdioPath_;
264 NSString* developerDir_;
265 NSString* simulatorHome_;
266 NSThread* outputThread_;
267 NSBundle* simulatorBundle_;
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 {
289 stdioPath_ = [stdioPath copy];
290 developerDir_ = [developerDir copy];
291 simulatorHome_ = [simulatorHome copy];
298 [stdioPath_ release];
299 [developerDir_ release];
300 [simulatorBundle_ release];
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];
328 #endif // IOSSIM_USE_XCODE_6
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];
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]];
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];
354 NSString *localizedStr =
355 [simulatorBundle_ localizedStringForKey:stringKey
358 if ([localizedStr length])
360 // Failed to get a value, follow Cocoa conventions and use the key as the
365 - (void)session:(DTiPhoneSimulatorSession*)session
366 didStart:(BOOL)started
367 withError:(NSError*)error {
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_]) {
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.
390 LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
391 [error localizedDescription],
392 [error domain], static_cast<long int>([error code]));
393 PrintSupportedDevices();
394 exit(kExitAppFailedToStart);
397 // Start a thread to write contents of outputPath to stdout.
400 [[NSThread alloc] initWithTarget:self
401 selector:@selector(tailOutputForSession:)
403 [outputThread_ start];
406 - (void)session:(DTiPhoneSimulatorSession*)session
407 didEndWithError:(NSError*)error {
409 // Wait for the output thread to finish copying data to stdout.
411 while (![outputThread_ isFinished]) {
412 [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
414 [outputThread_ release];
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
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]));
431 LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
432 localizedDescription, [error domain],
433 static_cast<long int>([error code]));
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);
456 if (snprintf(session_id, 20, "%d", [session simulatedApplicationPID]) < 0) {
457 LogError(@"Failed to get [session simulatedApplicationPID]");
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);
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"))
476 // Otherwise, the iOS Simulator's system logging is sandboxed, so parse the
477 // sandboxed system.log file for known errors.
479 if (IsRunningWithXcode6OrLater()) {
480 #if defined(IOSSIM_USE_XCODE_6)
481 NSString* dataPath = session.sessionConfig.device.dataPath;
483 [dataPath stringByAppendingPathComponent:@"Library/Logs/system.log"];
484 #endif // IOSSIM_USE_XCODE_6
486 NSString* relativePathToSystemLog =
487 [NSString stringWithFormat:
488 @"Library/Logs/iOS Simulator/%@/system.log", versionString];
489 path = [simulatorHome_
490 stringByAppendingPathComponent:relativePathToSystemLog];
492 NSFileManager* fileManager = [NSFileManager defaultManager];
493 if ([fileManager fileExistsAtPath:path]) {
495 [NSString stringWithContentsOfFile:path
496 encoding:NSUTF8StringEncoding
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:",
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);
520 // Remove the log file so subsequent invocations of iossim won't be
521 // looking at stale logs.
522 remove([path fileSystemRepresentation]);
524 LogWarning(@"Unable to find system log at '%@'.", path);
528 // If the query returned any nasty-looking results, iossim should exit with
531 LogError(@"Simulated app crashed or exited with non-zero status");
532 exit(kExitAppCrashed);
540 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
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)
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)
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])
570 NSString* devToolsFoundationPath = [developerDir
571 stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
572 NSBundle* devToolsFoundationBundle =
573 [NSBundle bundleWithPath:devToolsFoundationPath];
574 if (![devToolsFoundationBundle load])
577 // Prime DVTPlatform.
579 Class DVTPlatformClass = FindClassByName(@"DVTPlatform");
580 if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) {
581 LogError(@"Unable to loadAllPlatformsReturningError. Error: %@",
582 [error localizedDescription]);
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])
600 simulatorFrameworkRelativePath =
601 @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/"
602 @"DVTiPhoneSimulatorRemoteClient.framework";
604 NSString* simBundlePath = [developerDir
605 stringByAppendingPathComponent:simulatorFrameworkRelativePath];
606 NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
607 if (![simBundle load])
612 // Converts the given app path to an application spec, which requires an
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];
621 appPath = [appPath stringByStandardizingPath];
622 NSFileManager* fileManager = [NSFileManager defaultManager];
623 if (![fileManager fileExistsAtPath:appPath]) {
624 LogError(@"File not found: %@", appPath);
625 exit(kExitInvalidArguments);
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
633 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
634 Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
635 DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
637 systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
642 // Builds a config object for starting the specified app.
643 DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
644 DTiPhoneSimulatorApplicationSpecifier* appSpec,
645 DTiPhoneSimulatorSystemRoot* systemRoot,
646 NSString* stdoutPath,
647 NSString* stderrPath,
649 NSDictionary* appEnv,
650 NSNumber* deviceFamily,
651 NSString* deviceName) {
652 Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig");
653 DTiPhoneSimulatorSessionConfig* sessionConfig =
654 [[[sessionConfigClass alloc] init] autorelease];
655 sessionConfig.applicationToSimulateOnStart = appSpec;
656 sessionConfig.simulatedSystemRoot = systemRoot;
657 sessionConfig.localizedClientName = @"chromium";
658 sessionConfig.simulatedApplicationStdErrPath = stderrPath;
659 sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
660 sessionConfig.simulatedApplicationLaunchArgs = appArgs;
661 sessionConfig.simulatedApplicationLaunchEnvironment = appEnv;
662 sessionConfig.simulatedDeviceInfoName = deviceName;
663 sessionConfig.simulatedDeviceFamily = deviceFamily;
665 if (IsRunningWithXcode6OrLater()) {
666 #if defined(IOSSIM_USE_XCODE_6)
667 Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
669 [simDeviceTypeClass supportedDeviceTypesByName][deviceName];
670 Class simRuntimeClass = FindClassByName(@"SimRuntime");
671 NSString* identifier = systemRoot.runtime.identifier;
672 id simRuntime = [simRuntimeClass supportedRuntimesByIdentifier][identifier];
674 // Attempt to use an existing device, but create one if a suitable match
675 // can't be found. For example, if the simulator is running with a
676 // non-default home directory (e.g. via iossim's -u command line arg) then
677 // there won't be any devices so one will have to be created.
678 Class simDeviceSetClass = FindClassByName(@"SimDeviceSet");
680 [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]];
682 for (id device in [deviceSet availableDevices]) {
683 if ([device runtime] == simRuntime &&
684 [device deviceType] == simDeviceType) {
690 NSError* error = nil;
691 // n.b. only the device name is necessary because the iOS Simulator menu
692 // already splits devices by runtime version.
693 NSString* name = [NSString stringWithFormat:@"iossim - %@ ", deviceName];
694 simDevice = [deviceSet createDeviceWithType:simDeviceType
699 LogError(@"Failed to create device: %@", error);
700 exit(kExitInitializationFailure);
703 sessionConfig.device = simDevice;
704 #endif // IOSSIM_USE_XCODE_6
706 return sessionConfig;
709 // Builds a simulator session that will use the given delegate.
710 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
711 Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
712 DTiPhoneSimulatorSession* session =
713 [[[sessionClass alloc] init] autorelease];
714 session.delegate = delegate;
718 // Creates a temporary directory with a unique name based on the provided
719 // template. The template should not contain any path separators and be suffixed
720 // with X's, which will be substituted with a unique alphanumeric string (see
721 // 'man mkdtemp' for details). The directory will be created as a subdirectory
722 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
723 // this method would return something like '/path/to/tempdir/test-3n2'.
725 // Returns the absolute path of the newly-created directory, or nill if unable
726 // to create a unique directory.
727 NSString* CreateTempDirectory(NSString* dirNameTemplate) {
728 NSString* fullPathTemplate =
729 [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
730 char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
731 if (fullPath == NULL)
734 return [NSString stringWithUTF8String:fullPath];
737 // Creates the necessary directory structure under the given user home directory
739 // Returns YES if successful, NO if unable to create the directories.
740 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
741 NSFileManager* fileManager = [NSFileManager defaultManager];
743 // Create user home and subdirectories.
744 NSArray* subDirsToCreate = [NSArray arrayWithObjects:
747 @"Library/Preferences",
749 for (NSString* subDir in subDirsToCreate) {
750 NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
752 if (![fileManager createDirectoryAtPath:path
753 withIntermediateDirectories:YES
756 LogError(@"Unable to create directory: %@. Error: %@",
757 path, [error localizedDescription]);
765 // Creates the necessary directory structure under the given user home directory
766 // path, then sets the path in the appropriate environment variable.
767 // Returns YES if successful, NO if unable to create or initialize the given
769 BOOL InitializeSimulatorUserHome(NSString* userHomePath) {
770 if (!CreateHomeDirSubDirs(userHomePath))
773 // Update the environment to use the specified directory as the user home
775 // Note: the third param of setenv specifies whether or not to overwrite the
776 // variable's value if it has already been set.
777 if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
778 (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
779 LogError(@"Unable to set environment variables for home directory.");
786 // Performs a case-insensitive search to see if |stringToSearch| begins with
787 // |prefixToFind|. Returns true if a match is found.
788 BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch,
789 NSString* prefixToFind) {
790 NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch);
791 NSRange range = [stringToSearch rangeOfString:prefixToFind
793 return range.location != NSNotFound;
796 // Prints the usage information to stderr.
798 fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
799 "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n"
800 " where <appPath> is the path to the .app directory and appArgs are any"
801 " arguments to send the simulated app.\n"
804 " -d Specifies the device (must be one of the values from the iOS"
805 " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n"
806 " -s Specifies the SDK version to use (e.g '4.3')."
807 " Will use system default if not specified.\n"
808 " -u Specifies a user home directory for the simulator."
809 " Will create a new directory if not specified.\n"
810 " -e Specifies an environment key=value pair that will be"
811 " set in the simulated application's environment.\n"
812 " -t Specifies the session startup timeout (in seconds)."
814 " -l List supported devices and iOS versions.\n",
815 static_cast<int>(kDefaultSessionStartTimeoutSeconds));
819 void EnsureSupportForCurrentXcodeVersion() {
820 if (IsRunningWithXcode6OrLater()) {
821 #if !IOSSIM_USE_XCODE_6
822 LogError(@"Running on Xcode 6, but Xcode 6 support was not compiled in.");
823 exit(kExitUnsupportedXcodeVersion);
824 #endif // IOSSIM_USE_XCODE_6
828 int main(int argc, char* const argv[]) {
829 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
831 EnsureSupportForCurrentXcodeVersion();
833 // basename() may modify the passed in string and it returns a pointer to an
834 // internal buffer. Give it a copy to modify, and copy what it returns.
835 char* worker = strdup(argv[0]);
836 char* toolName = basename(worker);
837 if (toolName != NULL) {
838 toolName = strdup(toolName);
839 if (toolName != NULL)
840 gToolName = toolName;
845 NSString* appPath = nil;
846 NSString* appName = nil;
847 NSString* sdkVersion = nil;
848 NSString* deviceName =
849 IsRunningWithXcode6OrLater() ? @"iPhone 5s" : @"iPhone";
850 NSString* simHomePath = nil;
851 NSMutableArray* appArgs = [NSMutableArray array];
852 NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
853 NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
855 NSString* developerDir = FindDeveloperDir();
857 LogError(@"Unable to find developer directory.");
858 exit(kExitInitializationFailure);
861 NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
862 if (!simulatorFramework) {
863 LogError(@"Failed to load the Simulator Framework.");
864 exit(kExitInitializationFailure);
867 // Parse the optional arguments
869 while ((c = getopt(argc, argv, "hs:d:u:e:t:l")) != -1) {
872 sdkVersion = [NSString stringWithUTF8String:optarg];
875 deviceName = [NSString stringWithUTF8String:optarg];
878 simHomePath = [[NSFileManager defaultManager]
879 stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
882 NSString* envLine = [NSString stringWithUTF8String:optarg];
883 NSRange range = [envLine rangeOfString:@"="];
884 if (range.location == NSNotFound) {
885 LogError(@"Invalid key=value argument for -e.");
887 exit(kExitInvalidArguments);
889 NSString* key = [envLine substringToIndex:range.location];
890 NSString* value = [envLine substringFromIndex:(range.location + 1)];
891 [appEnv setObject:value forKey:key];
895 int timeout = atoi(optarg);
897 sessionStartTimeout = static_cast<NSTimeInterval>(timeout);
899 LogError(@"Invalid startup timeout (%s).", optarg);
901 exit(kExitInvalidArguments);
906 PrintSupportedDevices();
914 exit(kExitInvalidArguments);
918 // There should be at least one arg left, specifying the app path. Any
919 // additional args are passed as arguments to the app.
921 appPath = [[NSFileManager defaultManager]
922 stringWithFileSystemRepresentation:argv[optind]
923 length:strlen(argv[optind])];
924 appName = [appPath lastPathComponent];
925 while (++optind < argc) {
926 [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
929 LogError(@"Unable to parse command line arguments.");
931 exit(kExitInvalidArguments);
934 // Make sure the app path provided is legit.
935 DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
937 LogError(@"Invalid app path: %@", appPath);
938 exit(kExitInitializationFailure);
941 // Make sure the SDK path provided is legit (or nil).
942 DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
944 LogError(@"Invalid SDK version: %@", sdkVersion);
945 PrintSupportedDevices();
946 exit(kExitInitializationFailure);
949 // Get the paths for stdout and stderr so the simulated app's output will show
950 // up in the caller's stdout/stderr.
951 NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
952 NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
954 // Determine the deviceFamily based on the deviceName
955 NSNumber* deviceFamily = nil;
956 if (IsRunningWithXcode6OrLater()) {
957 #if defined(IOSSIM_USE_XCODE_6)
958 Class simDeviceTypeClass = FindClassByName(@"SimDeviceType");
959 if ([simDeviceTypeClass supportedDeviceTypesByName][deviceName] == nil) {
960 LogError(@"Invalid device name: %@.", deviceName);
961 PrintSupportedDevices();
962 exit(kExitInvalidArguments);
964 #endif // IOSSIM_USE_XCODE_6
966 if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) {
967 deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
968 } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) {
969 deviceFamily = [NSNumber numberWithInt:kIPadFamily];
972 LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'",
974 exit(kExitInvalidArguments);
978 // Set up the user home directory for the simulator only if a non-default
979 // value was specified.
981 if (!InitializeSimulatorUserHome(simHomePath)) {
982 LogError(@"Unable to initialize home directory for simulator: %@",
984 exit(kExitInitializationFailure);
987 simHomePath = NSHomeDirectory();
990 // Create the config and simulator session.
991 DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
999 SimulatorDelegate* delegate =
1000 [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
1001 developerDir:developerDir
1002 simulatorHome:simHomePath] autorelease];
1003 DTiPhoneSimulatorSession* session = BuildSession(delegate);
1005 // Start the simulator session.
1007 BOOL started = [session requestStartWithConfig:config
1008 timeout:sessionStartTimeout
1011 // Spin the runtime indefinitely. When the delegate gets the message that the
1012 // app has quit it will exit this program.
1014 [[NSRunLoop mainRunLoop] run];
1016 LogError(@"Simulator failed request to start: \"%@\" (%@:%ld)",
1017 [error localizedDescription],
1018 [error domain], static_cast<long int>([error code]));
1021 // Note that this code is only executed if the simulator fails to start
1022 // because once the main run loop is started, only the delegate calling
1023 // exit() will end the program.
1025 return kExitFailure;