Skip to content

Commit 28aa33a

Browse files
committed
fix(ax): recover native application root by pid
1 parent 274ad45 commit 28aa33a

2 files changed

Lines changed: 97 additions & 47 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Private simulator behavior is implemented locally in:
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.*`.
8181
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.
82+
Accessibility recovery may use a hit-tested translation only to recover the foreground pid; the returned tree must still be rooted at the tokenized `AXPTranslator` application object, because `translationApplicationObjectForPid:` can omit the bridge delegate token after private display lifecycle changes.
8283
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.
8384
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`.
8485

cli/XCWAccessibilityBridge.m

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -505,10 +505,10 @@ static pid_t XCWAXElementPID(id element) {
505505
return values;
506506
}
507507

508-
static NSArray<NSValue *> *XCWAXFallbackHitTestPoints(void) {
508+
static NSArray<NSValue *> *XCWAXRootRecoveryHitTestPoints(void) {
509509
NSMutableArray<NSValue *> *points = [NSMutableArray array];
510-
NSArray<NSNumber *> *xValues = @[@24, @100, @220, @340, @420];
511-
NSArray<NSNumber *> *yValues = @[@80, @150, @220, @300, @380, @460, @540, @620, @700, @780, @860, @930];
510+
NSArray<NSNumber *> *xValues = @[@40, @120, @220, @340, @420];
511+
NSArray<NSNumber *> *yValues = @[@120, @220, @360, @520, @700, @840];
512512
for (NSNumber *yValue in yValues) {
513513
for (NSNumber *xValue in xValues) {
514514
[points addObject:[NSValue valueWithPoint:CGPointMake(xValue.doubleValue, yValue.doubleValue)]];
@@ -517,12 +517,76 @@ static pid_t XCWAXElementPID(id element) {
517517
return points;
518518
}
519519

520-
static NSString *XCWAXElementIdentity(NSDictionary *element) {
521-
id identifier = element[@"AXUniqueId"];
522-
id label = element[@"AXLabel"];
523-
id role = element[@"role"];
524-
id frame = element[@"AXFrame"];
525-
return [@[role ?: @"", identifier ?: @"", label ?: @"", frame ?: @""] componentsJoinedByString:@"|"];
520+
static pid_t XCWAXTranslationPID(id translation) {
521+
SEL selector = sel_registerName("pid");
522+
if (translation == nil || ![translation respondsToSelector:selector]) {
523+
return 0;
524+
}
525+
@try {
526+
return ((pid_t(*)(id, SEL))objc_msgSend)(translation, selector);
527+
} @catch (NSException *exception) {
528+
XCWAXDebugLog(@"translation pid threw %@", exception);
529+
return 0;
530+
}
531+
}
532+
533+
static void XCWAXSetBridgeDelegateTokenOnTranslation(id translation, NSString *token) {
534+
if (translation != nil && [translation respondsToSelector:sel_registerName("setBridgeDelegateToken:")]) {
535+
((void(*)(id, SEL, id))objc_msgSend)(translation, sel_registerName("setBridgeDelegateToken:"), token);
536+
}
537+
}
538+
539+
static id XCWAXApplicationTranslationForPID(id translator, pid_t pid, NSString *token) {
540+
if (pid <= 0 || translator == nil) {
541+
return nil;
542+
}
543+
544+
Class requestClass = NSClassFromString(@"AXPTranslatorRequest");
545+
Class translationClass = NSClassFromString(@"AXPTranslationObject");
546+
SEL sendSelector = sel_registerName("sendTranslatorRequest:");
547+
if (requestClass != Nil && translationClass != Nil && [translator respondsToSelector:sendSelector]) {
548+
@try {
549+
// translationApplicationObjectForPid: builds this request without a
550+
// bridge token, which cannot route through the simulator delegate
551+
// after some private display lifecycle changes.
552+
id request = [requestClass new];
553+
id requestTranslation = [translationClass new];
554+
if ([requestTranslation respondsToSelector:sel_registerName("setPid:")]) {
555+
((void(*)(id, SEL, pid_t))objc_msgSend)(requestTranslation, sel_registerName("setPid:"), pid);
556+
}
557+
XCWAXSetBridgeDelegateTokenOnTranslation(requestTranslation, token);
558+
559+
if ([request respondsToSelector:sel_registerName("setRequestType:")]) {
560+
((void(*)(id, SEL, NSUInteger))objc_msgSend)(request, sel_registerName("setRequestType:"), (NSUInteger)1);
561+
}
562+
if ([request respondsToSelector:sel_registerName("setParameters:")]) {
563+
((void(*)(id, SEL, id))objc_msgSend)(request, sel_registerName("setParameters:"), @{ @"pid": @(pid) });
564+
}
565+
if ([request respondsToSelector:sel_registerName("setTranslation:")]) {
566+
((void(*)(id, SEL, id))objc_msgSend)(request, sel_registerName("setTranslation:"), requestTranslation);
567+
}
568+
569+
id response = ((id(*)(id, SEL, id))objc_msgSend)(translator, sendSelector, request);
570+
id translation = XCWAXObject(response, "translationResponse");
571+
XCWAXSetBridgeDelegateTokenOnTranslation(translation, token);
572+
if (translation != nil) {
573+
return translation;
574+
}
575+
} @catch (NSException *exception) {
576+
XCWAXDebugLog(@"tokenized application translation request for pid:%d threw %@", pid, exception);
577+
}
578+
}
579+
580+
SEL selector = sel_registerName("translationApplicationObjectForPid:");
581+
if (![translator respondsToSelector:selector]) {
582+
return nil;
583+
}
584+
@try {
585+
return ((id(*)(id, SEL, pid_t))objc_msgSend)(translator, selector, pid);
586+
} @catch (NSException *exception) {
587+
XCWAXDebugLog(@"translationApplicationObjectForPid:%d threw %@", pid, exception);
588+
return nil;
589+
}
526590
}
527591

528592
@implementation XCWAccessibilityBridge
@@ -596,65 +660,50 @@ + (nullable NSDictionary *)accessibilitySnapshotForSimulatorUDID:(NSString *)udi
596660
}
597661
}
598662
if (translation == nil && pointValue == nil) {
599-
NSMutableArray *fallbackRoots = [NSMutableArray array];
600-
NSMutableSet<NSString *> *seenElements = [NSMutableSet set];
601-
for (NSValue *fallbackPoint in XCWAXFallbackHitTestPoints()) {
663+
NSMutableSet<NSNumber *> *attemptedPIDs = [NSMutableSet set];
664+
for (NSValue *recoveryPoint in XCWAXRootRecoveryHitTestPoints()) {
602665
for (NSNumber *displayID in XCWAXCandidateDisplayIDs()) {
603666
uint32_t display = displayID.unsignedIntValue;
604-
CGPoint point = fallbackPoint.pointValue;
605-
id fallbackTranslation = ((id(*)(id, SEL, CGPoint, uint32_t, id))objc_msgSend)(
667+
CGPoint point = recoveryPoint.pointValue;
668+
id hitTranslation = ((id(*)(id, SEL, CGPoint, uint32_t, id))objc_msgSend)(
606669
translator,
607670
sel_registerName("objectAtPoint:displayId:bridgeDelegateToken:"),
608671
point,
609672
display,
610673
token
611674
);
612-
XCWAXDebugLog(@"fallback translation lookup point=%@ display=%@ result=%@", fallbackPoint, displayID, fallbackTranslation);
613-
if (fallbackTranslation == nil) {
675+
XCWAXDebugLog(@"root recovery hit-test point=%@ display=%@ result=%@", recoveryPoint, displayID, hitTranslation);
676+
pid_t pid = XCWAXTranslationPID(hitTranslation);
677+
if (pid <= 0) {
614678
continue;
615679
}
616-
if ([fallbackTranslation respondsToSelector:sel_registerName("setBridgeDelegateToken:")]) {
617-
((void(*)(id, SEL, id))objc_msgSend)(fallbackTranslation, sel_registerName("setBridgeDelegateToken:"), token);
618-
}
619-
id fallbackElement = ((id(*)(id, SEL, id))objc_msgSend)(
620-
translator,
621-
sel_registerName("macPlatformElementFromTranslation:"),
622-
fallbackTranslation
623-
);
624-
NSHashTable *visited = [NSHashTable hashTableWithOptions:NSPointerFunctionsObjectPointerPersonality];
625-
NSMutableDictionary *root = XCWAXSerializeElement(fallbackElement, token, visited, 0, MIN(maxDepth, XCWAXMaxDepth));
626-
NSString *identity = root != nil ? XCWAXElementIdentity(root) : @"";
627-
if (identity.length > 0 && ![seenElements containsObject:identity]) {
628-
[seenElements addObject:identity];
629-
[fallbackRoots addObject:root];
680+
NSNumber *pidNumber = @(pid);
681+
if (![attemptedPIDs containsObject:pidNumber]) {
682+
[attemptedPIDs addObject:pidNumber];
683+
684+
id applicationTranslation = XCWAXApplicationTranslationForPID(translator, pid, token);
685+
XCWAXDebugLog(@"root recovery pid=%d application=%@", pid, applicationTranslation);
686+
if (applicationTranslation != nil) {
687+
translation = applicationTranslation;
688+
resolvedDisplayID = displayID;
689+
break;
690+
}
630691
}
631692
}
632-
}
633-
if (fallbackRoots.count > 0) {
634-
NSArray *rootsWithChildren = [fallbackRoots filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *root, NSDictionary *bindings) {
635-
(void)bindings;
636-
NSArray *children = [root[@"children"] isKindOfClass:NSArray.class] ? root[@"children"] : @[];
637-
return children.count > 0;
638-
}]];
639-
NSArray *roots = rootsWithChildren.count > 0 ? rootsWithChildren : fallbackRoots;
640-
XCWAXDebugLog(@"frontmost lookup failed; returning %lu sampled fallback elements", (unsigned long)roots.count);
641-
return @{
642-
@"roots": roots,
643-
@"source": @"native-ax",
644-
};
693+
if (translation != nil) {
694+
break;
695+
}
645696
}
646697
}
647698

648699
if (translation == nil) {
649700
XCWAXDebugLog(@"translation lookup returned nil point=%@", pointValue);
650701
if (error != NULL) {
651-
*error = XCWAXError(9, @"No translation object returned for simulator. The point may be invalid or hidden by a fullscreen dialog.");
702+
*error = XCWAXError(9, @"No application accessibility root returned for simulator. The simulator may be between lifecycle states or hidden by a fullscreen dialog.");
652703
}
653704
return nil;
654705
}
655-
if ([translation respondsToSelector:sel_registerName("setBridgeDelegateToken:")]) {
656-
((void(*)(id, SEL, id))objc_msgSend)(translation, sel_registerName("setBridgeDelegateToken:"), token);
657-
}
706+
XCWAXSetBridgeDelegateTokenOnTranslation(translation, token);
658707
XCWAXDebugLog(@"using accessibility display %@", resolvedDisplayID);
659708

660709
id element = ((id(*)(id, SEL, id))objc_msgSend)(

0 commit comments

Comments
 (0)