Skip to content

Commit 57bd005

Browse files
authored
Merge pull request #34 from NativeScript/implement-private-boot-path
Implement private CoreSimulator boot path and direct display attach
2 parents 28b0178 + 3f1b2af commit 57bd005

14 files changed

Lines changed: 437 additions & 192 deletions

File tree

.github/workflows/simdeck-provider.yml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,6 @@ jobs:
178178
raise SystemExit("No available iOS simulator")
179179
')"
180180
181-
xcrun simctl boot "$ios_udid" || true
182-
if ! run_with_timeout 180 xcrun simctl bootstatus "$ios_udid" -b; then
183-
echo "simctl bootstatus timed out"
184-
xcrun simctl list devices "$ios_udid" || true
185-
exit 1
186-
fi
187-
188181
dump_logs() {
189182
echo "--- simdeck.log ---"
190183
cat simdeck.log || true
@@ -233,6 +226,12 @@ jobs:
233226
: > simdeck.log
234227
simdeck_pid=""
235228
start_simdeck_server
229+
./build/simdeck --server-url http://127.0.0.1:4310 boot "$ios_udid"
230+
if ! run_with_timeout 180 xcrun simctl bootstatus "$ios_udid" -b; then
231+
echo "simctl bootstatus timed out"
232+
xcrun simctl list devices "$ios_udid" || true
233+
exit 1
234+
fi
236235
echo "Skipping private stream prewarm; the first browser WebRTC connection will attach the stream."
237236
238237
provider_base_url="$SIMDECK_CLOUD_URL/simulator/$PREVIEW_ID"

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
3838
- `cli/native/XCWNativeSession.*`
3939
Wraps one Objective-C private simulator session handle for the Rust registry.
4040
- `cli/XCWPrivateSimulatorBooter.*`
41-
Uses private `CoreSimulator` APIs for direct simulator boot when available, with `simctl` as the fallback path.
41+
Uses private `CoreSimulator` APIs for direct simulator boot without launching Simulator.app.
4242
- `cli/XCWChromeRenderer.*`
4343
Renders Apple’s CoreSimulator device-type PDF chrome assets into PNGs for the browser.
4444
- `client/src/app/App.tsx`
@@ -78,6 +78,7 @@ Private simulator behavior is implemented locally in:
7878
- Accessibility bridge: `cli/XCWAccessibilityBridge.*`
7979

8080
The current repo uses the private boot path, private display bridge, and private accessibility translation bridge directly. The browser streams frames from that bridge, injects touch and keyboard events through the same native session layer, inspects accessibility through `AccessibilityPlatformTranslation`, and renders device chrome from `cli/XCWChromeRenderer.*`.
81+
CoreSimulator service contexts resolve the active developer directory from `DEVELOPER_DIR`, then `xcode-select -p`, then `/Applications/Xcode.app/Contents/Developer`. The display bridge prefers direct CoreSimulator screen IOSurface callbacks and activates the SimulatorKit offscreen renderable view only if direct callbacks are unavailable.
8182
Physical chrome button support uses DeviceKit `chrome.json` input geometry for browser hit targets. Volume, action, mute, Apple Watch digital crown, Watch side button, and Watch left-side button dispatch through `IndigoHIDMessageForHIDArbitrary` with consumer/telephony/vendor HID usage pairs from the device chrome metadata; home, lock, and app-switcher remain on the existing SimulatorKit button paths. Apple Watch Digital Crown rotation dispatches through `IndigoHIDMessageForScrollEvent` with the same digitizer target as touch input.
8283
WebKit inspection uses the simulator `webinspectord` Unix socket named `com.apple.webinspectord_sim.socket` and WebKit's binary-plist Remote Inspector selectors. It lists only WebKit content that the runtime exposes as inspectable. For app-owned `WKWebView` on iOS 16.4 and newer, the app must set `isInspectable = true`.
8384

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,10 @@ simdeck chrome-profile <udid>
162162
simdeck logs <udid> --seconds 30 --limit 200
163163
```
164164

165-
`boot` prefers SimDeck's private CoreSimulator boot path so it can start devices
166-
without launching Simulator.app, then falls back to `xcrun simctl` when private
167-
booting is unavailable.
165+
`boot` uses SimDeck's private CoreSimulator boot path so it can start devices
166+
without launching Simulator.app. If that private path is unavailable, the
167+
command returns the CoreSimulator error instead of falling back to
168+
`xcrun simctl boot`.
168169

169170
Android emulators appear in `simdeck list` with IDs like
170171
`android:SimDeck_Pixel_8_API_36`. For Android IDs, lifecycle, install, launch,

cli/DFPrivateSimulatorDisplayBridge.m

Lines changed: 123 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,10 @@
120120
static const NSUInteger DFKeyboardModifierCommand = 1 << 3;
121121
static const NSUInteger DFKeyboardModifierCapsLock = 1 << 4;
122122

123-
static NSString *DFSimulatorKitExecutablePath(void) {
123+
static NSString *DFActiveDeveloperDirectory(void) {
124124
const char *developerDir = getenv("DEVELOPER_DIR");
125125
if (developerDir != NULL && developerDir[0] != '\0') {
126-
return [[NSString stringWithUTF8String:developerDir] stringByAppendingPathComponent:@"Library/PrivateFrameworks/SimulatorKit.framework/SimulatorKit"];
126+
return [NSString stringWithUTF8String:developerDir];
127127
}
128128

129129
FILE *pipe = popen("/usr/bin/xcode-select -p 2>/dev/null", "r");
@@ -133,31 +133,26 @@
133133
NSString *selected = [[NSString stringWithUTF8String:buffer] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
134134
pclose(pipe);
135135
if (selected.length > 0) {
136-
return [selected stringByAppendingPathComponent:@"Library/PrivateFrameworks/SimulatorKit.framework/SimulatorKit"];
136+
return selected;
137137
}
138138
} else {
139139
pclose(pipe);
140140
}
141141
}
142142

143+
return @"/Applications/Xcode.app/Contents/Developer";
144+
}
145+
146+
static NSString *DFSimulatorKitExecutablePath(void) {
147+
NSString *developerDir = DFActiveDeveloperDirectory();
148+
if (developerDir.length > 0) {
149+
return [developerDir stringByAppendingPathComponent:@"Library/PrivateFrameworks/SimulatorKit.framework/SimulatorKit"];
150+
}
143151
return @"/Applications/Xcode.app/Contents/Developer/Library/PrivateFrameworks/SimulatorKit.framework/SimulatorKit";
144152
}
145153

146154
static NSInteger DFXcodeMajorVersion(void) {
147-
NSString *developerPath = nil;
148-
const char *developerDir = getenv("DEVELOPER_DIR");
149-
if (developerDir != NULL && developerDir[0] != '\0') {
150-
developerPath = [NSString stringWithUTF8String:developerDir];
151-
} else {
152-
FILE *pipe = popen("/usr/bin/xcode-select -p 2>/dev/null", "r");
153-
if (pipe != NULL) {
154-
char buffer[PATH_MAX] = {0};
155-
if (fgets(buffer, sizeof(buffer), pipe) != NULL) {
156-
developerPath = [[NSString stringWithUTF8String:buffer] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
157-
}
158-
pclose(pipe);
159-
}
160-
}
155+
NSString *developerPath = DFActiveDeveloperDirectory();
161156
if (developerPath.length == 0) {
162157
return 0;
163158
}
@@ -590,6 +585,103 @@ static id DFSendObject(id target, const char *selectorName) {
590585
return trimmed.length > 0 ? trimmed : nil;
591586
}
592587

588+
static id DFCreateCoreSimulatorServiceContext(NSError **error) {
589+
Class serviceContextClass = NSClassFromString(@"SimServiceContext");
590+
if (serviceContextClass == Nil) {
591+
if (error != NULL) {
592+
*error = DFMakeError(
593+
DFPrivateSimulatorErrorCodeServiceContextFailed,
594+
@"CoreSimulator did not expose SimServiceContext in this Xcode runtime."
595+
);
596+
}
597+
return nil;
598+
}
599+
600+
NSString *developerDir = DFActiveDeveloperDirectory();
601+
NSError *serviceError = nil;
602+
SEL sharedSelector = sel_registerName("sharedServiceContextForDeveloperDir:error:");
603+
if ([serviceContextClass respondsToSelector:sharedSelector]) {
604+
id serviceContext = ((id(*)(id, SEL, id, NSError **))objc_msgSend)(
605+
serviceContextClass,
606+
sharedSelector,
607+
developerDir,
608+
&serviceError
609+
);
610+
if (serviceContext != nil) {
611+
return serviceContext;
612+
}
613+
DFLog(@"sharedServiceContextForDeveloperDir:error: failed for %@: %@", developerDir, serviceError.localizedDescription ?: @"unknown error");
614+
}
615+
616+
serviceError = nil;
617+
SEL initSelector = sel_registerName("initWithDeveloperDir:connectionType:error:");
618+
id contextAlloc = ((id(*)(id, SEL))objc_msgSend)(serviceContextClass, sel_registerName("alloc"));
619+
if (![contextAlloc respondsToSelector:initSelector]) {
620+
if (error != NULL) {
621+
*error = DFMakeError(
622+
DFPrivateSimulatorErrorCodeServiceContextFailed,
623+
@"CoreSimulator did not expose a supported SimServiceContext initializer."
624+
);
625+
}
626+
return nil;
627+
}
628+
id serviceContext = ((id(*)(id, SEL, id, long long, NSError **))objc_msgSend)(
629+
contextAlloc,
630+
initSelector,
631+
developerDir,
632+
0LL,
633+
&serviceError
634+
);
635+
if (serviceContext == nil && error != NULL) {
636+
*error = serviceError ?: DFMakeError(
637+
DFPrivateSimulatorErrorCodeServiceContextFailed,
638+
[NSString stringWithFormat:@"Unable to create a CoreSimulator service context for %@.", developerDir]
639+
);
640+
}
641+
return serviceContext;
642+
}
643+
644+
static NSArray *DFFlattenCoreSimulatorDevices(id devicesPayload) {
645+
if ([devicesPayload isKindOfClass:[NSArray class]]) {
646+
return devicesPayload;
647+
}
648+
if ([devicesPayload isKindOfClass:[NSSet class]]) {
649+
return [devicesPayload allObjects];
650+
}
651+
if ([devicesPayload isKindOfClass:[NSDictionary class]]) {
652+
NSMutableArray *devices = [NSMutableArray array];
653+
for (id value in [(NSDictionary *)devicesPayload allValues]) {
654+
[devices addObjectsFromArray:DFFlattenCoreSimulatorDevices(value)];
655+
}
656+
return devices;
657+
}
658+
return @[];
659+
}
660+
661+
static NSArray *DFCoreSimulatorDevicesForDeviceSet(id deviceSet) {
662+
SEL availableSelector = sel_registerName("availableDevices");
663+
if ([deviceSet respondsToSelector:availableSelector]) {
664+
NSArray *availableDevices = DFFlattenCoreSimulatorDevices(((id(*)(id, SEL))objc_msgSend)(deviceSet, availableSelector));
665+
if (availableDevices.count > 0) {
666+
return availableDevices;
667+
}
668+
}
669+
670+
SEL devicesSelector = sel_registerName("devices");
671+
if ([deviceSet respondsToSelector:devicesSelector]) {
672+
return DFFlattenCoreSimulatorDevices(((id(*)(id, SEL))objc_msgSend)(deviceSet, devicesSelector));
673+
}
674+
return @[];
675+
}
676+
677+
static NSString *DFUDIDForCoreSimulatorDevice(id device) {
678+
id deviceUDID = DFSendObject(device, "UDID");
679+
if ([deviceUDID respondsToSelector:sel_registerName("UUIDString")]) {
680+
return DFSendObject(deviceUDID, "UUIDString");
681+
}
682+
return [deviceUDID description];
683+
}
684+
593685
static id DFAllocInitRect(Class cls, NSRect rect) {
594686
id instance = ((id(*)(id, SEL))objc_msgSend)(cls, sel_registerName("alloc"));
595687
return ((id(*)(id, SEL, NSRect))objc_msgSend)(instance, sel_registerName("initWithFrame:"), rect);
@@ -2565,26 +2657,8 @@ - (nullable instancetype)initWithUDID:(NSString *)udid
25652657
dispatch_queue_set_specific(_callbackQueue, DFPrivateSimulatorCallbackQueueKey, (void *)DFPrivateSimulatorCallbackQueueKey, NULL);
25662658
[self updateStatus:[NSString stringWithFormat:@"Starting private CoreSimulator attach for %@", udid]];
25672659

2568-
Class serviceContextClass = NSClassFromString(@"SimServiceContext");
2569-
if (serviceContextClass == Nil) {
2570-
if (error != NULL) {
2571-
*error = DFMakeError(
2572-
DFPrivateSimulatorErrorCodeServiceContextFailed,
2573-
@"CoreSimulator did not expose SimServiceContext in this Xcode runtime."
2574-
);
2575-
}
2576-
return nil;
2577-
}
2578-
25792660
NSError *serviceError = nil;
2580-
id contextAlloc = ((id(*)(id, SEL))objc_msgSend)(serviceContextClass, sel_registerName("alloc"));
2581-
_serviceContext = ((id(*)(id, SEL, id, long long, NSError **))objc_msgSend)(
2582-
contextAlloc,
2583-
sel_registerName("initWithDeveloperDir:connectionType:error:"),
2584-
nil,
2585-
0LL,
2586-
&serviceError
2587-
);
2661+
_serviceContext = DFCreateCoreSimulatorServiceContext(&serviceError);
25882662
if (_serviceContext == nil) {
25892663
if (error != NULL) {
25902664
*error = serviceError ?: DFMakeError(
@@ -2607,13 +2681,9 @@ - (nullable instancetype)initWithUDID:(NSString *)udid
26072681
return nil;
26082682
}
26092683

2610-
NSArray *devices = DFSendObject(deviceSet, "devices");
2684+
NSArray *devices = DFCoreSimulatorDevicesForDeviceSet(deviceSet);
26112685
for (id candidate in devices) {
2612-
id deviceUDID = DFSendObject(candidate, "UDID");
2613-
NSString *candidateUDID = [deviceUDID respondsToSelector:sel_registerName("UUIDString")]
2614-
? DFSendObject(deviceUDID, "UUIDString")
2615-
: [deviceUDID description];
2616-
if ([candidateUDID isEqualToString:udid]) {
2686+
if ([DFUDIDForCoreSimulatorDevice(candidate) isEqualToString:udid]) {
26172687
_device = candidate;
26182688
break;
26192689
}
@@ -2894,8 +2964,17 @@ - (nullable instancetype)initWithUDID:(NSString *)udid
28942964
_displayView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 430, 932)];
28952965
_displayView.wantsLayer = YES;
28962966
}
2897-
[self updateStatus:@"Waiting for IOSurface callback"];
2898-
[self activateDisplayIfNeeded];
2967+
[self updateStatus:@"Waiting for direct CoreSimulator IOSurface callback"];
2968+
NSDate *directFrameDeadline = [NSDate dateWithTimeIntervalSinceNow:1.0];
2969+
while (_latestPixelBuffer == nil && [directFrameDeadline timeIntervalSinceNow] > 0) {
2970+
DFSpinRunLoop(0.05);
2971+
}
2972+
if (_latestPixelBuffer == nil) {
2973+
[self updateStatus:@"Direct CoreSimulator frames unavailable; attaching SimulatorKit fallback display"];
2974+
[self activateDisplayIfNeeded];
2975+
} else {
2976+
DFLog(@"Using direct CoreSimulator screen callbacks without activating SimulatorKit fallback display.");
2977+
}
28992978

29002979
DFSpinRunLoop(0.25);
29012980
return self;

0 commit comments

Comments
 (0)