OLD | NEW |
(Empty) | |
| 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 |
| 46 namespace { |
| 47 |
| 48 // Name of environment variables that control the user's home directory in the |
| 49 // simulator. |
| 50 const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME"; |
| 51 const char* const kHomeEnvVariable = "HOME"; |
| 52 |
| 53 // Device family codes for iPhone and iPad. |
| 54 const int kIPhoneFamily = 1; |
| 55 const 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). |
| 66 const NSTimeInterval kSessionStartTimeoutSeconds = 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. |
| 71 const NSTimeInterval kOutputPollIntervalSeconds = 0.1; |
| 72 |
| 73 const char* gToolName = "iossim"; |
| 74 |
| 75 void LogError(NSString* format, ...) { |
| 76 va_list list; |
| 77 va_start(list, format); |
| 78 |
| 79 NSString* message = |
| 80 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; |
| 81 |
| 82 fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]); |
| 83 fflush(stderr); |
| 84 |
| 85 va_end(list); |
| 86 } |
| 87 |
| 88 void LogWarning(NSString* format, ...) { |
| 89 va_list list; |
| 90 va_start(list, format); |
| 91 |
| 92 NSString* message = |
| 93 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; |
| 94 |
| 95 fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]); |
| 96 fflush(stderr); |
| 97 |
| 98 va_end(list); |
| 99 } |
| 100 |
| 101 } // namespace |
| 102 |
| 103 // A delegate that is called when the simulated app is started or ended in the |
| 104 // simulator. |
| 105 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> { |
| 106 @private |
| 107 NSString* stdioPath_; // weak |
| 108 NSThread* outputThread_; |
| 109 BOOL appRunning_; |
| 110 } |
| 111 @end |
| 112 |
| 113 // An implementation that copies the simulated app's stdio to stdout of this |
| 114 // executable. While it would be nice to get stdout and stderr independently |
| 115 // from iOS Simulator, issues like I/O buffering and interleaved output |
| 116 // between iOS Simulator and the app would cause iossim to display things out |
| 117 // of order here. Printing all output to a single file keeps the order correct. |
| 118 // Instances of this classe should be initialized with the location of the |
| 119 // simulated app's output file. When the simulated app starts, a thread is |
| 120 // started which handles copying data from the simulated app's output file to |
| 121 // the stdout of this executable. |
| 122 @implementation SimulatorDelegate |
| 123 |
| 124 // Specifies the file locations of the simulated app's stdout and stderr. |
| 125 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath { |
| 126 self = [super init]; |
| 127 if (self) |
| 128 stdioPath_ = [stdioPath copy]; |
| 129 |
| 130 return self; |
| 131 } |
| 132 |
| 133 - (void)dealloc { |
| 134 [stdioPath_ release]; |
| 135 [super dealloc]; |
| 136 } |
| 137 |
| 138 // Reads data from the simulated app's output and writes it to stdout. This |
| 139 // method blocks, so it should be called in a separate thread. The iOS |
| 140 // Simulator takes a file path for the simulated app's stdout and stderr, but |
| 141 // this path isn't always available (e.g. when the stdout is Xcode's build |
| 142 // window). As a workaround, iossim creates a temp file to hold output, which |
| 143 // this method reads and copies to stdout. |
| 144 - (void)tailOutput { |
| 145 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; |
| 146 |
| 147 // Copy data to stdout/stderr while the app is running. |
| 148 NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_]; |
| 149 NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput]; |
| 150 while (appRunning_) { |
| 151 NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init]; |
| 152 [standardOutput writeData:[simio readDataToEndOfFile]]; |
| 153 [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; |
| 154 [innerPool drain]; |
| 155 } |
| 156 |
| 157 // Once the app is no longer running, copy any data that was written during |
| 158 // the last sleep cycle. |
| 159 [standardOutput writeData:[simio readDataToEndOfFile]]; |
| 160 |
| 161 [pool drain]; |
| 162 } |
| 163 |
| 164 - (void)session:(DTiPhoneSimulatorSession*)session |
| 165 didStart:(BOOL)started |
| 166 withError:(NSError*)error { |
| 167 if (!started) { |
| 168 // If the test executes very quickly (<30ms), the SimulatorDelegate may not |
| 169 // get the initial session:started:withError: message indicating successful |
| 170 // startup of the simulated app. Instead the delegate will get a |
| 171 // session:started:withError: message after the timeout has elapsed. To |
| 172 // account for this case, check if the simulated app's stdio file was |
| 173 // ever created and if it exists dump it to stdout and return success. |
| 174 NSFileManager* fileManager = [NSFileManager defaultManager]; |
| 175 if ([fileManager fileExistsAtPath:stdioPath_ isDirectory:NO]) { |
| 176 appRunning_ = NO; |
| 177 [self tailOutput]; |
| 178 // Note that exiting in this state leaves a process running |
| 179 // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will |
| 180 // prevent future simulator sessions from being started for 30 seconds |
| 181 // unless the iOS Simulator application is killed altogether. |
| 182 [self session:session didEndWithError:nil]; |
| 183 |
| 184 // session:didEndWithError should not return (because it exits) so |
| 185 // the execution path should never get here. |
| 186 exit(EXIT_FAILURE); |
| 187 } |
| 188 |
| 189 LogError(@"Simulator failed to start: %@", [error localizedDescription]); |
| 190 exit(EXIT_FAILURE); |
| 191 } |
| 192 |
| 193 // Start a thread to write contents of outputPath to stdout. |
| 194 appRunning_ = YES; |
| 195 outputThread_ = [[NSThread alloc] initWithTarget:self |
| 196 selector:@selector(tailOutput) |
| 197 object:nil]; |
| 198 [outputThread_ start]; |
| 199 } |
| 200 |
| 201 - (void)session:(DTiPhoneSimulatorSession*)session |
| 202 didEndWithError:(NSError*)error { |
| 203 appRunning_ = NO; |
| 204 // Wait for the output thread to finish copying data to stdout. |
| 205 if (outputThread_) { |
| 206 while (![outputThread_ isFinished]) { |
| 207 [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; |
| 208 } |
| 209 [outputThread_ release]; |
| 210 outputThread_ = nil; |
| 211 } |
| 212 |
| 213 if (error) { |
| 214 LogError(@"Simulator ended with error: %@", [error localizedDescription]); |
| 215 exit(EXIT_FAILURE); |
| 216 } |
| 217 |
| 218 // Check if the simulated app exited abnormally by looking for system log |
| 219 // messages from launchd that refer to the simulated app's PID. Limit query |
| 220 // to messages in the last minute since PIDs are cyclical. |
| 221 aslmsg query = asl_new(ASL_TYPE_QUERY); |
| 222 asl_set_query(query, ASL_KEY_SENDER, "launchd", |
| 223 ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING); |
| 224 asl_set_query(query, ASL_KEY_REF_PID, |
| 225 [[[session simulatedApplicationPID] stringValue] UTF8String], |
| 226 ASL_QUERY_OP_EQUAL); |
| 227 asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL); |
| 228 |
| 229 // Log any messages found. |
| 230 aslresponse response = asl_search(NULL, query); |
| 231 BOOL entryFound = NO; |
| 232 aslmsg entry; |
| 233 while ((entry = aslresponse_next(response)) != NULL) { |
| 234 entryFound = YES; |
| 235 LogWarning(@"Console message: %s", asl_get(entry, ASL_KEY_MSG)); |
| 236 } |
| 237 |
| 238 // launchd only sends messages if the process crashed or exits with a |
| 239 // non-zero status, so if the query returned any results iossim should exit |
| 240 // with non-zero status. |
| 241 if (entryFound) { |
| 242 LogError(@"Simulated app crashed or exited with non-zero status"); |
| 243 exit(EXIT_FAILURE); |
| 244 } |
| 245 exit(EXIT_SUCCESS); |
| 246 } |
| 247 @end |
| 248 |
| 249 namespace { |
| 250 |
| 251 // Converts the given app path to an application spec, which requires an |
| 252 // absolute path. |
| 253 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) { |
| 254 if (![appPath isAbsolutePath]) { |
| 255 NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath]; |
| 256 appPath = [cwd stringByAppendingPathComponent:appPath]; |
| 257 } |
| 258 appPath = [appPath stringByStandardizingPath]; |
| 259 return [DTiPhoneSimulatorApplicationSpecifier |
| 260 specifierWithApplicationPath:appPath]; |
| 261 } |
| 262 |
| 263 // Returns the system root for the given SDK version. If sdkVersion is nil, the |
| 264 // default system root is returned. Will return nil if the sdkVersion is not |
| 265 // valid. |
| 266 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) { |
| 267 DTiPhoneSimulatorSystemRoot* systemRoot = |
| 268 [DTiPhoneSimulatorSystemRoot defaultRoot]; |
| 269 if (sdkVersion) |
| 270 systemRoot = [DTiPhoneSimulatorSystemRoot rootWithSDKVersion:sdkVersion]; |
| 271 |
| 272 return systemRoot; |
| 273 } |
| 274 |
| 275 // Builds a config object for starting the specified app. |
| 276 DTiPhoneSimulatorSessionConfig* BuildSessionConfig( |
| 277 DTiPhoneSimulatorApplicationSpecifier* appSpec, |
| 278 DTiPhoneSimulatorSystemRoot* systemRoot, |
| 279 NSString* stdoutPath, |
| 280 NSString* stderrPath, |
| 281 NSArray* appArgs, |
| 282 NSNumber* deviceFamily) { |
| 283 DTiPhoneSimulatorSessionConfig* sessionConfig = |
| 284 [[[DTiPhoneSimulatorSessionConfig alloc] init] autorelease]; |
| 285 sessionConfig.applicationToSimulateOnStart = appSpec; |
| 286 sessionConfig.simulatedSystemRoot = systemRoot; |
| 287 sessionConfig.localizedClientName = @"chromium"; |
| 288 sessionConfig.simulatedApplicationStdErrPath = stderrPath; |
| 289 sessionConfig.simulatedApplicationStdOutPath = stdoutPath; |
| 290 sessionConfig.simulatedApplicationLaunchArgs = appArgs; |
| 291 // TODO(lliabraa): Add support for providing environment variables/values |
| 292 // for the simulated app's environment. The environment can be set using: |
| 293 // sessionConfig.simulatedApplicationLaunchEnvironment = |
| 294 // [NSDictionary dictionary]; |
| 295 sessionConfig.simulatedDeviceFamily = deviceFamily; |
| 296 return sessionConfig; |
| 297 } |
| 298 |
| 299 // Builds a simulator session that will use the given delegate. |
| 300 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) { |
| 301 DTiPhoneSimulatorSession* session = |
| 302 [[[DTiPhoneSimulatorSession alloc] init] autorelease]; |
| 303 session.delegate = delegate; |
| 304 return session; |
| 305 } |
| 306 |
| 307 // Creates a temporary directory with a unique name based on the provided |
| 308 // template. The template should not contain any path separators and be suffixed |
| 309 // with X's, which will be substituted with a unique alphanumeric string (see |
| 310 // 'man mkdtemp' for details). The directory will be created as a subdirectory |
| 311 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX', |
| 312 // this method would return something like '/path/to/tempdir/test-3n2'. |
| 313 // |
| 314 // Returns the absolute path of the newly-created directory, or nill if unable |
| 315 // to create a unique directory. |
| 316 NSString* CreateTempDirectory(NSString* dirNameTemplate) { |
| 317 NSString* fullPathTemplate = |
| 318 [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate]; |
| 319 char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String])); |
| 320 if (fullPath == NULL) |
| 321 return nil; |
| 322 |
| 323 return [NSString stringWithUTF8String:fullPath]; |
| 324 } |
| 325 |
| 326 // Creates the necessary directory structure under the given user home directory |
| 327 // path. |
| 328 // Returns YES if successful, NO if unable to create the directories. |
| 329 BOOL CreateHomeDirSubDirs(NSString* userHomePath) { |
| 330 NSFileManager* fileManager = [NSFileManager defaultManager]; |
| 331 |
| 332 // Create user home and subdirectories. |
| 333 NSArray* subDirsToCreate = [NSArray arrayWithObjects: |
| 334 @"Documents", |
| 335 @"Library/Caches", |
| 336 nil]; |
| 337 for (NSString* subDir in subDirsToCreate) { |
| 338 NSString* path = [userHomePath stringByAppendingPathComponent:subDir]; |
| 339 NSError* error; |
| 340 if (![fileManager createDirectoryAtPath:path |
| 341 withIntermediateDirectories:YES |
| 342 attributes:nil |
| 343 error:&error]) { |
| 344 LogError(@"Unable to create directory: %@. Error: %@", |
| 345 path, [error localizedDescription]); |
| 346 return NO; |
| 347 } |
| 348 } |
| 349 |
| 350 return YES; |
| 351 } |
| 352 |
| 353 // Creates the necessary directory structure under the given user home directory |
| 354 // path, then sets the path in the appropriate environment variable. |
| 355 // Returns YES if successful, NO if unable to create or initialize the given |
| 356 // directory. |
| 357 BOOL InitializeSimulatorUserHome(NSString* userHomePath) { |
| 358 if (!CreateHomeDirSubDirs(userHomePath)) |
| 359 return NO; |
| 360 |
| 361 // Update the environment to use the specified directory as the user home |
| 362 // directory. |
| 363 // Note: the third param of setenv specifies whether or not to overwrite the |
| 364 // variable's value if it has already been set. |
| 365 if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) || |
| 366 (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) { |
| 367 LogError(@"Unable to set environment variables for home directory."); |
| 368 return NO; |
| 369 } |
| 370 |
| 371 return YES; |
| 372 } |
| 373 |
| 374 // Prints the usage information to stderr. |
| 375 void PrintUsage() { |
| 376 fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] " |
| 377 "<appPath> [<appArgs>]\n" |
| 378 " where <appPath> is the path to the .app directory and appArgs are any" |
| 379 " arguments to send the simulated app.\n" |
| 380 "\n" |
| 381 "Options:\n" |
| 382 " -d Specifies the device (either 'iPhone' or 'iPad')." |
| 383 " Defaults to 'iPhone'.\n" |
| 384 " -s Specifies the SDK version to use (e.g '4.3')." |
| 385 " Will use system default if not specified.\n" |
| 386 " -u Specifies a user home directory for the simulator." |
| 387 " Will create a new directory if not specified.\n"); |
| 388 } |
| 389 |
| 390 } // namespace |
| 391 |
| 392 int main(int argc, char* const argv[]) { |
| 393 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; |
| 394 |
| 395 char* toolName = basename(argv[0]); |
| 396 if (toolName != NULL) |
| 397 gToolName = toolName; |
| 398 |
| 399 NSString* appPath = nil; |
| 400 NSString* appName = nil; |
| 401 NSString* sdkVersion = nil; |
| 402 NSString* deviceName = @"iPhone"; |
| 403 NSString* simHomePath = nil; |
| 404 NSMutableArray* appArgs = [NSMutableArray array]; |
| 405 |
| 406 // Parse the optional arguments |
| 407 int c; |
| 408 while ((c = getopt(argc, argv, "hs:d:u:")) != -1) { |
| 409 switch (c) { |
| 410 case 's': |
| 411 sdkVersion = [NSString stringWithUTF8String:optarg]; |
| 412 break; |
| 413 case 'd': |
| 414 deviceName = [NSString stringWithUTF8String:optarg]; |
| 415 break; |
| 416 case 'u': |
| 417 simHomePath = [[NSFileManager defaultManager] |
| 418 stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; |
| 419 break; |
| 420 case 'h': |
| 421 PrintUsage(); |
| 422 exit(EXIT_SUCCESS); |
| 423 default: |
| 424 PrintUsage(); |
| 425 exit(EXIT_FAILURE); |
| 426 } |
| 427 } |
| 428 |
| 429 // There should be at least one arg left, specifying the app path. Any |
| 430 // additional args are passed as arguments to the app. |
| 431 if (optind < argc) { |
| 432 appPath = [[NSFileManager defaultManager] |
| 433 stringWithFileSystemRepresentation:argv[optind] |
| 434 length:strlen(argv[optind])]; |
| 435 appName = [appPath lastPathComponent]; |
| 436 while (++optind < argc) { |
| 437 [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]]; |
| 438 } |
| 439 } else { |
| 440 LogError(@"Unable to parse command line arguments."); |
| 441 PrintUsage(); |
| 442 exit(EXIT_FAILURE); |
| 443 } |
| 444 |
| 445 // Make sure the app path provided is legit. |
| 446 DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath); |
| 447 if (!appSpec) { |
| 448 LogError(@"Invalid app path: %@", appPath); |
| 449 exit(EXIT_FAILURE); |
| 450 } |
| 451 |
| 452 // Make sure the SDK path provided is legit (or nil). |
| 453 DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion); |
| 454 if (!systemRoot) { |
| 455 LogError(@"Invalid SDK version: %@", sdkVersion); |
| 456 exit(EXIT_FAILURE); |
| 457 } |
| 458 |
| 459 // Get the paths for stdout and stderr so the simulated app's output will show |
| 460 // up in the caller's stdout/stderr. |
| 461 NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX"); |
| 462 NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"]; |
| 463 |
| 464 // Make sure the device name is legit. |
| 465 NSNumber* deviceFamily = nil; |
| 466 if (!deviceName || |
| 467 [@"iPhone" caseInsensitiveCompare:deviceName] == NSOrderedSame) { |
| 468 deviceFamily = [NSNumber numberWithInt:kIPhoneFamily]; |
| 469 } else if ([@"iPad" caseInsensitiveCompare:deviceName] == NSOrderedSame) { |
| 470 deviceFamily = [NSNumber numberWithInt:kIPadFamily]; |
| 471 } else { |
| 472 LogError(@"Invalid device name: %@", deviceName); |
| 473 exit(EXIT_FAILURE); |
| 474 } |
| 475 |
| 476 // Set up the user home directory for the simulator |
| 477 if (!simHomePath) { |
| 478 NSString* dirNameTemplate = |
| 479 [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName]; |
| 480 simHomePath = CreateTempDirectory(dirNameTemplate); |
| 481 if (!simHomePath) { |
| 482 LogError(@"Unable to create unique directory for template %@", |
| 483 dirNameTemplate); |
| 484 exit(EXIT_FAILURE); |
| 485 } |
| 486 } |
| 487 if (!InitializeSimulatorUserHome(simHomePath)) { |
| 488 LogError(@"Unable to initialize home directory for simulator: %@", |
| 489 simHomePath); |
| 490 exit(EXIT_FAILURE); |
| 491 } |
| 492 |
| 493 // Create the config and simulator session. |
| 494 DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec, |
| 495 systemRoot, |
| 496 stdioPath, |
| 497 stdioPath, |
| 498 appArgs, |
| 499 deviceFamily); |
| 500 SimulatorDelegate* delegate = |
| 501 [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath] autorelease]; |
| 502 DTiPhoneSimulatorSession* session = BuildSession(delegate); |
| 503 |
| 504 // Start the simulator session. |
| 505 NSError* error; |
| 506 BOOL started = [session requestStartWithConfig:config |
| 507 timeout:kSessionStartTimeoutSeconds |
| 508 error:&error]; |
| 509 |
| 510 // Spin the runtime indefinitely. When the delegate gets the message that the |
| 511 // app has quit it will exit this program. |
| 512 if (started) |
| 513 [[NSRunLoop mainRunLoop] run]; |
| 514 else |
| 515 LogError(@"Simulator failed to start: %@", [error localizedDescription]); |
| 516 |
| 517 // Note that this code is only executed if the simulator fails to start |
| 518 // because once the main run loop is started, only the delegate calling |
| 519 // exit() will end the program. |
| 520 [pool drain]; |
| 521 return EXIT_FAILURE; |
| 522 } |
OLD | NEW |