|
6 | 6 | #import "XCWPrivateSimulatorBooter.h" |
7 | 7 | #import "XCWProcessRunner.h" |
8 | 8 |
|
| 9 | +#import <dispatch/dispatch.h> |
9 | 10 | #import <errno.h> |
10 | 11 | #import <math.h> |
11 | 12 | #import <signal.h> |
@@ -53,10 +54,47 @@ - (BOOL)installIPAAtPath:(NSString *)ipaPath simulatorUDID:(NSString *)udid erro |
53 | 54 | return [value isKindOfClass:[NSString class]] ? value : @""; |
54 | 55 | } |
55 | 56 |
|
| 57 | +static NSString *XCWTrimmedString(NSString *value) { |
| 58 | + return [value stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; |
| 59 | +} |
| 60 | + |
56 | 61 | static NSNumber *XCWNumberValue(id value) { |
57 | 62 | return [value isKindOfClass:[NSNumber class]] ? value : nil; |
58 | 63 | } |
59 | 64 |
|
| 65 | +static NSString *XCWStringFromData(NSData *data) { |
| 66 | + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: @""; |
| 67 | +} |
| 68 | + |
| 69 | +static NSData *XCWDataSnapshot(NSMutableData *data) { |
| 70 | + @synchronized(data) { |
| 71 | + return [data copy]; |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +static void XCWAppendAvailableData(NSFileHandle *handle, NSMutableData *destination) { |
| 76 | + @try { |
| 77 | + NSData *chunk = [handle availableData]; |
| 78 | + if (chunk.length > 0) { |
| 79 | + @synchronized(destination) { |
| 80 | + [destination appendData:chunk]; |
| 81 | + } |
| 82 | + } |
| 83 | + } @catch (NSException *exception) { |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +static BOOL XCWWaitForTaskExit(NSTask *task, NSTimeInterval timeoutSeconds) { |
| 88 | + NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeoutSeconds]; |
| 89 | + while (task.running && [deadline timeIntervalSinceNow] > 0) { |
| 90 | + usleep(10 * 1000); |
| 91 | + } |
| 92 | + if (!task.running) { |
| 93 | + return YES; |
| 94 | + } |
| 95 | + return NO; |
| 96 | +} |
| 97 | + |
60 | 98 | static NSString * _Nullable XCWCreateTemporaryDirectory(NSString *prefix, NSError * _Nullable __autoreleasing *error) { |
61 | 99 | NSString *templatePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-XXXXXX", prefix]]; |
62 | 100 | char *directoryTemplate = strdup(templatePath.fileSystemRepresentation); |
@@ -682,38 +720,136 @@ - (nullable NSData *)screenRecordingMP4ForSimulatorUDID:(NSString *)udid |
682 | 720 |
|
683 | 721 | NSString *filename = [NSString stringWithFormat:@"simdeck-%@.mp4", NSUUID.UUID.UUIDString]; |
684 | 722 | NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; |
685 | | - XCWProcessResult *result = [self.class runSimctl:@[ |
686 | | - @"io", |
687 | | - udid, |
688 | | - @"recordVideo", |
689 | | - @"--codec=h264", |
690 | | - @"--force", |
691 | | - path, |
692 | | - ] |
693 | | - timeoutSec:durationSeconds |
694 | | - timeoutSignal:SIGINT |
695 | | - error:error]; |
696 | | - if (result == nil) { |
| 723 | + XCWProcessResult *resolvedSimctl = [XCWProcessRunner runLaunchPath:@"/usr/bin/xcrun" |
| 724 | + arguments:@[@"-f", @"simctl"] |
| 725 | + inputData:nil |
| 726 | + error:error]; |
| 727 | + if (resolvedSimctl == nil) { |
697 | 728 | [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; |
698 | 729 | return nil; |
699 | 730 | } |
700 | | - |
701 | | - BOOL expectedStop = result.terminationStatus == 0 || result.terminationStatus == 124 || result.terminationStatus == 130; |
702 | | - if (expectedStop) { |
703 | | - NSData *data = [NSData dataWithContentsOfFile:path options:0 error:error]; |
| 731 | + NSString *simctlPath = XCWTrimmedString(XCWStringFromData(resolvedSimctl.stdoutData)); |
| 732 | + if (resolvedSimctl.terminationStatus != 0 || simctlPath.length == 0) { |
704 | 733 | [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; |
705 | | - if (data.length > 0) { |
706 | | - return data; |
| 734 | + if (error != NULL) { |
| 735 | + NSString *message = resolvedSimctl.stderrString.length > 0 ? resolvedSimctl.stderrString : @"Unable to locate simctl."; |
| 736 | + *error = [self.class errorWithDescription:message code:34]; |
707 | 737 | } |
708 | | - if (error != NULL && *error == nil) { |
709 | | - *error = [self.class errorWithDescription:@"Simulator screen recording command produced an empty MP4." code:34]; |
| 738 | + return nil; |
| 739 | + } |
| 740 | + |
| 741 | + NSTask *task = [[NSTask alloc] init]; |
| 742 | + task.launchPath = simctlPath; |
| 743 | + task.arguments = @[@"io", udid, @"recordVideo", @"--codec=h264", @"--force", path]; |
| 744 | + task.standardInput = NSFileHandle.fileHandleWithNullDevice; |
| 745 | + NSPipe *stdoutPipe = [NSPipe pipe]; |
| 746 | + NSPipe *stderrPipe = [NSPipe pipe]; |
| 747 | + task.standardOutput = stdoutPipe; |
| 748 | + task.standardError = stderrPipe; |
| 749 | + |
| 750 | + NSMutableData *stdoutData = [NSMutableData data]; |
| 751 | + NSMutableData *stderrData = [NSMutableData data]; |
| 752 | + NSFileHandle *stdoutHandle = stdoutPipe.fileHandleForReading; |
| 753 | + NSFileHandle *stderrHandle = stderrPipe.fileHandleForReading; |
| 754 | + dispatch_semaphore_t recordingStartedSemaphore = dispatch_semaphore_create(0); |
| 755 | + __block BOOL recordingStarted = NO; |
| 756 | + |
| 757 | + stdoutHandle.readabilityHandler = ^(NSFileHandle *handle) { |
| 758 | + XCWAppendAvailableData(handle, stdoutData); |
| 759 | + }; |
| 760 | + stderrHandle.readabilityHandler = ^(NSFileHandle *handle) { |
| 761 | + @try { |
| 762 | + NSData *chunk = [handle availableData]; |
| 763 | + if (chunk.length == 0) { |
| 764 | + return; |
| 765 | + } |
| 766 | + BOOL shouldSignal = NO; |
| 767 | + @synchronized(stderrData) { |
| 768 | + [stderrData appendData:chunk]; |
| 769 | + NSString *text = XCWStringFromData(stderrData); |
| 770 | + if (!recordingStarted && [text rangeOfString:@"Recording started"].location != NSNotFound) { |
| 771 | + recordingStarted = YES; |
| 772 | + shouldSignal = YES; |
| 773 | + } |
| 774 | + } |
| 775 | + if (shouldSignal) { |
| 776 | + dispatch_semaphore_signal(recordingStartedSemaphore); |
| 777 | + } |
| 778 | + } @catch (NSException *exception) { |
| 779 | + } |
| 780 | + }; |
| 781 | + |
| 782 | + NSError *launchError = nil; |
| 783 | + if (![task launchAndReturnError:&launchError]) { |
| 784 | + stdoutHandle.readabilityHandler = nil; |
| 785 | + stderrHandle.readabilityHandler = nil; |
| 786 | + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; |
| 787 | + if (error != NULL) { |
| 788 | + *error = launchError ?: [self.class errorWithDescription:@"Unable to launch simulator screen recording." code:34]; |
710 | 789 | } |
711 | 790 | return nil; |
712 | 791 | } |
713 | 792 |
|
| 793 | + NSTimeInterval startTimeout = MAX(10.0, MIN(30.0, durationSeconds + 10.0)); |
| 794 | + NSDate *startDeadline = [NSDate dateWithTimeIntervalSinceNow:startTimeout]; |
| 795 | + BOOL didStart = NO; |
| 796 | + while ([startDeadline timeIntervalSinceNow] > 0) { |
| 797 | + if (dispatch_semaphore_wait(recordingStartedSemaphore, DISPATCH_TIME_NOW) == 0) { |
| 798 | + didStart = YES; |
| 799 | + break; |
| 800 | + } |
| 801 | + if (!task.running) { |
| 802 | + break; |
| 803 | + } |
| 804 | + usleep(10 * 1000); |
| 805 | + } |
| 806 | + |
| 807 | + if (didStart) { |
| 808 | + NSDate *stopAt = [NSDate dateWithTimeIntervalSinceNow:durationSeconds]; |
| 809 | + while (task.running && [stopAt timeIntervalSinceNow] > 0) { |
| 810 | + usleep(10 * 1000); |
| 811 | + } |
| 812 | + } |
| 813 | + if (task.running) { |
| 814 | + [task interrupt]; |
| 815 | + } |
| 816 | + if (!XCWWaitForTaskExit(task, didStart ? 10.0 : 2.0) && task.running) { |
| 817 | + [task terminate]; |
| 818 | + XCWWaitForTaskExit(task, 2.0); |
| 819 | + } |
| 820 | + |
| 821 | + stdoutHandle.readabilityHandler = nil; |
| 822 | + stderrHandle.readabilityHandler = nil; |
| 823 | + XCWAppendAvailableData(stdoutHandle, stdoutData); |
| 824 | + XCWAppendAvailableData(stderrHandle, stderrData); |
| 825 | + |
| 826 | + NSError *readError = nil; |
| 827 | + NSData *data = nil; |
| 828 | + NSDate *fileDeadline = [NSDate dateWithTimeIntervalSinceNow:2.0]; |
| 829 | + do { |
| 830 | + data = [NSData dataWithContentsOfFile:path options:0 error:&readError]; |
| 831 | + if (data.length > 0) { |
| 832 | + break; |
| 833 | + } |
| 834 | + usleep(50 * 1000); |
| 835 | + } while ([fileDeadline timeIntervalSinceNow] > 0); |
714 | 836 | [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; |
| 837 | + if (data.length > 0) { |
| 838 | + return data; |
| 839 | + } |
| 840 | + |
715 | 841 | if (error != NULL) { |
716 | | - *error = [self.class errorWithDescription:result.stderrString.length > 0 ? result.stderrString : @"Unable to record simulator screen." code:34]; |
| 842 | + NSString *stderrString = XCWStringFromData(XCWDataSnapshot(stderrData)); |
| 843 | + NSString *stdoutString = XCWStringFromData(XCWDataSnapshot(stdoutData)); |
| 844 | + NSString *details = stderrString.length > 0 ? stderrString : stdoutString; |
| 845 | + if (!didStart && details.length == 0) { |
| 846 | + details = @"Simulator screen recording did not start before the timeout."; |
| 847 | + } else if (details.length == 0 && readError.localizedDescription.length > 0) { |
| 848 | + details = readError.localizedDescription; |
| 849 | + } else if (details.length == 0) { |
| 850 | + details = @"Simulator screen recording command produced an empty MP4."; |
| 851 | + } |
| 852 | + *error = [self.class errorWithDescription:details code:34]; |
717 | 853 | } |
718 | 854 | return nil; |
719 | 855 | } |
|
0 commit comments