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 |