Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ Useful direct commands:
./build/simdeck pasteboard set <udid> "hello"
./build/simdeck pasteboard get <udid>
./build/simdeck screenshot <udid> --output screen.png
./build/simdeck screenshot <udid> --with-bezel --output screen-bezel.png
./build/simdeck record <udid> --seconds 5 --output screen-recording.mp4
./build/simdeck describe <udid>
./build/simdeck tap <udid> 120 240
./build/simdeck tap <udid> --label "Continue" --wait-timeout-ms 5000
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ simdeck toggle-appearance <udid>
simdeck pasteboard set <udid> "hello"
simdeck pasteboard get <udid>
simdeck screenshot <udid> --output screen.png
simdeck screenshot <udid> --with-bezel --output screen-bezel.png
simdeck record <udid> --seconds 5 --output screen-recording.mp4
simdeck stream <udid> --frames 120 > stream.h264
simdeck describe <udid>
simdeck describe <udid> --format agent --max-depth 4
Expand Down Expand Up @@ -206,6 +208,8 @@ try {
await sim.tap(0.5, 0.5);
await sim.waitFor({ label: "Continue" });
await sim.screenshot();
await sim.screenshot({ withBezel: true });
await sim.record({ seconds: 5 });
} finally {
sim.close();
}
Expand Down
3 changes: 3 additions & 0 deletions cli/XCWChromeRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
error:(NSError * _Nullable * _Nullable)error;
+ (nullable NSData *)screenMaskPNGDataForDeviceName:(NSString *)deviceName
error:(NSError * _Nullable * _Nullable)error;
+ (nullable NSData *)screenshotPNGDataForDeviceName:(NSString *)deviceName
screenPNGData:(NSData *)screenPNGData
error:(NSError * _Nullable * _Nullable)error;
+ (nullable NSDictionary<NSString *, id> *)profileForDeviceName:(NSString *)deviceName
error:(NSError * _Nullable * _Nullable)error;

Expand Down
105 changes: 105 additions & 0 deletions cli/XCWChromeRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,111 @@ + (nullable NSData *)screenMaskPNGDataForDeviceName:(NSString *)deviceName
return [self PNGDataForPDFAtPath:maskPath scale:1.0 error:error];
}

+ (nullable NSData *)screenshotPNGDataForDeviceName:(NSString *)deviceName
screenPNGData:(NSData *)screenPNGData
error:(NSError * _Nullable __autoreleasing *)error {
NSDictionary *profile = [self profileForDeviceName:deviceName error:error];
if (profile == nil) {
return nil;
}

NSData *chromePNGData = [self PNGDataForDeviceName:deviceName includeButtons:YES error:error];
if (chromePNGData == nil) {
return nil;
}

NSImage *screenImage = [[NSImage alloc] initWithData:screenPNGData];
NSImage *chromeImage = [[NSImage alloc] initWithData:chromePNGData];
if (screenImage == nil || chromeImage == nil) {
if (error != NULL) {
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
code:15
userInfo:@{
NSLocalizedDescriptionKey: @"Unable to decode simulator screenshot or chrome PNG data.",
}];
}
return nil;
}

CGFloat scale = 3.0;
CGFloat totalWidth = [self numberValue:profile[@"totalWidth"]];
CGFloat totalHeight = [self numberValue:profile[@"totalHeight"]];
CGFloat screenX = [self numberValue:profile[@"screenX"]];
CGFloat screenY = [self numberValue:profile[@"screenY"]];
CGFloat screenWidth = [self numberValue:profile[@"screenWidth"]];
CGFloat screenHeight = [self numberValue:profile[@"screenHeight"]];
NSInteger pixelWidth = MAX((NSInteger)ceil(totalWidth * scale), 1);
NSInteger pixelHeight = MAX((NSInteger)ceil(totalHeight * scale), 1);
if (totalWidth <= 0.0 || totalHeight <= 0.0 || screenWidth <= 0.0 || screenHeight <= 0.0) {
if (error != NULL) {
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
code:16
userInfo:@{
NSLocalizedDescriptionKey: @"Device chrome profile did not include usable screenshot geometry.",
}];
}
return nil;
}

NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
pixelsWide:pixelWidth
pixelsHigh:pixelHeight
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:0
bitsPerPixel:32];
if (bitmap == nil) {
if (error != NULL) {
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
code:17
userInfo:@{
NSLocalizedDescriptionKey: @"Unable to create a bitmap for bezeled screenshot rendering.",
}];
}
return nil;
}

NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:graphicsContext];
graphicsContext.imageInterpolation = NSImageInterpolationHigh;
NSRect outputRect = NSMakeRect(0.0, 0.0, pixelWidth, pixelHeight);
[[NSColor clearColor] setFill];
NSRectFillUsingOperation(outputRect, NSCompositingOperationClear);

NSRect screenRect = NSMakeRect(screenX * scale,
pixelHeight - ((screenY + screenHeight) * scale),
screenWidth * scale,
screenHeight * scale);
NSDictionary *hints = @{ NSImageHintInterpolation: @(NSImageInterpolationHigh) };
[screenImage drawInRect:screenRect
fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0
respectFlipped:NO
hints:hints];
[chromeImage drawInRect:outputRect
fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0
respectFlipped:NO
hints:hints];
[NSGraphicsContext restoreGraphicsState];

NSData *pngData = [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
if (pngData.length == 0 && error != NULL) {
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
code:18
userInfo:@{
NSLocalizedDescriptionKey: @"Unable to encode bezeled simulator screenshot PNG.",
}];
}
return pngData.length > 0 ? pngData : nil;
}

+ (nullable NSDictionary<NSString *, id> *)profileForChromeInfo:(NSDictionary *)chromeInfo
error:(NSError * _Nullable __autoreleasing *)error {
NSDictionary *plist = chromeInfo[@"plist"];
Expand Down
7 changes: 7 additions & 0 deletions cli/XCWProcessRunner.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ NS_ASSUME_NONNULL_BEGIN
timeoutSec:(NSTimeInterval)timeoutSec
error:(NSError * _Nullable * _Nullable)error;

+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
arguments:(NSArray<NSString *> *)arguments
inputData:(nullable NSData *)inputData
timeoutSec:(NSTimeInterval)timeoutSec
timeoutSignal:(int)timeoutSignal
error:(NSError * _Nullable * _Nullable)error;

@end

NS_ASSUME_NONNULL_END
20 changes: 18 additions & 2 deletions cli/XCWProcessRunner.m
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
inputData:(NSData *)inputData
timeoutSec:(NSTimeInterval)timeoutSec
error:(NSError * _Nullable __autoreleasing *)error {
return [self runLaunchPath:launchPath
arguments:arguments
inputData:inputData
timeoutSec:timeoutSec
timeoutSignal:SIGTERM
error:error];
}

+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
arguments:(NSArray<NSString *> *)arguments
inputData:(NSData *)inputData
timeoutSec:(NSTimeInterval)timeoutSec
timeoutSignal:(int)timeoutSignal
error:(NSError * _Nullable __autoreleasing *)error {
int stdoutFD = -1;
int stderrFD = -1;
int stdinPipe[2] = { -1, -1 };
Expand Down Expand Up @@ -244,8 +258,10 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
}
if (hasTimeout && [deadline timeIntervalSinceNow] <= 0) {
timedOut = YES;
kill(pid, SIGTERM);
NSDate *killDeadline = [NSDate dateWithTimeIntervalSinceNow:2.0];
int signalToSend = timeoutSignal > 0 ? timeoutSignal : SIGTERM;
kill(pid, signalToSend);
NSTimeInterval graceSeconds = signalToSend == SIGINT ? 10.0 : 2.0;
NSDate *killDeadline = [NSDate dateWithTimeIntervalSinceNow:graceSeconds];
do {
waitResult = waitpid(pid, &waitStatus, WNOHANG);
if (waitResult == pid || (waitResult < 0 && errno != EINTR)) {
Expand Down
6 changes: 6 additions & 0 deletions cli/XCWSimctl.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
- (BOOL)launchBundleID:(NSString *)bundleID simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
- (nullable NSData *)screenshotPNGForSimulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
- (nullable NSData *)screenshotPNGForSimulatorUDID:(NSString *)udid
includeBezel:(BOOL)includeBezel
error:(NSError * _Nullable * _Nullable)error;
- (nullable NSData *)screenRecordingMP4ForSimulatorUDID:(NSString *)udid
durationSeconds:(NSTimeInterval)durationSeconds
error:(NSError * _Nullable * _Nullable)error;
- (BOOL)eraseSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
- (BOOL)installAppAtPath:(NSString *)appPath simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
- (BOOL)uninstallBundleID:(NSString *)bundleID simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
Expand Down
Loading
Loading