• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5#import <Foundation/Foundation.h>
6#include <asl.h>
7#include <libgen.h>
8#include <stdarg.h>
9#include <stdio.h>
10
11// An executable (iossim) that runs an app in the iOS Simulator.
12// Run 'iossim -h' for usage information.
13//
14// For best results, the iOS Simulator application should not be running when
15// iossim is invoked.
16//
17// Headers for the iPhoneSimulatorRemoteClient framework used in this tool are
18// generated by class-dump, via GYP.
19// (class-dump is available at http://www.codethecode.com/projects/class-dump/)
20//
21// However, there are some forward declarations required to get things to
22// compile. Also, the DTiPhoneSimulatorSessionDelegate protocol is referenced
23// by the iPhoneSimulatorRemoteClient framework, but not defined in the object
24// file, so it must be defined here before importing the generated
25// iPhoneSimulatorRemoteClient.h file.
26
27@class DTiPhoneSimulatorApplicationSpecifier;
28@class DTiPhoneSimulatorSession;
29@class DTiPhoneSimulatorSessionConfig;
30@class DTiPhoneSimulatorSystemRoot;
31
32@protocol DTiPhoneSimulatorSessionDelegate
33- (void)session:(DTiPhoneSimulatorSession*)session
34    didEndWithError:(NSError*)error;
35- (void)session:(DTiPhoneSimulatorSession*)session
36       didStart:(BOOL)started
37      withError:(NSError*)error;
38@end
39
40#import "iPhoneSimulatorRemoteClient.h"
41
42// An undocumented system log key included in messages from launchd. The value
43// is the PID of the process the message is about (as opposed to launchd's PID).
44#define ASL_KEY_REF_PID "RefPID"
45
46namespace {
47
48// Name of environment variables that control the user's home directory in the
49// simulator.
50const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME";
51const char* const kHomeEnvVariable = "HOME";
52
53// Device family codes for iPhone and iPad.
54const int kIPhoneFamily = 1;
55const int kIPadFamily = 2;
56
57// Max number of seconds to wait for the simulator session to start.
58// This timeout must allow time to start up iOS Simulator, install the app
59// and perform any other black magic that is encoded in the
60// iPhoneSimulatorRemoteClient framework to kick things off. Normal start up
61// time is only a couple seconds but machine load, disk caches, etc., can all
62// affect startup time in the wild so the timeout needs to be fairly generous.
63// If this timeout occurs iossim will likely exit with non-zero status; the
64// exception being if the app is invoked and completes execution before the
65// session is started (this case is handled in session:didStart:withError).
66const NSTimeInterval kDefaultSessionStartTimeoutSeconds = 30;
67
68// While the simulated app is running, its stdout is redirected to a file which
69// is polled by iossim and written to iossim's stdout using the following
70// polling interval.
71const NSTimeInterval kOutputPollIntervalSeconds = 0.1;
72
73// The path within the developer dir of the private Simulator frameworks.
74NSString* const kSimulatorFrameworkRelativePath =
75    @"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/"
76    @"iPhoneSimulatorRemoteClient.framework";
77NSString* const kDevToolsFoundationRelativePath =
78    @"../OtherFrameworks/DevToolsFoundation.framework";
79NSString* const kSimulatorRelativePath =
80    @"Platforms/iPhoneSimulator.platform/Developer/Applications/"
81    @"iPhone Simulator.app";
82
83// Simulator Error String Key. This can be found by looking in the Simulator's
84// Localizable.strings files.
85NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit.";
86
87const char* gToolName = "iossim";
88
89// Exit status codes.
90const int kExitSuccess = EXIT_SUCCESS;
91const int kExitFailure = EXIT_FAILURE;
92const int kExitInvalidArguments = 2;
93const int kExitInitializationFailure = 3;
94const int kExitAppFailedToStart = 4;
95const int kExitAppCrashed = 5;
96
97void LogError(NSString* format, ...) {
98  va_list list;
99  va_start(list, format);
100
101  NSString* message =
102      [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
103
104  fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
105  fflush(stderr);
106
107  va_end(list);
108}
109
110void LogWarning(NSString* format, ...) {
111  va_list list;
112  va_start(list, format);
113
114  NSString* message =
115      [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
116
117  fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
118  fflush(stderr);
119
120  va_end(list);
121}
122
123}  // namespace
124
125// A delegate that is called when the simulated app is started or ended in the
126// simulator.
127@interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
128 @private
129  NSString* stdioPath_;
130  NSString* developerDir_;
131  NSString* simulatorHome_;
132  NSThread* outputThread_;
133  NSBundle* simulatorBundle_;
134  BOOL appRunning_;
135}
136@end
137
138// An implementation that copies the simulated app's stdio to stdout of this
139// executable. While it would be nice to get stdout and stderr independently
140// from iOS Simulator, issues like I/O buffering and interleaved output
141// between iOS Simulator and the app would cause iossim to display things out
142// of order here. Printing all output to a single file keeps the order correct.
143// Instances of this classe should be initialized with the location of the
144// simulated app's output file. When the simulated app starts, a thread is
145// started which handles copying data from the simulated app's output file to
146// the stdout of this executable.
147@implementation SimulatorDelegate
148
149// Specifies the file locations of the simulated app's stdout and stderr.
150- (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath
151                           developerDir:(NSString*)developerDir
152                          simulatorHome:(NSString*)simulatorHome {
153  self = [super init];
154  if (self) {
155    stdioPath_ = [stdioPath copy];
156    developerDir_ = [developerDir copy];
157    simulatorHome_ = [simulatorHome copy];
158  }
159
160  return self;
161}
162
163- (void)dealloc {
164  [stdioPath_ release];
165  [developerDir_ release];
166  [simulatorBundle_ release];
167  [super dealloc];
168}
169
170// Reads data from the simulated app's output and writes it to stdout. This
171// method blocks, so it should be called in a separate thread. The iOS
172// Simulator takes a file path for the simulated app's stdout and stderr, but
173// this path isn't always available (e.g. when the stdout is Xcode's build
174// window). As a workaround, iossim creates a temp file to hold output, which
175// this method reads and copies to stdout.
176- (void)tailOutput {
177  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
178
179  // Copy data to stdout/stderr while the app is running.
180  NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_];
181  NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput];
182  while (appRunning_) {
183    NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init];
184    [standardOutput writeData:[simio readDataToEndOfFile]];
185    [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
186    [innerPool drain];
187  }
188
189  // Once the app is no longer running, copy any data that was written during
190  // the last sleep cycle.
191  [standardOutput writeData:[simio readDataToEndOfFile]];
192
193  [pool drain];
194}
195
196// Fetches a localized error string from the Simulator.
197- (NSString *)localizedSimulatorErrorString:(NSString*)stringKey {
198  // Lazy load of the simulator bundle.
199  if (simulatorBundle_ == nil) {
200    NSString* simulatorPath = [developerDir_
201        stringByAppendingPathComponent:kSimulatorRelativePath];
202    simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath];
203  }
204  NSString *localizedStr =
205      [simulatorBundle_ localizedStringForKey:stringKey
206                                        value:nil
207                                        table:nil];
208  if ([localizedStr length])
209    return localizedStr;
210  // Failed to get a value, follow Cocoa conventions and use the key as the
211  // string.
212  return stringKey;
213}
214
215- (void)session:(DTiPhoneSimulatorSession*)session
216       didStart:(BOOL)started
217      withError:(NSError*)error {
218  if (!started) {
219    // If the test executes very quickly (<30ms), the SimulatorDelegate may not
220    // get the initial session:started:withError: message indicating successful
221    // startup of the simulated app. Instead the delegate will get a
222    // session:started:withError: message after the timeout has elapsed. To
223    // account for this case, check if the simulated app's stdio file was
224    // ever created and if it exists dump it to stdout and return success.
225    NSFileManager* fileManager = [NSFileManager defaultManager];
226    if ([fileManager fileExistsAtPath:stdioPath_]) {
227      appRunning_ = NO;
228      [self tailOutput];
229      // Note that exiting in this state leaves a process running
230      // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will
231      // prevent future simulator sessions from being started for 30 seconds
232      // unless the iOS Simulator application is killed altogether.
233      [self session:session didEndWithError:nil];
234
235      // session:didEndWithError should not return (because it exits) so
236      // the execution path should never get here.
237      exit(kExitFailure);
238    }
239
240    LogError(@"Simulator failed to start: \"%@\" (%@:%ld)",
241             [error localizedDescription],
242             [error domain], static_cast<long int>([error code]));
243    exit(kExitAppFailedToStart);
244  }
245
246  // Start a thread to write contents of outputPath to stdout.
247  appRunning_ = YES;
248  outputThread_ = [[NSThread alloc] initWithTarget:self
249                                          selector:@selector(tailOutput)
250                                            object:nil];
251  [outputThread_ start];
252}
253
254- (void)session:(DTiPhoneSimulatorSession*)session
255    didEndWithError:(NSError*)error {
256  appRunning_ = NO;
257  // Wait for the output thread to finish copying data to stdout.
258  if (outputThread_) {
259    while (![outputThread_ isFinished]) {
260      [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds];
261    }
262    [outputThread_ release];
263    outputThread_ = nil;
264  }
265
266  if (error) {
267    // There appears to be a race condition where sometimes the simulator
268    // framework will end with an error, but the error is that the simulated
269    // app cleanly shut down; try to trap this error and don't fail the
270    // simulator run.
271    NSString* localizedDescription = [error localizedDescription];
272    NSString* ignorableErrorStr =
273        [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey];
274    if ([ignorableErrorStr isEqual:localizedDescription]) {
275      LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)",
276                 localizedDescription, [error domain],
277                 static_cast<long int>([error code]));
278    } else {
279      LogError(@"Simulator ended with error: \"%@\" (%@:%ld)",
280               localizedDescription, [error domain],
281               static_cast<long int>([error code]));
282      exit(kExitFailure);
283    }
284  }
285
286  // Try to determine if the simulated app crashed or quit with a non-zero
287  // status code. iOS Simluator handles things a bit differently depending on
288  // the version, so first determine the iOS version being used.
289  BOOL badEntryFound = NO;
290  NSString* versionString =
291      [[[session sessionConfig] simulatedSystemRoot] sdkVersion];
292  NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."]
293      objectAtIndex:0] intValue];
294  if (majorVersion <= 6) {
295    // In iOS 6 and before, logging from the simulated apps went to the main
296    // system logs, so use ASL to check if the simulated app exited abnormally
297    // by looking for system log messages from launchd that refer to the
298    // simulated app's PID. Limit query to messages in the last minute since
299    // PIDs are cyclical.
300    aslmsg query = asl_new(ASL_TYPE_QUERY);
301    asl_set_query(query, ASL_KEY_SENDER, "launchd",
302                  ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING);
303    asl_set_query(query, ASL_KEY_REF_PID,
304                  [[[session simulatedApplicationPID] stringValue] UTF8String],
305                  ASL_QUERY_OP_EQUAL);
306    asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL);
307
308    // Log any messages found, and take note of any messages that may indicate
309    // the app crashed or did not exit cleanly.
310    aslresponse response = asl_search(NULL, query);
311    aslmsg entry;
312    while ((entry = aslresponse_next(response)) != NULL) {
313      const char* message = asl_get(entry, ASL_KEY_MSG);
314      LogWarning(@"Console message: %s", message);
315      // Some messages are harmless, so don't trigger a failure for them.
316      if (strstr(message, "The following job tried to hijack the service"))
317        continue;
318      badEntryFound = YES;
319    }
320  } else {
321    // Otherwise, the iOS Simulator's system logging is sandboxed, so parse the
322    // sandboxed system.log file for known errors.
323    NSString* relativePathToSystemLog =
324        [NSString stringWithFormat:
325            @"Library/Logs/iOS Simulator/%@/system.log", versionString];
326    NSString* path =
327        [simulatorHome_ stringByAppendingPathComponent:relativePathToSystemLog];
328    NSFileManager* fileManager = [NSFileManager defaultManager];
329    if ([fileManager fileExistsAtPath:path]) {
330      NSString* content =
331          [NSString stringWithContentsOfFile:path
332                                    encoding:NSUTF8StringEncoding
333                                       error:NULL];
334      NSArray* lines = [content componentsSeparatedByCharactersInSet:
335          [NSCharacterSet newlineCharacterSet]];
336      for (NSString* line in lines) {
337        NSString* const kErrorString = @"Service exited with abnormal code:";
338        if ([line rangeOfString:kErrorString].location != NSNotFound) {
339          LogWarning(@"Console message: %@", line);
340          badEntryFound = YES;
341          break;
342        }
343      }
344      // Remove the log file so subsequent invocations of iossim won't be
345      // looking at stale logs.
346      remove([path fileSystemRepresentation]);
347    } else {
348        LogWarning(@"Unable to find sandboxed system log.");
349    }
350  }
351
352  // If the query returned any nasty-looking results, iossim should exit with
353  // non-zero status.
354  if (badEntryFound) {
355    LogError(@"Simulated app crashed or exited with non-zero status");
356    exit(kExitAppCrashed);
357  }
358  exit(kExitSuccess);
359}
360@end
361
362namespace {
363
364// Finds the developer dir via xcode-select or the DEVELOPER_DIR environment
365// variable.
366NSString* FindDeveloperDir() {
367  // Check the env first.
368  NSDictionary* env = [[NSProcessInfo processInfo] environment];
369  NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"];
370  if ([developerDir length] > 0)
371    return developerDir;
372
373  // Go look for it via xcode-select.
374  NSTask* xcodeSelectTask = [[[NSTask alloc] init] autorelease];
375  [xcodeSelectTask setLaunchPath:@"/usr/bin/xcode-select"];
376  [xcodeSelectTask setArguments:[NSArray arrayWithObject:@"-print-path"]];
377
378  NSPipe* outputPipe = [NSPipe pipe];
379  [xcodeSelectTask setStandardOutput:outputPipe];
380  NSFileHandle* outputFile = [outputPipe fileHandleForReading];
381
382  [xcodeSelectTask launch];
383  NSData* outputData = [outputFile readDataToEndOfFile];
384  [xcodeSelectTask terminate];
385
386  NSString* output =
387      [[[NSString alloc] initWithData:outputData
388                             encoding:NSUTF8StringEncoding] autorelease];
389  output = [output stringByTrimmingCharactersInSet:
390      [NSCharacterSet whitespaceAndNewlineCharacterSet]];
391  if ([output length] == 0)
392    output = nil;
393  return output;
394}
395
396// Loads the Simulator framework from the given developer dir.
397NSBundle* LoadSimulatorFramework(NSString* developerDir) {
398  // The Simulator framework depends on some of the other Xcode private
399  // frameworks; manually load them first so everything can be linked up.
400  NSString* devToolsFoundationPath = [developerDir
401      stringByAppendingPathComponent:kDevToolsFoundationRelativePath];
402  NSBundle* devToolsFoundationBundle =
403      [NSBundle bundleWithPath:devToolsFoundationPath];
404  if (![devToolsFoundationBundle load])
405    return nil;
406  NSString* simBundlePath = [developerDir
407      stringByAppendingPathComponent:kSimulatorFrameworkRelativePath];
408  NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath];
409  if (![simBundle load])
410    return nil;
411  return simBundle;
412}
413
414// Helper to find a class by name and die if it isn't found.
415Class FindClassByName(NSString* nameOfClass) {
416  Class theClass = NSClassFromString(nameOfClass);
417  if (!theClass) {
418    LogError(@"Failed to find class %@ at runtime.", nameOfClass);
419    exit(kExitInitializationFailure);
420  }
421  return theClass;
422}
423
424// Converts the given app path to an application spec, which requires an
425// absolute path.
426DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) {
427  Class applicationSpecifierClass =
428      FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier");
429  if (![appPath isAbsolutePath]) {
430    NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath];
431    appPath = [cwd stringByAppendingPathComponent:appPath];
432  }
433  appPath = [appPath stringByStandardizingPath];
434  return [applicationSpecifierClass specifierWithApplicationPath:appPath];
435}
436
437// Returns the system root for the given SDK version. If sdkVersion is nil, the
438// default system root is returned.  Will return nil if the sdkVersion is not
439// valid.
440DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
441  Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot");
442  DTiPhoneSimulatorSystemRoot* systemRoot = [systemRootClass defaultRoot];
443  if (sdkVersion)
444    systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion];
445
446  return systemRoot;
447}
448
449// Builds a config object for starting the specified app.
450DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
451    DTiPhoneSimulatorApplicationSpecifier* appSpec,
452    DTiPhoneSimulatorSystemRoot* systemRoot,
453    NSString* stdoutPath,
454    NSString* stderrPath,
455    NSArray* appArgs,
456    NSDictionary* appEnv,
457    NSNumber* deviceFamily) {
458  Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig");
459  DTiPhoneSimulatorSessionConfig* sessionConfig =
460      [[[sessionConfigClass alloc] init] autorelease];
461  sessionConfig.applicationToSimulateOnStart = appSpec;
462  sessionConfig.simulatedSystemRoot = systemRoot;
463  sessionConfig.localizedClientName = @"chromium";
464  sessionConfig.simulatedApplicationStdErrPath = stderrPath;
465  sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
466  sessionConfig.simulatedApplicationLaunchArgs = appArgs;
467  sessionConfig.simulatedApplicationLaunchEnvironment = appEnv;
468  sessionConfig.simulatedDeviceFamily = deviceFamily;
469  return sessionConfig;
470}
471
472// Builds a simulator session that will use the given delegate.
473DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
474  Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession");
475  DTiPhoneSimulatorSession* session =
476      [[[sessionClass alloc] init] autorelease];
477  session.delegate = delegate;
478  return session;
479}
480
481// Creates a temporary directory with a unique name based on the provided
482// template. The template should not contain any path separators and be suffixed
483// with X's, which will be substituted with a unique alphanumeric string (see
484// 'man mkdtemp' for details). The directory will be created as a subdirectory
485// of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
486// this method would return something like '/path/to/tempdir/test-3n2'.
487//
488// Returns the absolute path of the newly-created directory, or nill if unable
489// to create a unique directory.
490NSString* CreateTempDirectory(NSString* dirNameTemplate) {
491  NSString* fullPathTemplate =
492      [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
493  char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
494  if (fullPath == NULL)
495    return nil;
496
497  return [NSString stringWithUTF8String:fullPath];
498}
499
500// Creates the necessary directory structure under the given user home directory
501// path.
502// Returns YES if successful, NO if unable to create the directories.
503BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
504  NSFileManager* fileManager = [NSFileManager defaultManager];
505
506  // Create user home and subdirectories.
507  NSArray* subDirsToCreate = [NSArray arrayWithObjects:
508                              @"Documents",
509                              @"Library/Caches",
510                              @"Library/Preferences",
511                              nil];
512  for (NSString* subDir in subDirsToCreate) {
513    NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
514    NSError* error;
515    if (![fileManager createDirectoryAtPath:path
516                withIntermediateDirectories:YES
517                                 attributes:nil
518                                      error:&error]) {
519      LogError(@"Unable to create directory: %@. Error: %@",
520               path, [error localizedDescription]);
521      return NO;
522    }
523  }
524
525  return YES;
526}
527
528// Creates the necessary directory structure under the given user home directory
529// path, then sets the path in the appropriate environment variable.
530// Returns YES if successful, NO if unable to create or initialize the given
531// directory.
532BOOL InitializeSimulatorUserHome(NSString* userHomePath, NSString* deviceName) {
533  if (!CreateHomeDirSubDirs(userHomePath))
534    return NO;
535
536  // Set the device to simulate. Note that the iOS Simulator must not be running
537  // for this setting to take effect.
538  CFStringRef iPhoneSimulatorAppID = CFSTR("com.apple.iphonesimulator");
539  CFPreferencesSetAppValue(CFSTR("SimulateDevice"),
540                           deviceName,
541                           iPhoneSimulatorAppID);
542  CFPreferencesAppSynchronize(iPhoneSimulatorAppID);
543
544  // Update the environment to use the specified directory as the user home
545  // directory.
546  // Note: the third param of setenv specifies whether or not to overwrite the
547  // variable's value if it has already been set.
548  if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
549      (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
550    LogError(@"Unable to set environment variables for home directory.");
551    return NO;
552  }
553
554  return YES;
555}
556
557// Performs a case-insensitive search to see if |stringToSearch| begins with
558// |prefixToFind|. Returns true if a match is found.
559BOOL CaseInsensitivePrefixSearch(NSString* stringToSearch,
560                                 NSString* prefixToFind) {
561  NSStringCompareOptions options = (NSAnchoredSearch | NSCaseInsensitiveSearch);
562  NSRange range = [stringToSearch rangeOfString:prefixToFind
563                                        options:options];
564  return range.location != NSNotFound;
565}
566
567// Prints the usage information to stderr.
568void PrintUsage() {
569  fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
570      "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n"
571      "  where <appPath> is the path to the .app directory and appArgs are any"
572      " arguments to send the simulated app.\n"
573      "\n"
574      "Options:\n"
575      "  -d  Specifies the device (must be one of the values from the iOS"
576      " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n"
577      "  -s  Specifies the SDK version to use (e.g '4.3')."
578      " Will use system default if not specified.\n"
579      "  -u  Specifies a user home directory for the simulator."
580      " Will create a new directory if not specified.\n"
581      "  -e  Specifies an environment key=value pair that will be"
582      " set in the simulated application's environment.\n"
583      "  -t  Specifies the session startup timeout (in seconds)."
584      " Defaults to %d.\n",
585      static_cast<int>(kDefaultSessionStartTimeoutSeconds));
586}
587
588}  // namespace
589
590int main(int argc, char* const argv[]) {
591  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
592
593  // basename() may modify the passed in string and it returns a pointer to an
594  // internal buffer. Give it a copy to modify, and copy what it returns.
595  char* worker = strdup(argv[0]);
596  char* toolName = basename(worker);
597  if (toolName != NULL) {
598    toolName = strdup(toolName);
599    if (toolName != NULL)
600      gToolName = toolName;
601  }
602  if (worker != NULL)
603    free(worker);
604
605  NSString* appPath = nil;
606  NSString* appName = nil;
607  NSString* sdkVersion = nil;
608  NSString* deviceName = @"iPhone";
609  NSString* simHomePath = nil;
610  NSMutableArray* appArgs = [NSMutableArray array];
611  NSMutableDictionary* appEnv = [NSMutableDictionary dictionary];
612  NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds;
613
614  // Parse the optional arguments
615  int c;
616  while ((c = getopt(argc, argv, "hs:d:u:e:t:")) != -1) {
617    switch (c) {
618      case 's':
619        sdkVersion = [NSString stringWithUTF8String:optarg];
620        break;
621      case 'd':
622        deviceName = [NSString stringWithUTF8String:optarg];
623        break;
624      case 'u':
625        simHomePath = [[NSFileManager defaultManager]
626            stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
627        break;
628      case 'e': {
629        NSString* envLine = [NSString stringWithUTF8String:optarg];
630        NSRange range = [envLine rangeOfString:@"="];
631        if (range.location == NSNotFound) {
632          LogError(@"Invalid key=value argument for -e.");
633          PrintUsage();
634          exit(kExitInvalidArguments);
635        }
636        NSString* key = [envLine substringToIndex:range.location];
637        NSString* value = [envLine substringFromIndex:(range.location + 1)];
638        [appEnv setObject:value forKey:key];
639      }
640        break;
641      case 't': {
642        int timeout = atoi(optarg);
643        if (timeout > 0) {
644          sessionStartTimeout = static_cast<NSTimeInterval>(timeout);
645        } else {
646          LogError(@"Invalid startup timeout (%s).", optarg);
647          PrintUsage();
648          exit(kExitInvalidArguments);
649        }
650      }
651        break;
652      case 'h':
653        PrintUsage();
654        exit(kExitSuccess);
655      default:
656        PrintUsage();
657        exit(kExitInvalidArguments);
658    }
659  }
660
661  // There should be at least one arg left, specifying the app path. Any
662  // additional args are passed as arguments to the app.
663  if (optind < argc) {
664    appPath = [[NSFileManager defaultManager]
665        stringWithFileSystemRepresentation:argv[optind]
666                                    length:strlen(argv[optind])];
667    appName = [appPath lastPathComponent];
668    while (++optind < argc) {
669      [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
670    }
671  } else {
672    LogError(@"Unable to parse command line arguments.");
673    PrintUsage();
674    exit(kExitInvalidArguments);
675  }
676
677  NSString* developerDir = FindDeveloperDir();
678  if (!developerDir) {
679    LogError(@"Unable to find developer directory.");
680    exit(kExitInitializationFailure);
681  }
682
683  NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir);
684  if (!simulatorFramework) {
685    LogError(@"Failed to load the Simulator Framework.");
686    exit(kExitInitializationFailure);
687  }
688
689  // Make sure the app path provided is legit.
690  DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
691  if (!appSpec) {
692    LogError(@"Invalid app path: %@", appPath);
693    exit(kExitInitializationFailure);
694  }
695
696  // Make sure the SDK path provided is legit (or nil).
697  DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
698  if (!systemRoot) {
699    LogError(@"Invalid SDK version: %@", sdkVersion);
700    exit(kExitInitializationFailure);
701  }
702
703  // Get the paths for stdout and stderr so the simulated app's output will show
704  // up in the caller's stdout/stderr.
705  NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
706  NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
707
708  // Determine the deviceFamily based on the deviceName
709  NSNumber* deviceFamily = nil;
710  if (!deviceName || CaseInsensitivePrefixSearch(deviceName, @"iPhone")) {
711    deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
712  } else if (CaseInsensitivePrefixSearch(deviceName, @"iPad")) {
713    deviceFamily = [NSNumber numberWithInt:kIPadFamily];
714  } else {
715    LogError(@"Invalid device name: %@. Must begin with 'iPhone' or 'iPad'",
716             deviceName);
717    exit(kExitInvalidArguments);
718  }
719
720  // Set up the user home directory for the simulator
721  if (!simHomePath) {
722    NSString* dirNameTemplate =
723        [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName];
724    simHomePath = CreateTempDirectory(dirNameTemplate);
725    if (!simHomePath) {
726      LogError(@"Unable to create unique directory for template %@",
727               dirNameTemplate);
728      exit(kExitInitializationFailure);
729    }
730  }
731  if (!InitializeSimulatorUserHome(simHomePath, deviceName)) {
732    LogError(@"Unable to initialize home directory for simulator: %@",
733             simHomePath);
734    exit(kExitInitializationFailure);
735  }
736
737  // Create the config and simulator session.
738  DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
739                                                              systemRoot,
740                                                              stdioPath,
741                                                              stdioPath,
742                                                              appArgs,
743                                                              appEnv,
744                                                              deviceFamily);
745  SimulatorDelegate* delegate =
746      [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath
747                                       developerDir:developerDir
748                                      simulatorHome:simHomePath] autorelease];
749  DTiPhoneSimulatorSession* session = BuildSession(delegate);
750
751  // Start the simulator session.
752  NSError* error;
753  BOOL started = [session requestStartWithConfig:config
754                                         timeout:sessionStartTimeout
755                                           error:&error];
756
757  // Spin the runtime indefinitely. When the delegate gets the message that the
758  // app has quit it will exit this program.
759  if (started) {
760    [[NSRunLoop mainRunLoop] run];
761  } else {
762    LogError(@"Simulator failed request to start:  \"%@\" (%@:%ld)",
763             [error localizedDescription],
764             [error domain], static_cast<long int>([error code]));
765  }
766
767  // Note that this code is only executed if the simulator fails to start
768  // because once the main run loop is started, only the delegate calling
769  // exit() will end the program.
770  [pool drain];
771  return kExitFailure;
772}
773