| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 #import <Foundation/Foundation.h> | 4 #import <Foundation/Foundation.h> |
| 5 #if defined(__MAC_10_12) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_12 | |
| 6 #include <getopt.h> | 5 #include <getopt.h> |
| 7 #include <string> | 6 #include <string> |
| 8 | 7 |
| 9 namespace { | 8 namespace { |
| 10 | 9 |
| 11 void PrintUsage() { | 10 void PrintUsage() { |
| 12 fprintf( | 11 fprintf( |
| 13 stderr, | 12 stderr, |
| 14 "Usage: iossim [-d device] [-s sdk_version] <app_path> <xctest_path>\n" | 13 "Usage: iossim [-d device] [-s sdk_version] <app_path> <xctest_path>\n" |
| 15 " where <app_path> is the path to the .app directory and <xctest_path> " | 14 " where <app_path> is the path to the .app directory and <xctest_path> " |
| (...skipping 399 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 415 } else { | 414 } else { |
| 416 LogError(@"Unable to parse command line arguments."); | 415 LogError(@"Unable to parse command line arguments."); |
| 417 PrintUsage(); | 416 PrintUsage(); |
| 418 exit(kExitInvalidArguments); | 417 exit(kExitInvalidArguments); |
| 419 } | 418 } |
| 420 | 419 |
| 421 RunApplication(app_path, xctest_path, udid, app_env, cmd_args); | 420 RunApplication(app_path, xctest_path, udid, app_env, cmd_args); |
| 422 KillSimulator(); | 421 KillSimulator(); |
| 423 return kExitSuccess; | 422 return kExitSuccess; |
| 424 } | 423 } |
| 425 #else | |
| 426 #import <Appkit/Appkit.h> | |
| 427 #include <asl.h> | |
| 428 #import <Foundation/Foundation.h> | |
| 429 #include <libgen.h> | |
| 430 #include <stdarg.h> | |
| 431 #include <stdio.h> | |
| 432 | |
| 433 // An executable (iossim) that runs an app in the iOS Simulator. | |
| 434 // Run 'iossim -h' for usage information. | |
| 435 // | |
| 436 // For best results, the iOS Simulator application should not be running when | |
| 437 // iossim is invoked. | |
| 438 // | |
| 439 // Headers for iPhoneSimulatorRemoteClient and other frameworks used in this | |
| 440 // tool are generated by class-dump, via GYP. | |
| 441 // (class-dump is available at http://www.codethecode.com/projects/class-dump/) | |
| 442 // | |
| 443 // However, there are some forward declarations required to get things to | |
| 444 // compile. | |
| 445 | |
| 446 @class DVTStackBacktrace; | |
| 447 #import "DVTFoundation.h" | |
| 448 | |
| 449 @protocol SimBridge; | |
| 450 @class DVTSimulatorApplication; | |
| 451 @class SimDeviceSet; | |
| 452 @class SimDeviceType; | |
| 453 @class SimProfilesPathMonitor; | |
| 454 @class SimRuntime; | |
| 455 @class SimServiceConnectionManager; | |
| 456 #import "CoreSimulator.h" | |
| 457 | |
| 458 @interface DVTPlatform : NSObject | |
| 459 + (BOOL)loadAllPlatformsReturningError:(id*)arg1; | |
| 460 @end | |
| 461 @class DTiPhoneSimulatorApplicationSpecifier; | |
| 462 @class DTiPhoneSimulatorSession; | |
| 463 @class DTiPhoneSimulatorSessionConfig; | |
| 464 @class DTiPhoneSimulatorSystemRoot; | |
| 465 @class DVTConfinementServiceConnection; | |
| 466 @class DVTDispatchLock; | |
| 467 @class DVTiPhoneSimulatorMessenger; | |
| 468 @class DVTNotificationToken; | |
| 469 @class DVTTask; | |
| 470 // The DTiPhoneSimulatorSessionDelegate protocol is referenced | |
| 471 // by the iPhoneSimulatorRemoteClient framework, but not defined in the object | |
| 472 // file, so it must be defined here before importing the generated | |
| 473 // iPhoneSimulatorRemoteClient.h file. | |
| 474 @protocol DTiPhoneSimulatorSessionDelegate | |
| 475 - (void)session:(DTiPhoneSimulatorSession*)session | |
| 476 didEndWithError:(NSError*)error; | |
| 477 - (void)session:(DTiPhoneSimulatorSession*)session | |
| 478 didStart:(BOOL)started | |
| 479 withError:(NSError*)error; | |
| 480 @end | |
| 481 | |
| 482 #import "DVTiPhoneSimulatorRemoteClient.h" | |
| 483 | |
| 484 // An undocumented system log key included in messages from launchd. The value | |
| 485 // is the PID of the process the message is about (as opposed to launchd's PID). | |
| 486 #define ASL_KEY_REF_PID "RefPID" | |
| 487 | |
| 488 namespace { | |
| 489 | |
| 490 // Name of environment variables that control the user's home directory in the | |
| 491 // simulator. | |
| 492 const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME"; | |
| 493 const char* const kHomeEnvVariable = "HOME"; | |
| 494 | |
| 495 // Max number of seconds to wait for the simulator session to start. | |
| 496 // This timeout must allow time to start up iOS Simulator, install the app | |
| 497 // and perform any other black magic that is encoded in the | |
| 498 // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up | |
| 499 // time is only a couple seconds but machine load, disk caches, etc., can all | |
| 500 // affect startup time in the wild so the timeout needs to be fairly generous. | |
| 501 // If this timeout occurs iossim will likely exit with non-zero status; the | |
| 502 // exception being if the app is invoked and completes execution before the | |
| 503 // session is started (this case is handled in session:didStart:withError). | |
| 504 const NSTimeInterval kDefaultSessionStartTimeoutSeconds = 30; | |
| 505 | |
| 506 // While the simulated app is running, its stdout is redirected to a file which | |
| 507 // is polled by iossim and written to iossim's stdout using the following | |
| 508 // polling interval. | |
| 509 const NSTimeInterval kOutputPollIntervalSeconds = 0.1; | |
| 510 | |
| 511 NSString* const kDVTFoundationRelativePath = | |
| 512 @"../SharedFrameworks/DVTFoundation.framework"; | |
| 513 NSString* const kDevToolsFoundationRelativePath = | |
| 514 @"../OtherFrameworks/DevToolsFoundation.framework"; | |
| 515 NSString* const kSimulatorRelativePath = | |
| 516 @"Platforms/iPhoneSimulator.platform/Developer/Applications/" | |
| 517 @"iPhone Simulator.app"; | |
| 518 | |
| 519 // Simulator Error String Key. This can be found by looking in the Simulator's | |
| 520 // Localizable.strings files. | |
| 521 NSString* const kSimulatorAppQuitErrorKey = @"The simulated application quit."; | |
| 522 | |
| 523 const char* gToolName = "iossim"; | |
| 524 | |
| 525 // Exit status codes. | |
| 526 const int kExitSuccess = EXIT_SUCCESS; | |
| 527 const int kExitFailure = EXIT_FAILURE; | |
| 528 const int kExitInvalidArguments = 2; | |
| 529 const int kExitInitializationFailure = 3; | |
| 530 const int kExitAppFailedToStart = 4; | |
| 531 const int kExitAppCrashed = 5; | |
| 532 | |
| 533 void LogError(NSString* format, ...) { | |
| 534 va_list list; | |
| 535 va_start(list, format); | |
| 536 | |
| 537 NSString* message = | |
| 538 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; | |
| 539 | |
| 540 fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]); | |
| 541 fflush(stderr); | |
| 542 | |
| 543 va_end(list); | |
| 544 } | |
| 545 | |
| 546 void LogWarning(NSString* format, ...) { | |
| 547 va_list list; | |
| 548 va_start(list, format); | |
| 549 | |
| 550 NSString* message = | |
| 551 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; | |
| 552 | |
| 553 fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]); | |
| 554 fflush(stderr); | |
| 555 | |
| 556 va_end(list); | |
| 557 } | |
| 558 | |
| 559 // Helper to find a class by name and die if it isn't found. | |
| 560 Class FindClassByName(NSString* nameOfClass) { | |
| 561 Class theClass = NSClassFromString(nameOfClass); | |
| 562 if (!theClass) { | |
| 563 LogError(@"Failed to find class %@ at runtime.", nameOfClass); | |
| 564 exit(kExitInitializationFailure); | |
| 565 } | |
| 566 return theClass; | |
| 567 } | |
| 568 | |
| 569 // Returns the a NSString containing the stdout from running an NSTask that | |
| 570 // launches |toolPath| with th given command line |args|. | |
| 571 NSString* GetOutputFromTask(NSString* toolPath, NSArray* args) { | |
| 572 NSTask* task = [[[NSTask alloc] init] autorelease]; | |
| 573 [task setLaunchPath:toolPath]; | |
| 574 [task setArguments:args]; | |
| 575 NSPipe* outputPipe = [NSPipe pipe]; | |
| 576 [task setStandardOutput:outputPipe]; | |
| 577 NSFileHandle* outputFile = [outputPipe fileHandleForReading]; | |
| 578 | |
| 579 [task launch]; | |
| 580 NSData* outputData = [outputFile readDataToEndOfFile]; | |
| 581 [task waitUntilExit]; | |
| 582 if ([task isRunning]) { | |
| 583 LogError(@"Task '%@ %@' is still running.", | |
| 584 toolPath, | |
| 585 [args componentsJoinedByString:@" "]); | |
| 586 return nil; | |
| 587 } else if ([task terminationStatus]) { | |
| 588 LogError(@"Task '%@ %@' exited with return code %d.", | |
| 589 toolPath, | |
| 590 [args componentsJoinedByString:@" "], | |
| 591 [task terminationStatus]); | |
| 592 return nil; | |
| 593 } | |
| 594 return [[[NSString alloc] initWithData:outputData | |
| 595 encoding:NSUTF8StringEncoding] autorelease]; | |
| 596 } | |
| 597 | |
| 598 // Prints supported devices and SDKs. | |
| 599 void PrintSupportedDevices() { | |
| 600 printf("Supported device/SDK combinations:\n"); | |
| 601 Class simDeviceSetClass = FindClassByName(@"SimDeviceSet"); | |
| 602 id deviceSet = | |
| 603 [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]]; | |
| 604 for (id simDevice in [deviceSet availableDevices]) { | |
| 605 NSString* deviceInfo = | |
| 606 [NSString stringWithFormat:@" -d '%@' -s '%@'\n", | |
| 607 [simDevice name], [[simDevice runtime] versionString]]; | |
| 608 printf("%s", [deviceInfo UTF8String]); | |
| 609 } | |
| 610 } | |
| 611 } // namespace | |
| 612 | |
| 613 // A delegate that is called when the simulated app is started or ended in the | |
| 614 // simulator. | |
| 615 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> { | |
| 616 @private | |
| 617 NSString* stdioPath_; | |
| 618 NSString* developerDir_; | |
| 619 NSString* simulatorHome_; | |
| 620 NSThread* outputThread_; | |
| 621 NSBundle* simulatorBundle_; | |
| 622 BOOL appRunning_; | |
| 623 } | |
| 624 @end | |
| 625 | |
| 626 // An implementation that copies the simulated app's stdio to stdout of this | |
| 627 // executable. While it would be nice to get stdout and stderr independently | |
| 628 // from iOS Simulator, issues like I/O buffering and interleaved output | |
| 629 // between iOS Simulator and the app would cause iossim to display things out | |
| 630 // of order here. Printing all output to a single file keeps the order correct. | |
| 631 // Instances of this classe should be initialized with the location of the | |
| 632 // simulated app's output file. When the simulated app starts, a thread is | |
| 633 // started which handles copying data from the simulated app's output file to | |
| 634 // the stdout of this executable. | |
| 635 @implementation SimulatorDelegate | |
| 636 | |
| 637 // Specifies the file locations of the simulated app's stdout and stderr. | |
| 638 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath | |
| 639 developerDir:(NSString*)developerDir | |
| 640 simulatorHome:(NSString*)simulatorHome { | |
| 641 self = [super init]; | |
| 642 if (self) { | |
| 643 stdioPath_ = [stdioPath copy]; | |
| 644 developerDir_ = [developerDir copy]; | |
| 645 simulatorHome_ = [simulatorHome copy]; | |
| 646 } | |
| 647 | |
| 648 return self; | |
| 649 } | |
| 650 | |
| 651 - (void)dealloc { | |
| 652 [stdioPath_ release]; | |
| 653 [developerDir_ release]; | |
| 654 [simulatorBundle_ release]; | |
| 655 [super dealloc]; | |
| 656 } | |
| 657 | |
| 658 // Reads data from the simulated app's output and writes it to stdout. This | |
| 659 // method blocks, so it should be called in a separate thread. The iOS | |
| 660 // Simulator takes a file path for the simulated app's stdout and stderr, but | |
| 661 // this path isn't always available (e.g. when the stdout is Xcode's build | |
| 662 // window). As a workaround, iossim creates a temp file to hold output, which | |
| 663 // this method reads and copies to stdout. | |
| 664 - (void)tailOutputForSession:(DTiPhoneSimulatorSession*)session { | |
| 665 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; | |
| 666 | |
| 667 NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_]; | |
| 668 NSString* versionString = | |
| 669 [[[session sessionConfig] simulatedSystemRoot] sdkVersion]; | |
| 670 NSInteger majorVersion = [[[versionString componentsSeparatedByString:@"."] | |
| 671 objectAtIndex:0] intValue]; | |
| 672 if (majorVersion >= 8) { | |
| 673 NSString* dataPath = session.sessionConfig.device.dataPath; | |
| 674 NSString* appOutput = | |
| 675 [dataPath stringByAppendingPathComponent:stdioPath_]; | |
| 676 simio = [NSFileHandle fileHandleForReadingAtPath:appOutput]; | |
| 677 } | |
| 678 NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput]; | |
| 679 // Copy data to stdout/stderr while the app is running. | |
| 680 while (appRunning_) { | |
| 681 NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init]; | |
| 682 [standardOutput writeData:[simio readDataToEndOfFile]]; | |
| 683 [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; | |
| 684 [innerPool drain]; | |
| 685 } | |
| 686 | |
| 687 // Once the app is no longer running, copy any data that was written during | |
| 688 // the last sleep cycle. | |
| 689 [standardOutput writeData:[simio readDataToEndOfFile]]; | |
| 690 | |
| 691 [pool drain]; | |
| 692 } | |
| 693 | |
| 694 // Fetches a localized error string from the Simulator. | |
| 695 - (NSString *)localizedSimulatorErrorString:(NSString*)stringKey { | |
| 696 // Lazy load of the simulator bundle. | |
| 697 if (simulatorBundle_ == nil) { | |
| 698 NSString* simulatorPath = [developerDir_ | |
| 699 stringByAppendingPathComponent:kSimulatorRelativePath]; | |
| 700 simulatorBundle_ = [NSBundle bundleWithPath:simulatorPath]; | |
| 701 } | |
| 702 NSString *localizedStr = | |
| 703 [simulatorBundle_ localizedStringForKey:stringKey | |
| 704 value:nil | |
| 705 table:nil]; | |
| 706 if ([localizedStr length]) | |
| 707 return localizedStr; | |
| 708 // Failed to get a value, follow Cocoa conventions and use the key as the | |
| 709 // string. | |
| 710 return stringKey; | |
| 711 } | |
| 712 | |
| 713 - (void)session:(DTiPhoneSimulatorSession*)session | |
| 714 didStart:(BOOL)started | |
| 715 withError:(NSError*)error { | |
| 716 if (!started) { | |
| 717 // If the test executes very quickly (<30ms), the SimulatorDelegate may not | |
| 718 // get the initial session:started:withError: message indicating successful | |
| 719 // startup of the simulated app. Instead the delegate will get a | |
| 720 // session:started:withError: message after the timeout has elapsed. To | |
| 721 // account for this case, check if the simulated app's stdio file was | |
| 722 // ever created and if it exists dump it to stdout and return success. | |
| 723 NSFileManager* fileManager = [NSFileManager defaultManager]; | |
| 724 if ([fileManager fileExistsAtPath:stdioPath_]) { | |
| 725 appRunning_ = NO; | |
| 726 [self tailOutputForSession:session]; | |
| 727 // Note that exiting in this state leaves a process running | |
| 728 // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will | |
| 729 // prevent future simulator sessions from being started for 30 seconds | |
| 730 // unless the iOS Simulator application is killed altogether. | |
| 731 [self session:session didEndWithError:nil]; | |
| 732 | |
| 733 // session:didEndWithError should not return (because it exits) so | |
| 734 // the execution path should never get here. | |
| 735 exit(kExitFailure); | |
| 736 } | |
| 737 | |
| 738 LogError(@"Simulator failed to start: \"%@\" (%@:%ld)", | |
| 739 [error localizedDescription], | |
| 740 [error domain], static_cast<long int>([error code])); | |
| 741 PrintSupportedDevices(); | |
| 742 exit(kExitAppFailedToStart); | |
| 743 } | |
| 744 | |
| 745 // Start a thread to write contents of outputPath to stdout. | |
| 746 appRunning_ = YES; | |
| 747 outputThread_ = | |
| 748 [[NSThread alloc] initWithTarget:self | |
| 749 selector:@selector(tailOutputForSession:) | |
| 750 object:session]; | |
| 751 [outputThread_ start]; | |
| 752 } | |
| 753 | |
| 754 - (void)session:(DTiPhoneSimulatorSession*)session | |
| 755 didEndWithError:(NSError*)error { | |
| 756 appRunning_ = NO; | |
| 757 // Wait for the output thread to finish copying data to stdout. | |
| 758 if (outputThread_) { | |
| 759 while (![outputThread_ isFinished]) { | |
| 760 [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; | |
| 761 } | |
| 762 [outputThread_ release]; | |
| 763 outputThread_ = nil; | |
| 764 } | |
| 765 | |
| 766 if (error) { | |
| 767 // There appears to be a race condition where sometimes the simulator | |
| 768 // framework will end with an error, but the error is that the simulated | |
| 769 // app cleanly shut down; try to trap this error and don't fail the | |
| 770 // simulator run. | |
| 771 NSString* localizedDescription = [error localizedDescription]; | |
| 772 NSString* ignorableErrorStr = | |
| 773 [self localizedSimulatorErrorString:kSimulatorAppQuitErrorKey]; | |
| 774 if ([ignorableErrorStr isEqual:localizedDescription]) { | |
| 775 LogWarning(@"Ignoring that Simulator ended with: \"%@\" (%@:%ld)", | |
| 776 localizedDescription, [error domain], | |
| 777 static_cast<long int>([error code])); | |
| 778 } else { | |
| 779 LogError(@"Simulator ended with error: \"%@\" (%@:%ld)", | |
| 780 localizedDescription, [error domain], | |
| 781 static_cast<long int>([error code])); | |
| 782 exit(kExitFailure); | |
| 783 } | |
| 784 } | |
| 785 | |
| 786 // Try to determine if the simulated app crashed or quit with a non-zero | |
| 787 // status code. iOS Simluator handles things a bit differently depending on | |
| 788 // the version, so first determine the iOS version being used. | |
| 789 BOOL badEntryFound = NO; | |
| 790 | |
| 791 // The iOS Simulator's system logging is sandboxed, so parse the sandboxed | |
| 792 // system.log file for known errors. | |
| 793 NSString* path; | |
| 794 NSString* dataPath = session.sessionConfig.device.dataPath; | |
| 795 path = | |
| 796 [dataPath stringByAppendingPathComponent:@"Library/Logs/system.log"]; | |
| 797 NSFileManager* fileManager = [NSFileManager defaultManager]; | |
| 798 if ([fileManager fileExistsAtPath:path]) { | |
| 799 NSString* content = | |
| 800 [NSString stringWithContentsOfFile:path | |
| 801 encoding:NSUTF8StringEncoding | |
| 802 error:NULL]; | |
| 803 NSArray* lines = [content componentsSeparatedByCharactersInSet: | |
| 804 [NSCharacterSet newlineCharacterSet]]; | |
| 805 NSString* simulatedAppPID = | |
| 806 [NSString stringWithFormat:@"%d", session.simulatedApplicationPID]; | |
| 807 NSArray* kErrorStrings = @[ | |
| 808 @"Service exited with abnormal code:", | |
| 809 @"Service exited due to signal:", | |
| 810 ]; | |
| 811 for (NSString* line in lines) { | |
| 812 if ([line rangeOfString:simulatedAppPID].location != NSNotFound) { | |
| 813 for (NSString* errorString in kErrorStrings) { | |
| 814 if ([line rangeOfString:errorString].location != NSNotFound) { | |
| 815 LogWarning(@"Console message: %@", line); | |
| 816 badEntryFound = YES; | |
| 817 break; | |
| 818 } | |
| 819 } | |
| 820 if (badEntryFound) { | |
| 821 break; | |
| 822 } | |
| 823 } | |
| 824 } | |
| 825 // Remove the log file so subsequent invocations of iossim won't be | |
| 826 // looking at stale logs. | |
| 827 remove([path fileSystemRepresentation]); | |
| 828 } else { | |
| 829 LogWarning(@"Unable to find system log at '%@'.", path); | |
| 830 } | |
| 831 | |
| 832 // If the query returned any nasty-looking results, iossim should exit with | |
| 833 // non-zero status. | |
| 834 if (badEntryFound) { | |
| 835 LogError(@"Simulated app crashed or exited with non-zero status"); | |
| 836 exit(kExitAppCrashed); | |
| 837 } | |
| 838 exit(kExitSuccess); | |
| 839 } | |
| 840 @end | |
| 841 | |
| 842 namespace { | |
| 843 | |
| 844 // Finds the developer dir via xcode-select or the DEVELOPER_DIR environment | |
| 845 // variable. | |
| 846 NSString* FindDeveloperDir() { | |
| 847 // Check the env first. | |
| 848 NSDictionary* env = [[NSProcessInfo processInfo] environment]; | |
| 849 NSString* developerDir = [env objectForKey:@"DEVELOPER_DIR"]; | |
| 850 if ([developerDir length] > 0) | |
| 851 return developerDir; | |
| 852 | |
| 853 // Go look for it via xcode-select. | |
| 854 NSString* output = GetOutputFromTask(@"/usr/bin/xcode-select", | |
| 855 @[ @"-print-path" ]); | |
| 856 output = [output stringByTrimmingCharactersInSet: | |
| 857 [NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
| 858 if ([output length] == 0) | |
| 859 output = nil; | |
| 860 return output; | |
| 861 } | |
| 862 | |
| 863 // Loads the Simulator framework from the given developer dir. | |
| 864 NSBundle* LoadSimulatorFramework(NSString* developerDir) { | |
| 865 // The Simulator framework depends on some of the other Xcode private | |
| 866 // frameworks; manually load them first so everything can be linked up. | |
| 867 NSString* dvtFoundationPath = [developerDir | |
| 868 stringByAppendingPathComponent:kDVTFoundationRelativePath]; | |
| 869 NSBundle* dvtFoundationBundle = | |
| 870 [NSBundle bundleWithPath:dvtFoundationPath]; | |
| 871 if (![dvtFoundationBundle load]) | |
| 872 return nil; | |
| 873 | |
| 874 NSString* devToolsFoundationPath = [developerDir | |
| 875 stringByAppendingPathComponent:kDevToolsFoundationRelativePath]; | |
| 876 NSBundle* devToolsFoundationBundle = | |
| 877 [NSBundle bundleWithPath:devToolsFoundationPath]; | |
| 878 if (![devToolsFoundationBundle load]) | |
| 879 return nil; | |
| 880 | |
| 881 // Prime DVTPlatform. | |
| 882 NSError* error; | |
| 883 Class DVTPlatformClass = FindClassByName(@"DVTPlatform"); | |
| 884 if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) { | |
| 885 LogError(@"Unable to loadAllPlatformsReturningError. Error: %@", | |
| 886 [error localizedDescription]); | |
| 887 return nil; | |
| 888 } | |
| 889 | |
| 890 // The path within the developer dir of the private Simulator frameworks. | |
| 891 NSString* simulatorFrameworkRelativePath; | |
| 892 simulatorFrameworkRelativePath = | |
| 893 @"../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework"; | |
| 894 NSString* const kCoreSimulatorRelativePath = | |
| 895 @"Library/PrivateFrameworks/CoreSimulator.framework"; | |
| 896 NSString* coreSimulatorPath = [developerDir | |
| 897 stringByAppendingPathComponent:kCoreSimulatorRelativePath]; | |
| 898 NSBundle* coreSimulatorBundle = | |
| 899 [NSBundle bundleWithPath:coreSimulatorPath]; | |
| 900 if (![coreSimulatorBundle load]) | |
| 901 return nil; | |
| 902 NSString* simBundlePath = [developerDir | |
| 903 stringByAppendingPathComponent:simulatorFrameworkRelativePath]; | |
| 904 NSBundle* simBundle = [NSBundle bundleWithPath:simBundlePath]; | |
| 905 if (![simBundle load]) | |
| 906 return nil; | |
| 907 return simBundle; | |
| 908 } | |
| 909 | |
| 910 // Converts the given app path to an application spec, which requires an | |
| 911 // absolute path. | |
| 912 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) { | |
| 913 Class applicationSpecifierClass = | |
| 914 FindClassByName(@"DTiPhoneSimulatorApplicationSpecifier"); | |
| 915 if (![appPath isAbsolutePath]) { | |
| 916 NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath]; | |
| 917 appPath = [cwd stringByAppendingPathComponent:appPath]; | |
| 918 } | |
| 919 appPath = [appPath stringByStandardizingPath]; | |
| 920 NSFileManager* fileManager = [NSFileManager defaultManager]; | |
| 921 if (![fileManager fileExistsAtPath:appPath]) { | |
| 922 LogError(@"File not found: %@", appPath); | |
| 923 exit(kExitInvalidArguments); | |
| 924 } | |
| 925 return [applicationSpecifierClass specifierWithApplicationPath:appPath]; | |
| 926 } | |
| 927 | |
| 928 // Returns the system root for the given SDK version. If sdkVersion is nil, the | |
| 929 // default system root is returned. Will return nil if the sdkVersion is not | |
| 930 // valid. | |
| 931 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) { | |
| 932 Class systemRootClass = FindClassByName(@"DTiPhoneSimulatorSystemRoot"); | |
| 933 Class simRuntimeClass = FindClassByName(@"SimRuntime"); | |
| 934 NSArray* sorted = | |
| 935 [[simRuntimeClass supportedRuntimes] sortedArrayUsingDescriptors:@[ | |
| 936 [NSSortDescriptor sortDescriptorWithKey:@"version" ascending:YES] | |
| 937 ]]; | |
| 938 NSString* versionString = [[sorted lastObject] versionString]; | |
| 939 DTiPhoneSimulatorSystemRoot* systemRoot = | |
| 940 [systemRootClass rootWithSDKVersion:versionString]; | |
| 941 if (sdkVersion) | |
| 942 systemRoot = [systemRootClass rootWithSDKVersion:sdkVersion]; | |
| 943 | |
| 944 return systemRoot; | |
| 945 } | |
| 946 | |
| 947 // Builds a config object for starting the specified app. | |
| 948 DTiPhoneSimulatorSessionConfig* BuildSessionConfig( | |
| 949 DTiPhoneSimulatorApplicationSpecifier* appSpec, | |
| 950 DTiPhoneSimulatorSystemRoot* systemRoot, | |
| 951 NSString* stdoutPath, | |
| 952 NSString* stderrPath, | |
| 953 NSArray* appArgs, | |
| 954 NSDictionary* appEnv, | |
| 955 NSNumber* deviceFamily, | |
| 956 NSString* deviceName) { | |
| 957 Class sessionConfigClass = FindClassByName(@"DTiPhoneSimulatorSessionConfig"); | |
| 958 DTiPhoneSimulatorSessionConfig* sessionConfig = | |
| 959 [[[sessionConfigClass alloc] init] autorelease]; | |
| 960 sessionConfig.applicationToSimulateOnStart = appSpec; | |
| 961 sessionConfig.simulatedSystemRoot = systemRoot; | |
| 962 sessionConfig.localizedClientName = @"chromium"; | |
| 963 sessionConfig.simulatedApplicationStdErrPath = stderrPath; | |
| 964 sessionConfig.simulatedApplicationStdOutPath = stdoutPath; | |
| 965 sessionConfig.simulatedApplicationLaunchArgs = appArgs; | |
| 966 sessionConfig.simulatedApplicationLaunchEnvironment = appEnv; | |
| 967 sessionConfig.simulatedDeviceInfoName = deviceName; | |
| 968 sessionConfig.simulatedDeviceFamily = deviceFamily; | |
| 969 | |
| 970 Class simDeviceTypeClass = FindClassByName(@"SimDeviceType"); | |
| 971 id simDeviceType = | |
| 972 [simDeviceTypeClass supportedDeviceTypesByAlias][deviceName]; | |
| 973 Class simRuntimeClass = FindClassByName(@"SimRuntime"); | |
| 974 NSString* identifier = systemRoot.runtime.identifier; | |
| 975 id simRuntime = [simRuntimeClass supportedRuntimesByIdentifier][identifier]; | |
| 976 | |
| 977 // Attempt to use an existing device, but create one if a suitable match | |
| 978 // can't be found. For example, if the simulator is running with a | |
| 979 // non-default home directory (e.g. via iossim's -u command line arg) then | |
| 980 // there won't be any devices so one will have to be created. | |
| 981 Class simDeviceSetClass = FindClassByName(@"SimDeviceSet"); | |
| 982 id deviceSet = | |
| 983 [simDeviceSetClass setForSetPath:[simDeviceSetClass defaultSetPath]]; | |
| 984 id simDevice = nil; | |
| 985 for (id device in [deviceSet availableDevices]) { | |
| 986 if ([device runtime] == simRuntime && | |
| 987 [device deviceType] == simDeviceType) { | |
| 988 simDevice = device; | |
| 989 break; | |
| 990 } | |
| 991 } | |
| 992 if (!simDevice) { | |
| 993 NSError* error = nil; | |
| 994 // n.b. only the device name is necessary because the iOS Simulator menu | |
| 995 // already splits devices by runtime version. | |
| 996 NSString* name = [NSString stringWithFormat:@"iossim - %@ ", deviceName]; | |
| 997 simDevice = [deviceSet createDeviceWithType:simDeviceType | |
| 998 runtime:simRuntime | |
| 999 name:name | |
| 1000 error:&error]; | |
| 1001 if (error) { | |
| 1002 LogError(@"Failed to create device: %@", error); | |
| 1003 exit(kExitInitializationFailure); | |
| 1004 } | |
| 1005 } | |
| 1006 sessionConfig.device = simDevice; | |
| 1007 return sessionConfig; | |
| 1008 } | |
| 1009 | |
| 1010 // Builds a simulator session that will use the given delegate. | |
| 1011 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) { | |
| 1012 Class sessionClass = FindClassByName(@"DTiPhoneSimulatorSession"); | |
| 1013 DTiPhoneSimulatorSession* session = | |
| 1014 [[[sessionClass alloc] init] autorelease]; | |
| 1015 session.delegate = delegate; | |
| 1016 return session; | |
| 1017 } | |
| 1018 | |
| 1019 // Creates a temporary directory with a unique name based on the provided | |
| 1020 // template. The template should not contain any path separators and be suffixed | |
| 1021 // with X's, which will be substituted with a unique alphanumeric string (see | |
| 1022 // 'man mkdtemp' for details). The directory will be created as a subdirectory | |
| 1023 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX', | |
| 1024 // this method would return something like '/path/to/tempdir/test-3n2'. | |
| 1025 // | |
| 1026 // Returns the absolute path of the newly-created directory, or nill if unable | |
| 1027 // to create a unique directory. | |
| 1028 NSString* CreateTempDirectory(NSString* dirNameTemplate) { | |
| 1029 NSString* fullPathTemplate = | |
| 1030 [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate]; | |
| 1031 char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String])); | |
| 1032 if (fullPath == NULL) | |
| 1033 return nil; | |
| 1034 | |
| 1035 return [NSString stringWithUTF8String:fullPath]; | |
| 1036 } | |
| 1037 | |
| 1038 // Creates the necessary directory structure under the given user home directory | |
| 1039 // path. | |
| 1040 // Returns YES if successful, NO if unable to create the directories. | |
| 1041 BOOL CreateHomeDirSubDirs(NSString* userHomePath) { | |
| 1042 NSFileManager* fileManager = [NSFileManager defaultManager]; | |
| 1043 | |
| 1044 // Create user home and subdirectories. | |
| 1045 NSArray* subDirsToCreate = [NSArray arrayWithObjects: | |
| 1046 @"Documents", | |
| 1047 @"Library/Caches", | |
| 1048 @"Library/Preferences", | |
| 1049 nil]; | |
| 1050 for (NSString* subDir in subDirsToCreate) { | |
| 1051 NSString* path = [userHomePath stringByAppendingPathComponent:subDir]; | |
| 1052 NSError* error; | |
| 1053 if (![fileManager createDirectoryAtPath:path | |
| 1054 withIntermediateDirectories:YES | |
| 1055 attributes:nil | |
| 1056 error:&error]) { | |
| 1057 LogError(@"Unable to create directory: %@. Error: %@", | |
| 1058 path, [error localizedDescription]); | |
| 1059 return NO; | |
| 1060 } | |
| 1061 } | |
| 1062 | |
| 1063 return YES; | |
| 1064 } | |
| 1065 | |
| 1066 // Creates the necessary directory structure under the given user home directory | |
| 1067 // path, then sets the path in the appropriate environment variable. | |
| 1068 // Returns YES if successful, NO if unable to create or initialize the given | |
| 1069 // directory. | |
| 1070 BOOL InitializeSimulatorUserHome(NSString* userHomePath) { | |
| 1071 if (!CreateHomeDirSubDirs(userHomePath)) | |
| 1072 return NO; | |
| 1073 | |
| 1074 // Update the environment to use the specified directory as the user home | |
| 1075 // directory. | |
| 1076 // Note: the third param of setenv specifies whether or not to overwrite the | |
| 1077 // variable's value if it has already been set. | |
| 1078 if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) || | |
| 1079 (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) { | |
| 1080 LogError(@"Unable to set environment variables for home directory."); | |
| 1081 return NO; | |
| 1082 } | |
| 1083 | |
| 1084 return YES; | |
| 1085 } | |
| 1086 | |
| 1087 // Prints the usage information to stderr. | |
| 1088 void PrintUsage() { | |
| 1089 fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] " | |
| 1090 "[-e envKey=value]* [-t startupTimeout] <appPath> [<appArgs>]\n" | |
| 1091 " where <appPath> is the path to the .app directory and appArgs are any" | |
| 1092 " arguments to send the simulated app.\n" | |
| 1093 "\n" | |
| 1094 "Options:\n" | |
| 1095 " -d Specifies the device (must be one of the values from the iOS" | |
| 1096 " Simulator's Hardware -> Device menu. Defaults to 'iPhone'.\n" | |
| 1097 " -s Specifies the SDK version to use (e.g '4.3')." | |
| 1098 " Will use system default if not specified.\n" | |
| 1099 " -u Specifies a user home directory for the simulator." | |
| 1100 " Will create a new directory if not specified.\n" | |
| 1101 " -e Specifies an environment key=value pair that will be" | |
| 1102 " set in the simulated application's environment.\n" | |
| 1103 " -t Specifies the session startup timeout (in seconds)." | |
| 1104 " Defaults to %d.\n" | |
| 1105 " -l List supported devices and iOS versions.\n", | |
| 1106 static_cast<int>(kDefaultSessionStartTimeoutSeconds)); | |
| 1107 } | |
| 1108 } // namespace | |
| 1109 | |
| 1110 int main(int argc, char* const argv[]) { | |
| 1111 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; | |
| 1112 | |
| 1113 // basename() may modify the passed in string and it returns a pointer to an | |
| 1114 // internal buffer. Give it a copy to modify, and copy what it returns. | |
| 1115 char* worker = strdup(argv[0]); | |
| 1116 char* toolName = basename(worker); | |
| 1117 if (toolName != NULL) { | |
| 1118 toolName = strdup(toolName); | |
| 1119 if (toolName != NULL) | |
| 1120 gToolName = toolName; | |
| 1121 } | |
| 1122 if (worker != NULL) | |
| 1123 free(worker); | |
| 1124 | |
| 1125 NSString* appPath = nil; | |
| 1126 NSString* appName = nil; | |
| 1127 NSString* sdkVersion = nil; | |
| 1128 NSString* deviceName = @"iPhone 5s"; | |
| 1129 NSString* simHomePath = nil; | |
| 1130 NSMutableArray* appArgs = [NSMutableArray array]; | |
| 1131 NSMutableDictionary* appEnv = [NSMutableDictionary dictionary]; | |
| 1132 NSTimeInterval sessionStartTimeout = kDefaultSessionStartTimeoutSeconds; | |
| 1133 | |
| 1134 NSString* developerDir = FindDeveloperDir(); | |
| 1135 if (!developerDir) { | |
| 1136 LogError(@"Unable to find developer directory."); | |
| 1137 exit(kExitInitializationFailure); | |
| 1138 } | |
| 1139 | |
| 1140 NSBundle* simulatorFramework = LoadSimulatorFramework(developerDir); | |
| 1141 if (!simulatorFramework) { | |
| 1142 LogError(@"Failed to load the Simulator Framework."); | |
| 1143 exit(kExitInitializationFailure); | |
| 1144 } | |
| 1145 | |
| 1146 // Parse the optional arguments | |
| 1147 int c; | |
| 1148 while ((c = getopt(argc, argv, "hs:d:u:e:t:l")) != -1) { | |
| 1149 switch (c) { | |
| 1150 case 's': | |
| 1151 sdkVersion = [NSString stringWithUTF8String:optarg]; | |
| 1152 break; | |
| 1153 case 'd': | |
| 1154 deviceName = [NSString stringWithUTF8String:optarg]; | |
| 1155 break; | |
| 1156 case 'u': | |
| 1157 simHomePath = [[NSFileManager defaultManager] | |
| 1158 stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; | |
| 1159 break; | |
| 1160 case 'e': { | |
| 1161 NSString* envLine = [NSString stringWithUTF8String:optarg]; | |
| 1162 NSRange range = [envLine rangeOfString:@"="]; | |
| 1163 if (range.location == NSNotFound) { | |
| 1164 LogError(@"Invalid key=value argument for -e."); | |
| 1165 PrintUsage(); | |
| 1166 exit(kExitInvalidArguments); | |
| 1167 } | |
| 1168 NSString* key = [envLine substringToIndex:range.location]; | |
| 1169 NSString* value = [envLine substringFromIndex:(range.location + 1)]; | |
| 1170 [appEnv setObject:value forKey:key]; | |
| 1171 } | |
| 1172 break; | |
| 1173 case 't': { | |
| 1174 int timeout = atoi(optarg); | |
| 1175 if (timeout > 0) { | |
| 1176 sessionStartTimeout = static_cast<NSTimeInterval>(timeout); | |
| 1177 } else { | |
| 1178 LogError(@"Invalid startup timeout (%s).", optarg); | |
| 1179 PrintUsage(); | |
| 1180 exit(kExitInvalidArguments); | |
| 1181 } | |
| 1182 } | |
| 1183 break; | |
| 1184 case 'l': | |
| 1185 PrintSupportedDevices(); | |
| 1186 exit(kExitSuccess); | |
| 1187 break; | |
| 1188 case 'h': | |
| 1189 PrintUsage(); | |
| 1190 exit(kExitSuccess); | |
| 1191 default: | |
| 1192 PrintUsage(); | |
| 1193 exit(kExitInvalidArguments); | |
| 1194 } | |
| 1195 } | |
| 1196 | |
| 1197 // There should be at least one arg left, specifying the app path. Any | |
| 1198 // additional args are passed as arguments to the app. | |
| 1199 if (optind < argc) { | |
| 1200 appPath = [[NSFileManager defaultManager] | |
| 1201 stringWithFileSystemRepresentation:argv[optind] | |
| 1202 length:strlen(argv[optind])]; | |
| 1203 appName = [appPath lastPathComponent]; | |
| 1204 while (++optind < argc) { | |
| 1205 [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]]; | |
| 1206 } | |
| 1207 } else { | |
| 1208 LogError(@"Unable to parse command line arguments."); | |
| 1209 PrintUsage(); | |
| 1210 exit(kExitInvalidArguments); | |
| 1211 } | |
| 1212 | |
| 1213 // Make sure the app path provided is legit. | |
| 1214 DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath); | |
| 1215 if (!appSpec) { | |
| 1216 LogError(@"Invalid app path: %@", appPath); | |
| 1217 exit(kExitInitializationFailure); | |
| 1218 } | |
| 1219 | |
| 1220 // Make sure the SDK path provided is legit (or nil). | |
| 1221 DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion); | |
| 1222 if (!systemRoot) { | |
| 1223 LogError(@"Invalid SDK version: %@", sdkVersion); | |
| 1224 PrintSupportedDevices(); | |
| 1225 exit(kExitInitializationFailure); | |
| 1226 } | |
| 1227 | |
| 1228 // Get the paths for stdout and stderr so the simulated app's output will show | |
| 1229 // up in the caller's stdout/stderr. | |
| 1230 NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX"); | |
| 1231 NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"]; | |
| 1232 | |
| 1233 // Determine the deviceFamily based on the deviceName | |
| 1234 NSNumber* deviceFamily = nil; | |
| 1235 Class simDeviceTypeClass = FindClassByName(@"SimDeviceType"); | |
| 1236 if ([simDeviceTypeClass supportedDeviceTypesByAlias][deviceName] == nil) { | |
| 1237 LogError(@"Invalid device name: %@.", deviceName); | |
| 1238 PrintSupportedDevices(); | |
| 1239 exit(kExitInvalidArguments); | |
| 1240 } | |
| 1241 | |
| 1242 // Set up the user home directory for the simulator only if a non-default | |
| 1243 // value was specified. | |
| 1244 if (simHomePath) { | |
| 1245 if (!InitializeSimulatorUserHome(simHomePath)) { | |
| 1246 LogError(@"Unable to initialize home directory for simulator: %@", | |
| 1247 simHomePath); | |
| 1248 exit(kExitInitializationFailure); | |
| 1249 } | |
| 1250 } else { | |
| 1251 simHomePath = NSHomeDirectory(); | |
| 1252 } | |
| 1253 | |
| 1254 // Create the config and simulator session. | |
| 1255 DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec, | |
| 1256 systemRoot, | |
| 1257 stdioPath, | |
| 1258 stdioPath, | |
| 1259 appArgs, | |
| 1260 appEnv, | |
| 1261 deviceFamily, | |
| 1262 deviceName); | |
| 1263 SimulatorDelegate* delegate = | |
| 1264 [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath | |
| 1265 developerDir:developerDir | |
| 1266 simulatorHome:simHomePath] autorelease]; | |
| 1267 DTiPhoneSimulatorSession* session = BuildSession(delegate); | |
| 1268 | |
| 1269 // Start the simulator session. | |
| 1270 NSError* error; | |
| 1271 BOOL started = [session requestStartWithConfig:config | |
| 1272 timeout:sessionStartTimeout | |
| 1273 error:&error]; | |
| 1274 | |
| 1275 // Spin the runtime indefinitely. When the delegate gets the message that the | |
| 1276 // app has quit it will exit this program. | |
| 1277 if (started) { | |
| 1278 [[NSRunLoop mainRunLoop] run]; | |
| 1279 } else { | |
| 1280 LogError(@"Simulator failed request to start: \"%@\" (%@:%ld)", | |
| 1281 [error localizedDescription], | |
| 1282 [error domain], static_cast<long int>([error code])); | |
| 1283 } | |
| 1284 | |
| 1285 // Note that this code is only executed if the simulator fails to start | |
| 1286 // because once the main run loop is started, only the delegate calling | |
| 1287 // exit() will end the program. | |
| 1288 [pool drain]; | |
| 1289 return kExitFailure; | |
| 1290 } | |
| 1291 | |
| 1292 #endif | |
| OLD | NEW |