728 lines
28 KiB
Plaintext
728 lines
28 KiB
Plaintext
|
// 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.
|
||
|
|
||
|
#import <Foundation/Foundation.h>
|
||
|
#include <asl.h>
|
||
|
#include <libgen.h>
|
||
|
#include <stdarg.h>
|
||
|
#include <stdio.h>
|
||
|
|
||
|
// 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 the iPhoneSimulatorRemoteClient framework 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. Also, 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.
|
||
|
|
||
|
@class DTiPhoneSimulatorApplicationSpecifier;
|
||
|
@class DTiPhoneSimulatorSession;
|
||
|
@class DTiPhoneSimulatorSessionConfig;
|
||
|
@class DTiPhoneSimulatorSystemRoot;
|
||
|
|
||
|
@protocol DTiPhoneSimulatorSessionDelegate
|
||
|
- (void)session:(DTiPhoneSimulatorSession*)session
|
||
|
didEndWithError:(NSError*)error;
|
||
|
- (void)session:(DTiPhoneSimulatorSession*)session
|
||
|
didStart:(BOOL)started
|
||
|
withError:(NSError*)error;
|
||
|
@end
|
||
|
|
||
|
#import "iPhoneSimulatorRemoteClient.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;
|
||
|
|
||
|
// The path within the developer dir of the private Simulator frameworks.
|
||
|
NSString* const kSimulatorFrameworkRelativePath =
|
||
|
@"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/"
|
||
|
@"iPhoneSimulatorRemoteClient.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;
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
} // namespace
|
||
|
|
||
|
// A delegate that is called when the simulated app is started or ended in the
|
||
|
// simulator.
|
||
|
@interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
|
||
|
@private
|
||
|
NSString* stdioPath_;
|
||
|
NSString* developerDir_;
|
||
|
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 {
|
||
|
self = [super init];
|
||
|
if (self) {
|
||
|
stdioPath_ = [stdioPath copy];
|
||
|
developerDir_ = [developerDir 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)tailOutput {
|
||
|
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
|
||
|
|
||
|
// Copy data to stdout/stderr while the app is running.
|
||
|
NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_];
|
||
|
NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput];
|
||
|
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 tailOutput];
|
||
|
// 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<long int>([error code]));
|
||
|
exit(kExitAppFailedToStart);
|
||
|
}
|
||
|
|
||
|
// Start a thread to write contents of outputPath to stdout.
|
||
|
appRunning_ = YES;
|
||
|
outputThread_ = [[NSThread alloc] initWithTarget:self
|
||
|
selector:@selector(tailOutput)
|
||
|
object:nil];
|
||
|
[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<long int>([error code]));
|
||
|
} else {
|
||
|
LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
|
||
|
localizedDescription, [error domain],
|
||
|
static_cast<long int>([error code]));
|
||
|
exit(kExitFailure);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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);
|
||
|
asl_set_query(query, ASL_KEY_REF_PID,
|
||
|
[[[session simulatedApplicationPID] stringValue] UTF8String],
|
||
|
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);
|
||
|
BOOL badEntryFound = NO;
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// 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.
|
||
|
NSTask* xcodeSelectTask = [[[NSTask alloc] init] autorelease];
|
||
|
[xcodeSelectTask setLaunchPath:@"/usr/bin/xcode-select"];
|
||
|
[xcodeSelectTask setArguments:[NSArray arrayWithObject:@"-print-path"]];
|
||
|
|
||
|
NSPipe* outputPipe = [NSPipe pipe];
|
||
|
[xcodeSelectTask setStandardOutput:outputPipe];
|
||
|
NSFileHandle* outputFile = [outputPipe fileHandleForReading];
|
||
|
|
||
|
[xcodeSelectTask launch];
|
||
|
NSData* outputData = [outputFile readDataToEndOfFile];
|
||
|
[xcodeSelectTask terminate];
|
||
|
|
||
|
NSString* output =
|
||
|
[[[NSString alloc] initWithData:outputData
|
||
|
encoding:NSUTF8StringEncoding] autorelease];
|
||
|
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* devToolsFoundationPath = [developerDir
|
||
|
stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
|
||
|
NSBundle* devToolsFoundationBundle =
|
||
|
[NSBundle bundleWithPath:devToolsFoundationPath];
|
||
|
if (![devToolsFoundationBundle load])
|
||
|
return nil;
|
||
|
NSString* simBundlePath = [developerDir
|
||
|
stringByAppendingPathComponent:kSimulatorFrameworkRelativePath];
|
||
|
NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
|
||
|
if (![simBundle load])
|
||
|
return nil;
|
||
|
return simBundle;
|
||
|
}
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
// 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];
|
||
|
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");
|
||
|
DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
|
||
|
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) {
|
||
|
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.simulatedDeviceFamily = deviceFamily;
|
||
|
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<char*>([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, NSString* deviceName) {
|
||
|
if (!CreateHomeDirSubDirs(userHomePath))
|
||
|
return NO;
|
||
|
|
||
|
// Set the device to simulate. Note that the iOS Simulator must not be running
|
||
|
// for this setting to take effect.
|
||
|
CFStringRef iPhoneSimulatorAppID = CFSTR("com.apple.iphonesimulator");
|
||
|
CFPreferencesSetAppValue(CFSTR("SimulateDevice"),
|
||
|
deviceName,
|
||
|
iPhoneSimulatorAppID);
|
||
|
CFPreferencesAppSynchronize(iPhoneSimulatorAppID);
|
||
|
|
||
|
// 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] <appPath> [<appArgs>]\n"
|
||
|
" where <appPath> 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",
|
||
|
static_cast<int>(kDefaultSessionStartTimeoutSeconds));
|
||
|
}
|
||
|
|
||
|
} // namespace
|
||
|
|
||
|
int main(int argc, char* const argv[]) {
|
||
|
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
|
||
|
|
||
|
// 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 = @"iPhone";
|
||
|
NSString* simHomePath = nil;
|
||
|
NSMutableArray* appArgs = [NSMutableArray array];
|
||
|
NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
|
||
|
NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
|
||
|
|
||
|
// Parse the optional arguments
|
||
|
int c;
|
||
|
while ((c = getopt(argc, argv, "hs:d:u:e:t:")) != -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<NSTimeInterval>(timeout);
|
||
|
} else {
|
||
|
LogError(@"Invalid startup timeout (%s).", optarg);
|
||
|
PrintUsage();
|
||
|
exit(kExitInvalidArguments);
|
||
|
}
|
||
|
}
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
// 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);
|
||
|
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 (!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
|
||
|
if (!simHomePath) {
|
||
|
NSString* dirNameTemplate =
|
||
|
[NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName];
|
||
|
simHomePath = CreateTempDirectory(dirNameTemplate);
|
||
|
if (!simHomePath) {
|
||
|
LogError(@"Unable to create unique directory for template %@",
|
||
|
dirNameTemplate);
|
||
|
exit(kExitInitializationFailure);
|
||
|
}
|
||
|
}
|
||
|
if (!InitializeSimulatorUserHome(simHomePath, deviceName)) {
|
||
|
LogError(@"Unable to initialize home directory for simulator: %@",
|
||
|
simHomePath);
|
||
|
exit(kExitInitializationFailure);
|
||
|
}
|
||
|
|
||
|
// Create the config and simulator session.
|
||
|
DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
|
||
|
systemRoot,
|
||
|
stdioPath,
|
||
|
stdioPath,
|
||
|
appArgs,
|
||
|
appEnv,
|
||
|
deviceFamily);
|
||
|
SimulatorDelegate* delegate =
|
||
|
[[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
|
||
|
developerDir:developerDir] 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<long int>([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;
|
||
|
}
|