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