// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #import #include #include #include // An executable (iossim) that runs an app in the iOS Simulator. // Run 'iossim -h' for usage information. // // For best results, the iOS Simulator application should not be running when // iossim is invoked. // // Headers for iPhoneSimulatorRemoteClient and other frameworks used in this // tool are generated by class-dump, via GYP. // (class-dump is available at http://www.codethecode.com/projects/class-dump/) // // However, there are some forward declarations required to get things to // compile. // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed // (crbug.com/385030). #if defined(IOSSIM_USE_XCODE_6) @class DVTStackBacktrace; #import "DVTFoundation.h" #endif // IOSSIM_USE_XCODE_6 // TODO(lliabraa): Once all builders are on Xcode 6 this ifdef can be removed // (crbug.com/385030). #if defined(IOSSIM_USE_XCODE_6) @protocol SimBridge; @class DVTSimulatorApplication; @class SimDeviceSet; @class SimDeviceType; @class SimProfilesPathMonitor; @class SimRuntime; @class SimServiceConnectionManager; #import "CoreSimulator.h" #endif // IOSSIM_USE_XCODE_6 @interface DVTPlatform : NSObject + (BOOL)loadAllPlatformsReturningError:(id*)arg1; @end @class DTiPhoneSimulatorApplicationSpecifier; @class DTiPhoneSimulatorSession; @class DTiPhoneSimulatorSessionConfig; @class DTiPhoneSimulatorSystemRoot; @class DVTConfinementServiceConnection; @class DVTDispatchLock; @class DVTiPhoneSimulatorMessenger; @class DVTNotificationToken; @class DVTTask; // The DTiPhoneSimulatorSessionDelegate protocol is referenced // by the iPhoneSimulatorRemoteClient framework, but not defined in the object // file, so it must be defined here before importing the generated // iPhoneSimulatorRemoteClient.h file. @protocol DTiPhoneSimulatorSessionDelegate - (void)session:(DTiPhoneSimulatorSession*)session didEndWithError:(NSError*)error; - (void)session:(DTiPhoneSimulatorSession*)session didStart:(BOOL)started withError:(NSError*)error; @end #import "DVTiPhoneSimulatorRemoteClient.h" // An undocumented system log key included in messages from launchd. The value // is the PID of the process the message is about (as opposed to launchd's PID). #define ASL_KEY_REF_PID "RefPID" namespace { // Name of environment variables that control the user's home directory in the // simulator. const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME"; const char* const kHomeEnvVariable = "HOME"; // Device family codes for iPhone and iPad. const int kIPhoneFamily = 1; const int kIPadFamily = 2; // Max number of seconds to wait for the simulator session to start. // This timeout must allow time to start up iOS Simulator, install the app // and perform any other black magic that is encoded in the // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up // time is only a couple seconds but machine load, disk caches, etc., can all // affect startup time in the wild so the timeout needs to be fairly generous. // If this timeout occurs iossim will likely exit with non-zero status; the // exception being if the app is invoked and completes execution before the // session is started (this case is handled in session:didStart:withError). const NSTimeInterval kDefaultSessionStartTimeoutSeconds = 30; // While the simulated app is running, its stdout is redirected to a file which // is polled by iossim and written to iossim's stdout using the following // polling interval. const NSTimeInterval kOutputPollIntervalSeconds = 0.1; NSString* const kDVTFoundationRelativePath = @"../SharedFrameworks/DVTFoundation.framework"; NSString* const kDevToolsFoundationRelativePath = @"../OtherFrameworks/DevToolsFoundation.framework"; NSString* const kSimulatorRelativePath = @"Platforms/iPhoneSimulator.platform/Developer/Applications/" @"iPhone Simulator.app"; // Simulator Error String Key. This can be found by looking in the Simulator's // Localizable.strings files. NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit."; const char* gToolName = "iossim"; // Exit status codes. const int kExitSuccess = EXIT_SUCCESS; const int kExitFailure = EXIT_FAILURE; const int kExitInvalidArguments = 2; const int kExitInitializationFailure = 3; const int kExitAppFailedToStart = 4; const int kExitAppCrashed = 5; const int kExitUnsupportedXcodeVersion = 6; void LogError(NSString* format, ...) { va_list list; va_start(list, format); NSString* message = [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]); fflush(stderr); va_end(list); } void LogWarning(NSString* format, ...) { va_list list; va_start(list, format); NSString* message = [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]); fflush(stderr); va_end(list); } // Helper to find a class by name and die if it isn't found. Class FindClassByName(NSString* nameOfClass) { Class theClass = NSClassFromString(nameOfClass); if (!theClass) { LogError(@"Failed to find class %@ at runtime.", nameOfClass); exit(kExitInitializationFailure); } return theClass; } // Returns the a NSString containing the stdout from running an NSTask that // launches |toolPath| with th given command line |args|. NSString* GetOutputFromTask(NSString* toolPath, NSArray* args) { NSTask* task = [[[NSTask alloc] init] autorelease]; [task setLaunchPath:toolPath]; [task setArguments:args]; NSPipe* outputPipe = [NSPipe pipe]; [task setStandardOutput:outputPipe]; NSFileHandle* outputFile = [outputPipe fileHandleForReading]; [task launch]; NSData* outputData = [outputFile readDataToEndOfFile]; [task waitUntilExit]; if ([task isRunning]) { LogError(@"Task '%@ %@' is still running.", toolPath, [args componentsJoinedByString:@" "]); return nil; } else if ([task terminationStatus]) { LogError(@"Task '%@ %@' exited with return code %d.", toolPath, [args componentsJoinedByString:@" "], [task terminationStatus]); return nil; } return [[[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding] autorelease]; } // Finds the Xcode version via xcodebuild -version. Output from xcodebuild is // expected to look like: // Xcode // Build version 5B130a // where is the string returned by this function (e.g. 6.0). NSString* FindXcodeVersion() { NSString* output = GetOutputFromTask(@"/usr/bin/xcodebuild", @[ @"-version" ]); // Scan past the "Xcode ", then scan the rest of the line into |version|. NSScanner* scanner = [NSScanner scannerWithString:output]; BOOL valid = [scanner scanString:@"Xcode " intoString:NULL]; NSString* version; valid = [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet] intoString:&version]; if (!valid) { LogError(@"Unable to find Xcode version. 'xcodebuild -version' " @"returned \n%@", output); return nil; } return version; } // Returns true if iossim is running with Xcode 6 or later installed on the // host. BOOL IsRunningWithXcode6OrLater() { static NSString* xcodeVersion = FindXcodeVersion(); if (!xcodeVersion) { return false; } NSArray* components = [xcodeVersion componentsSeparatedByString:@"."]; if ([components count] < 1) { return false; } NSInteger majorVersion = [[components objectAtIndex:0] integerValue]; return majorVersion >= 6; } // Prints supported devices and SDKs. void PrintSupportedDevices() { if (IsRunningWithXcode6OrLater()) { #if defined(IOSSIM_USE_XCODE_6) printf("Supported device/SDK combinations:\n"); Class simDeviceSetClass = FindClassByName(@"SimDeviceSet"); id deviceSet = [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]]; for (id simDevice in [deviceSet availableDevices]) { NSString* deviceInfo = [NSString stringWithFormat:@" -d '%@' -s '%@'\n", [simDevice name], [[simDevice runtime] versionString]]; printf("%s", [deviceInfo UTF8String]); } #endif // IOSSIM_USE_XCODE_6 } else { printf("Supported SDK versions:\n"); Class rootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot"); for (id root in [rootClass knownRoots]) { printf(" '%s'\n", [[root sdkVersion] UTF8String]); } // This is the list of devices supported on Xcode 5.1.x. printf("Supported devices:\n"); printf(" 'iPhone'\n"); printf(" 'iPhone Retina (3.5-inch)'\n"); printf(" 'iPhone Retina (4-inch)'\n"); printf(" 'iPhone Retina (4-inch 64-bit)'\n"); printf(" 'iPad'\n"); printf(" 'iPad Retina'\n"); printf(" 'iPad Retina (64-bit)'\n"); } } } // namespace // A delegate that is called when the simulated app is started or ended in the // simulator. @interface SimulatorDelegate : NSObject { @private NSString* stdioPath_; NSString* developerDir_; NSString* simulatorHome_; NSThread* outputThread_; NSBundle* simulatorBundle_; BOOL appRunning_; } @end // An implementation that copies the simulated app's stdio to stdout of this // executable. While it would be nice to get stdout and stderr independently // from iOS Simulator, issues like I/O buffering and interleaved output // between iOS Simulator and the app would cause iossim to display things out // of order here. Printing all output to a single file keeps the order correct. // Instances of this classe should be initialized with the location of the // simulated app's output file. When the simulated app starts, a thread is // started which handles copying data from the simulated app's output file to // the stdout of this executable. @implementation SimulatorDelegate // Specifies the file locations of the simulated app's stdout and stderr. - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath developerDir:(NSString*)developerDir simulatorHome:(NSString*)simulatorHome { self = [super init]; if (self) { stdioPath_ = [stdioPath copy]; developerDir_ = [developerDir copy]; simulatorHome_ = [simulatorHome copy]; } return self; } - (void)dealloc { [stdioPath_ release]; [developerDir_ release]; [simulatorBundle_ release]; [super dealloc]; } // Reads data from the simulated app's output and writes it to stdout. This // method blocks, so it should be called in a separate thread. The iOS // Simulator takes a file path for the simulated app's stdout and stderr, but // this path isn't always available (e.g. when the stdout is Xcode's build // window). As a workaround, iossim creates a temp file to hold output, which // this method reads and copies to stdout. - (void)tailOutputForSession:(DTiPhoneSimulatorSession*)session { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_]; if (IsRunningWithXcode6OrLater()) { #if defined(IOSSIM_USE_XCODE_6) // With iOS 8 simulators on Xcode 6, the app output is relative to the // simulator's data directory. NSString* versionString = [[[session sessionConfig] simulatedSystemRoot] sdkVersion]; NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."] objectAtIndex:0] intValue]; if (majorVersion >= 8) { NSString* dataPath = session.sessionConfig.device.dataPath; NSString* appOutput = [dataPath stringByAppendingPathComponent:stdioPath_]; simio = [NSFileHandle fileHandleForReadingAtPath:appOutput]; } #endif // IOSSIM_USE_XCODE_6 } NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput]; // Copy data to stdout/stderr while the app is running. while (appRunning_) { NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init]; [standardOutput writeData:[simio readDataToEndOfFile]]; [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; [innerPool drain]; } // Once the app is no longer running, copy any data that was written during // the last sleep cycle. [standardOutput writeData:[simio readDataToEndOfFile]]; [pool drain]; } // Fetches a localized error string from the Simulator. - (NSString *)localizedSimulatorErrorString:(NSString*)stringKey { // Lazy load of the simulator bundle. if (simulatorBundle_ == nil) { NSString* simulatorPath = [developerDir_ stringByAppendingPathComponent:kSimulatorRelativePath]; simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath]; } NSString *localizedStr = [simulatorBundle_ localizedStringForKey:stringKey value:nil table:nil]; if ([localizedStr length]) return localizedStr; // Failed to get a value, follow Cocoa conventions and use the key as the // string. return stringKey; } - (void)session:(DTiPhoneSimulatorSession*)session didStart:(BOOL)started withError:(NSError*)error { if (!started) { // If the test executes very quickly (<30ms), the SimulatorDelegate may not // get the initial session:started:withError: message indicating successful // startup of the simulated app. Instead the delegate will get a // session:started:withError: message after the timeout has elapsed. To // account for this case, check if the simulated app's stdio file was // ever created and if it exists dump it to stdout and return success. NSFileManager* fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:stdioPath_]) { appRunning_ = NO; [self tailOutputForSession:session]; // Note that exiting in this state leaves a process running // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will // prevent future simulator sessions from being started for 30 seconds // unless the iOS Simulator application is killed altogether. [self session:session didEndWithError:nil]; // session:didEndWithError should not return (because it exits) so // the execution path should never get here. exit(kExitFailure); } LogError(@"Simulator failed to start: \"%@\" (%@:%ld)", [error localizedDescription], [error domain], static_cast([error code])); PrintSupportedDevices(); exit(kExitAppFailedToStart); } // Start a thread to write contents of outputPath to stdout. appRunning_ = YES; outputThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(tailOutputForSession:) object:session]; [outputThread_ start]; } - (void)session:(DTiPhoneSimulatorSession*)session didEndWithError:(NSError*)error { appRunning_ = NO; // Wait for the output thread to finish copying data to stdout. if (outputThread_) { while (![outputThread_ isFinished]) { [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; } [outputThread_ release]; outputThread_ = nil; } if (error) { // There appears to be a race condition where sometimes the simulator // framework will end with an error, but the error is that the simulated // app cleanly shut down; try to trap this error and don't fail the // simulator run. NSString* localizedDescription = [error localizedDescription]; NSString* ignorableErrorStr = [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey]; if ([ignorableErrorStr isEqual:localizedDescription]) { LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)", localizedDescription, [error domain], static_cast([error code])); } else { LogError(@"Simulator ended with error: \"%@\" (%@:%ld)", localizedDescription, [error domain], static_cast([error code])); exit(kExitFailure); } } // Try to determine if the simulated app crashed or quit with a non-zero // status code. iOS Simluator handles things a bit differently depending on // the version, so first determine the iOS version being used. BOOL badEntryFound = NO; NSString* versionString = [[[session sessionConfig] simulatedSystemRoot] sdkVersion]; NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."] objectAtIndex:0] intValue]; if (majorVersion <= 6) { // In iOS 6 and before, logging from the simulated apps went to the main // system logs, so use ASL to check if the simulated app exited abnormally // by looking for system log messages from launchd that refer to the // simulated app's PID. Limit query to messages in the last minute since // PIDs are cyclical. aslmsg query = asl_new(ASL_TYPE_QUERY); asl_set_query(query, ASL_KEY_SENDER, "launchd", ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING); char session_id[20]; if (snprintf(session_id, 20, "%d", [session simulatedApplicationPID]) < 0) { LogError(@"Failed to get [session simulatedApplicationPID]"); exit(kExitFailure); } asl_set_query(query, ASL_KEY_REF_PID, session_id, ASL_QUERY_OP_EQUAL); asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL); // Log any messages found, and take note of any messages that may indicate // the app crashed or did not exit cleanly. aslresponse response = asl_search(NULL, query); aslmsg entry; while ((entry = aslresponse_next(response)) != NULL) { const char* message = asl_get(entry, ASL_KEY_MSG); LogWarning(@"Console message: %s", message); // Some messages are harmless, so don't trigger a failure for them. if (strstr(message, "The following job tried to hijack the service")) continue; badEntryFound = YES; } } else { // Otherwise, the iOS Simulator's system logging is sandboxed, so parse the // sandboxed system.log file for known errors. NSString* path; if (IsRunningWithXcode6OrLater()) { #if defined(IOSSIM_USE_XCODE_6) NSString* dataPath = session.sessionConfig.device.dataPath; path = [dataPath stringByAppendingPathComponent:@"Library/Logs/system.log"]; #endif // IOSSIM_USE_XCODE_6 } else { NSString* relativePathToSystemLog = [NSString stringWithFormat: @"Library/Logs/iOS Simulator/%@/system.log", versionString]; path = [simulatorHome_ stringByAppendingPathComponent:relativePathToSystemLog]; } NSFileManager* fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:path]) { NSString* content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; NSArray* lines = [content componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]]; NSString* simulatedAppPID = [NSString stringWithFormat:@"%d", session.simulatedApplicationPID]; NSArray* kErrorStrings = @[ @"Service exited with abnormal code:", @"Service exited due to signal:", ]; for (NSString* line in lines) { if ([line rangeOfString:simulatedAppPID].location != NSNotFound) { for (NSString* errorString in kErrorStrings) { if ([line rangeOfString:errorString].location != NSNotFound) { LogWarning(@"Console message: %@", line); badEntryFound = YES; break; } } if (badEntryFound) { break; } } } // Remove the log file so subsequent invocations of iossim won't be // looking at stale logs. remove([path fileSystemRepresentation]); } else { LogWarning(@"Unable to find system log at '%@'.", path); } } // If the query returned any nasty-looking results, iossim should exit with // non-zero status. if (badEntryFound) { LogError(@"Simulated app crashed or exited with non-zero status"); exit(kExitAppCrashed); } exit(kExitSuccess); } @end namespace { // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment // variable. NSString* FindDeveloperDir() { // Check the env first. NSDictionary* env = [[NSProcessInfo processInfo] environment]; NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"]; if ([developerDir length] > 0) return developerDir; // Go look for it via xcode-select. NSString* output = GetOutputFromTask(@"/usr/bin/xcode-select", @[ @"-print-path" ]); output = [output stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ([output length] == 0) output = nil; return output; } // Loads the Simulator framework from the given developer dir. NSBundle* LoadSimulatorFramework(NSString* developerDir) { // The Simulator framework depends on some of the other Xcode private // frameworks; manually load them first so everything can be linked up. NSString* dvtFoundationPath = [developerDir stringByAppendingPathComponent:kDVTFoundationRelativePath]; NSBundle* dvtFoundationBundle = [NSBundle bundleWithPath:dvtFoundationPath]; if (![dvtFoundationBundle load]) return nil; NSString* devToolsFoundationPath = [developerDir stringByAppendingPathComponent:kDevToolsFoundationRelativePath]; NSBundle* devToolsFoundationBundle = [NSBundle bundleWithPath:devToolsFoundationPath]; if (![devToolsFoundationBundle load]) return nil; // Prime DVTPlatform. NSError* error; Class DVTPlatformClass = FindClassByName(@"DVTPlatform"); if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) { LogError(@"Unable to loadAllPlatformsReturningError. Error: %@", [error localizedDescription]); return nil; } // The path within the developer dir of the private Simulator frameworks. NSString* simulatorFrameworkRelativePath; if (IsRunningWithXcode6OrLater()) { simulatorFrameworkRelativePath = @"../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework"; NSString* const kCoreSimulatorRelativePath = @"Library/PrivateFrameworks/CoreSimulator.framework"; NSString* coreSimulatorPath = [developerDir stringByAppendingPathComponent:kCoreSimulatorRelativePath]; NSBundle* coreSimulatorBundle = [NSBundle bundleWithPath:coreSimulatorPath]; if (![coreSimulatorBundle load]) return nil; } else { simulatorFrameworkRelativePath = @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/" @"DVTiPhoneSimulatorRemoteClient.framework"; } NSString* simBundlePath = [developerDir stringByAppendingPathComponent:simulatorFrameworkRelativePath]; NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath]; if (![simBundle load]) return nil; return simBundle; } // Converts the given app path to an application spec, which requires an // absolute path. DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) { Class applicationSpecifierClass = FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier"); if (![appPath isAbsolutePath]) { NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath]; appPath = [cwd stringByAppendingPathComponent:appPath]; } appPath = [appPath stringByStandardizingPath]; NSFileManager* fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:appPath]) { LogError(@"File not found: %@", appPath); exit(kExitInvalidArguments); } return [applicationSpecifierClass specifierWithApplicationPath:appPath]; } // Returns the system root for the given SDK version. If sdkVersion is nil, the // default system root is returned. Will return nil if the sdkVersion is not // valid. DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) { Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot"); #if defined(IOSSIM_USE_XCODE_6) Class simRuntimeClass = FindClassByName(@"SimRuntime"); NSArray* sorted = [[simRuntimeClass supportedRuntimes] sortedArrayUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"version" ascending:YES] ]]; NSString* versionString = [[sorted lastObject] versionString]; DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass rootWithSDKVersion:versionString]; #else DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot]; #endif if (sdkVersion) systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion]; return systemRoot; } // Builds a config object for starting the specified app. DTiPhoneSimulatorSessionConfig* BuildSessionConfig( DTiPhoneSimulatorApplicationSpecifier* appSpec, DTiPhoneSimulatorSystemRoot* systemRoot, NSString* stdoutPath, NSString* stderrPath, NSArray* appArgs, NSDictionary* appEnv, NSNumber* deviceFamily, NSString* deviceName) { Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig"); DTiPhoneSimulatorSessionConfig* sessionConfig = [[[sessionConfigClass alloc] init] autorelease]; sessionConfig.applicationToSimulateOnStart = appSpec; sessionConfig.simulatedSystemRoot = systemRoot; sessionConfig.localizedClientName = @"chromium"; sessionConfig.simulatedApplicationStdErrPath = stderrPath; sessionConfig.simulatedApplicationStdOutPath = stdoutPath; sessionConfig.simulatedApplicationLaunchArgs = appArgs; sessionConfig.simulatedApplicationLaunchEnvironment = appEnv; sessionConfig.simulatedDeviceInfoName = deviceName; sessionConfig.simulatedDeviceFamily = deviceFamily; if (IsRunningWithXcode6OrLater()) { #if defined(IOSSIM_USE_XCODE_6) Class simDeviceTypeClass = FindClassByName(@"SimDeviceType"); id simDeviceType = [simDeviceTypeClass supportedDeviceTypesByAlias][deviceName]; Class simRuntimeClass = FindClassByName(@"SimRuntime"); NSString* identifier = systemRoot.runtime.identifier; id simRuntime = [simRuntimeClass supportedRuntimesByIdentifier][identifier]; // Attempt to use an existing device, but create one if a suitable match // can't be found. For example, if the simulator is running with a // non-default home directory (e.g. via iossim's -u command line arg) then // there won't be any devices so one will have to be created. Class simDeviceSetClass = FindClassByName(@"SimDeviceSet"); id deviceSet = [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]]; id simDevice = nil; for (id device in [deviceSet availableDevices]) { if ([device runtime] == simRuntime && [device deviceType] == simDeviceType) { simDevice = device; break; } } if (!simDevice) { NSError* error = nil; // n.b. only the device name is necessary because the iOS Simulator menu // already splits devices by runtime version. NSString* name = [NSString stringWithFormat:@"iossim - %@ ", deviceName]; simDevice = [deviceSet createDeviceWithType:simDeviceType runtime:simRuntime name:name error:&error]; if (error) { LogError(@"Failed to create device: %@", error); exit(kExitInitializationFailure); } } sessionConfig.device = simDevice; #endif // IOSSIM_USE_XCODE_6 } return sessionConfig; } // Builds a simulator session that will use the given delegate. DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) { Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession"); DTiPhoneSimulatorSession* session = [[[sessionClass alloc] init] autorelease]; session.delegate = delegate; return session; } // Creates a temporary directory with a unique name based on the provided // template. The template should not contain any path separators and be suffixed // with X's, which will be substituted with a unique alphanumeric string (see // 'man mkdtemp' for details). The directory will be created as a subdirectory // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX', // this method would return something like '/path/to/tempdir/test-3n2'. // // Returns the absolute path of the newly-created directory, or nill if unable // to create a unique directory. NSString* CreateTempDirectory(NSString* dirNameTemplate) { NSString* fullPathTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate]; char* fullPath = mkdtemp(const_cast([fullPathTemplate UTF8String])); if (fullPath == NULL) return nil; return [NSString stringWithUTF8String:fullPath]; } // Creates the necessary directory structure under the given user home directory // path. // Returns YES if successful, NO if unable to create the directories. BOOL CreateHomeDirSubDirs(NSString* userHomePath) { NSFileManager* fileManager = [NSFileManager defaultManager]; // Create user home and subdirectories. NSArray* subDirsToCreate = [NSArray arrayWithObjects: @"Documents", @"Library/Caches", @"Library/Preferences", nil]; for (NSString* subDir in subDirsToCreate) { NSString* path = [userHomePath stringByAppendingPathComponent:subDir]; NSError* error; if (![fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]) { LogError(@"Unable to create directory: %@. Error: %@", path, [error localizedDescription]); return NO; } } return YES; } // Creates the necessary directory structure under the given user home directory // path, then sets the path in the appropriate environment variable. // Returns YES if successful, NO if unable to create or initialize the given // directory. BOOL InitializeSimulatorUserHome(NSString* userHomePath) { if (!CreateHomeDirSubDirs(userHomePath)) return NO; // Update the environment to use the specified directory as the user home // directory. // Note: the third param of setenv specifies whether or not to overwrite the // variable's value if it has already been set. if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) || (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) { LogError(@"Unable to set environment variables for home directory."); return NO; } return YES; } // Performs a case-insensitive search to see if |stringToSearch| begins with // |prefixToFind|. Returns true if a match is found. BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch, NSString* prefixToFind) { NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch); NSRange range = [stringToSearch rangeOfString:prefixToFind options:options]; return range.location != NSNotFound; } // Prints the usage information to stderr. void PrintUsage() { fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] " "[-e envKey=value]* [-t startupTimeout] []\n" " where is the path to the .app directory and appArgs are any" " arguments to send the simulated app.\n" "\n" "Options:\n" " -d Specifies the device (must be one of the values from the iOS" " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n" " -s Specifies the SDK version to use (e.g '4.3')." " Will use system default if not specified.\n" " -u Specifies a user home directory for the simulator." " Will create a new directory if not specified.\n" " -e Specifies an environment key=value pair that will be" " set in the simulated application's environment.\n" " -t Specifies the session startup timeout (in seconds)." " Defaults to %d.\n" " -l List supported devices and iOS versions.\n", static_cast(kDefaultSessionStartTimeoutSeconds)); } } // namespace void EnsureSupportForCurrentXcodeVersion() { if (IsRunningWithXcode6OrLater()) { #if !IOSSIM_USE_XCODE_6 LogError(@"Running on Xcode 6, but Xcode 6 support was not compiled in."); exit(kExitUnsupportedXcodeVersion); #endif // IOSSIM_USE_XCODE_6 } } int main(int argc, char* const argv[]) { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; EnsureSupportForCurrentXcodeVersion(); // basename() may modify the passed in string and it returns a pointer to an // internal buffer. Give it a copy to modify, and copy what it returns. char* worker = strdup(argv[0]); char* toolName = basename(worker); if (toolName != NULL) { toolName = strdup(toolName); if (toolName != NULL) gToolName = toolName; } if (worker != NULL) free(worker); NSString* appPath = nil; NSString* appName = nil; NSString* sdkVersion = nil; NSString* deviceName = IsRunningWithXcode6OrLater() ? @"iPhone 5s" : @"iPhone"; NSString* simHomePath = nil; NSMutableArray* appArgs = [NSMutableArray array]; NSMutableDictionary* appEnv = [NSMutableDictionary dictionary]; NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds; NSString* developerDir = FindDeveloperDir(); if (!developerDir) { LogError(@"Unable to find developer directory."); exit(kExitInitializationFailure); } NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir); if (!simulatorFramework) { LogError(@"Failed to load the Simulator Framework."); exit(kExitInitializationFailure); } // Parse the optional arguments int c; while ((c = getopt(argc, argv, "hs:d:u:e:t:l")) != -1) { switch (c) { case 's': sdkVersion = [NSString stringWithUTF8String:optarg]; break; case 'd': deviceName = [NSString stringWithUTF8String:optarg]; break; case 'u': simHomePath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; break; case 'e': { NSString* envLine = [NSString stringWithUTF8String:optarg]; NSRange range = [envLine rangeOfString:@"="]; if (range.location == NSNotFound) { LogError(@"Invalid key=value argument for -e."); PrintUsage(); exit(kExitInvalidArguments); } NSString* key = [envLine substringToIndex:range.location]; NSString* value = [envLine substringFromIndex:(range.location + 1)]; [appEnv setObject:value forKey:key]; } break; case 't': { int timeout = atoi(optarg); if (timeout > 0) { sessionStartTimeout = static_cast(timeout); } else { LogError(@"Invalid startup timeout (%s).", optarg); PrintUsage(); exit(kExitInvalidArguments); } } break; case 'l': PrintSupportedDevices(); exit(kExitSuccess); break; case 'h': PrintUsage(); exit(kExitSuccess); default: PrintUsage(); exit(kExitInvalidArguments); } } // There should be at least one arg left, specifying the app path. Any // additional args are passed as arguments to the app. if (optind < argc) { appPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[optind] length:strlen(argv[optind])]; appName = [appPath lastPathComponent]; while (++optind < argc) { [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]]; } } else { LogError(@"Unable to parse command line arguments."); PrintUsage(); exit(kExitInvalidArguments); } // Make sure the app path provided is legit. DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath); if (!appSpec) { LogError(@"Invalid app path: %@", appPath); exit(kExitInitializationFailure); } // Make sure the SDK path provided is legit (or nil). DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion); if (!systemRoot) { LogError(@"Invalid SDK version: %@", sdkVersion); PrintSupportedDevices(); exit(kExitInitializationFailure); } // Get the paths for stdout and stderr so the simulated app's output will show // up in the caller's stdout/stderr. NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX"); NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"]; // Determine the deviceFamily based on the deviceName NSNumber* deviceFamily = nil; if (IsRunningWithXcode6OrLater()) { #if defined(IOSSIM_USE_XCODE_6) Class simDeviceTypeClass = FindClassByName(@"SimDeviceType"); if ([simDeviceTypeClass supportedDeviceTypesByAlias][deviceName] == nil) { LogError(@"Invalid device name: %@.", deviceName); PrintSupportedDevices(); exit(kExitInvalidArguments); } #endif // IOSSIM_USE_XCODE_6 } else { if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) { deviceFamily = [NSNumber numberWithInt:kIPhoneFamily]; } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) { deviceFamily = [NSNumber numberWithInt:kIPadFamily]; } else { LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'", deviceName); exit(kExitInvalidArguments); } } // Set up the user home directory for the simulator only if a non-default // value was specified. if (simHomePath) { if (!InitializeSimulatorUserHome(simHomePath)) { LogError(@"Unable to initialize home directory for simulator: %@", simHomePath); exit(kExitInitializationFailure); } } else { simHomePath = NSHomeDirectory(); } // Create the config and simulator session. DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec, systemRoot, stdioPath, stdioPath, appArgs, appEnv, deviceFamily, deviceName); SimulatorDelegate* delegate = [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath developerDir:developerDir simulatorHome:simHomePath] autorelease]; DTiPhoneSimulatorSession* session = BuildSession(delegate); // Start the simulator session. NSError* error; BOOL started = [session requestStartWithConfig:config timeout:sessionStartTimeout error:&error]; // Spin the runtime indefinitely. When the delegate gets the message that the // app has quit it will exit this program. if (started) { [[NSRunLoop mainRunLoop] run]; } else { LogError(@"Simulator failed request to start: \"%@\" (%@:%ld)", [error localizedDescription], [error domain], static_cast([error code])); } // Note that this code is only executed if the simulator fails to start // because once the main run loop is started, only the delegate calling // exit() will end the program. [pool drain]; return kExitFailure; }