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
21 changes: 12 additions & 9 deletions .github/workflows/test-depot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,20 @@ jobs:
rm GhosttyKit.xcframework.tar.gz
test -d GhosttyKit.xcframework

- name: Display diagnostics
- name: Create virtual display
run: |
echo "=== Screen resolution ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "No display info available"
echo ""
echo "=== Window server ==="
/usr/sbin/system_profiler SPSoftwareDataType | grep -i "boot mode" || true
set -euo pipefail
echo "=== Display before ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
echo ""
echo "=== Accessibility permissions ==="
tccutil reset Accessibility 2>/dev/null || true
echo "Accessibility reset attempted"
clang -framework Foundation -framework CoreGraphics \
-o /tmp/create-virtual-display scripts/create-virtual-display.m
/tmp/create-virtual-display &

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail if virtual display process dies after launch

Starting the helper with /tmp/create-virtual-display & does not propagate its runtime failure to the step, so if CGVirtualDisplay is unavailable (or the helper exits for any reason), this step still succeeds and the workflow proceeds with no active display; UI tests then fail later with unrelated foreground-activation errors. Because set -e does not monitor background jobs, this should explicitly verify the process is still alive (or that a display appeared) before continuing.

Useful? React with 👍 / 👎.

VDISPLAY_PID=$!
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
sleep 3
echo "=== Display after ==="
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"

- name: Clean DerivedData
run: |
Expand Down
93 changes: 93 additions & 0 deletions scripts/create-virtual-display.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Creates a virtual display on headless macOS (CI runners without a physical monitor).
// Uses the private CGVirtualDisplay API from CoreGraphics.
// The display stays alive as long as this process runs.
//
// Build: clang -framework Foundation -framework CoreGraphics -o create-virtual-display create-virtual-display.m
// Usage: ./create-virtual-display &

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

// Private CoreGraphics classes (declared here since they're not in public headers)
@interface CGVirtualDisplayMode : NSObject
- (instancetype)initWithWidth:(unsigned int)width height:(unsigned int)height refreshRate:(double)refreshRate;
@end

@interface CGVirtualDisplayDescriptor : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) unsigned int maxPixelsWide;
@property (nonatomic) unsigned int maxPixelsHigh;
@property (nonatomic) CGSize sizeInMillimeters;
@property (nonatomic) unsigned int vendorID;
@property (nonatomic) unsigned int productID;
@property (nonatomic) unsigned int serialNum;
@property (nonatomic, strong) dispatch_queue_t queue;
@end

@interface CGVirtualDisplaySettings : NSObject
@property (nonatomic) unsigned int hiDPI;
@property (nonatomic, strong) NSArray *modes;
@end

@interface CGVirtualDisplay : NSObject
- (instancetype)initWithDescriptor:(CGVirtualDisplayDescriptor *)descriptor;
- (BOOL)applySettings:(CGVirtualDisplaySettings *)settings;
@property (nonatomic, readonly) unsigned int displayID;
@end

int main(int argc, const char *argv[]) {
@autoreleasepool {
unsigned int width = 1920;
unsigned int height = 1080;

// Verify the private classes exist
if (!NSClassFromString(@"CGVirtualDisplay")) {
fprintf(stderr, "ERROR: CGVirtualDisplay API not available on this system\n");
return 1;
}

// Create display mode
CGVirtualDisplayMode *mode = [[CGVirtualDisplayMode alloc] initWithWidth:width height:height refreshRate:60.0];
if (!mode) {
fprintf(stderr, "ERROR: Failed to create CGVirtualDisplayMode\n");
return 1;
}

// Configure descriptor
CGVirtualDisplayDescriptor *descriptor = [[CGVirtualDisplayDescriptor alloc] init];
descriptor.name = @"CI Virtual Display";
descriptor.maxPixelsWide = width;
descriptor.maxPixelsHigh = height;
descriptor.sizeInMillimeters = CGSizeMake(530, 300);
descriptor.vendorID = 0x1234;
descriptor.productID = 0x5678;
descriptor.serialNum = 0x0001;
descriptor.queue = dispatch_get_main_queue();

// Create virtual display
CGVirtualDisplay *display = [[CGVirtualDisplay alloc] initWithDescriptor:descriptor];
if (!display) {
fprintf(stderr, "ERROR: Failed to create CGVirtualDisplay\n");
return 1;
}

// Apply settings with display mode
CGVirtualDisplaySettings *settings = [[CGVirtualDisplaySettings alloc] init];
settings.hiDPI = 0;
settings.modes = @[mode];

BOOL ok = [display applySettings:settings];
if (!ok) {
fprintf(stderr, "ERROR: Failed to apply display settings\n");
return 1;
}

printf("Virtual display created: %ux%u@60Hz (displayID: %u)\n", width, height, display.displayID);
printf("PID: %d\n", getpid());
fflush(stdout);

// Keep alive so the display persists
dispatch_main();
}
return 0;
}
Loading