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
17 changes: 17 additions & 0 deletions apple/snapshot-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ At each native fragment boundary, an absent or invalid child count means unknown
completeness and fails closed. Natural leaves above that boundary need no
continuation evidence. Unchanged native dictionaries and child arrays are reused.

## Accepted-depth hints

The host source (`packages/platform-apple/src/snapshot-source/depth-hints.ts`)
remembers the native levels a finished recovery accepted, keyed by the resolved
target id, its app generation, and this producer. The next capture of that
generation sends `nativeLevelsHint`, so the guest's first request asks for the
accepted levels instead of re-paying the known rejection. A hint changes the
request strategy only: the delivered depth, node bounds, and completeness rules
are unchanged. Hints are learned only from a recovery that observed a rejection
and then finished (a tree bounded by the requested depth or node budget still
teaches), never cross apps, generations, or producers, and expire after eight
hinted captures so the next capture probes the full depth again; a capture that
merely succeeds at the hinted depth does not renew it. Explicit raw-depth
requests neither use nor teach hints. Every response carries `recovery`
(`requests`, `rejected`, `continuations`, `acceptedLevels`), which the host
emits as the `ios_snapshot_source_recovery` diagnostic.

## Recovery conformance

`contracts/fixtures/ios-ax-recovery-conformance.json` is the shared, executable
Expand Down
6 changes: 6 additions & 0 deletions apple/snapshot-bridge/SnapshotBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ static BOOL validBoundInteger(id value, NSUInteger minimum, NSUInteger maximum,
!validBoundInteger(request[@"maxResponseBytes"], 1024, kMaximumFrameBytes, &maxResponseBytes)) {
return failureResponse(requestId, @"bad_request", @"bounds-invalid", @"snapshot bridge request bounds are outside the bridge limits");
}
NSUInteger nativeLevelsHint = 0;
if (request[@"nativeLevelsHint"] != nil &&
!validBoundInteger(request[@"nativeLevelsHint"], 1, kMaximumDepth + 1, &nativeLevelsHint)) {
return failureResponse(requestId, @"bad_request", @"bounds-invalid", @"nativeLevelsHint is outside the bridge depth limits");
}

NSString *setupError = nil;
BridgeRuntime *runtime = sharedRuntime(&setupError);
Expand All @@ -98,6 +103,7 @@ static BOOL validBoundInteger(id value, NSUInteger minimum, NSUInteger maximum,
NSDictionary *response = [runtime snapshotForProcess:pidValue.intValue
maxDepth:maxDepth
maxNodes:maxNodes
nativeLevelsHint:nativeLevelsHint
requestId:requestId
generation:generation
maxDurationMs:maxDurationMs
Expand Down
18 changes: 16 additions & 2 deletions apple/snapshot-bridge/SnapshotBridgeCapture.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@ NS_ASSUME_NONNULL_BEGIN

typedef id _Nullable (^SnapshotElementReader)(id element, NSUInteger depth, NSUInteger nodes, NSError **error);

/// Materializes one bounded tree; retries bounded native acquisition failures and re-roots withheld children.
/// Native request accounting for one acquisition: requests issued, requests the accessibility
/// server rejected at their depth, continuations (requests that reached the reader beyond the
/// root's accepted one), and the native levels of the last accepted request.
typedef struct {
NSUInteger requests;
NSUInteger rejected;
NSUInteger continuations;
NSUInteger acceptedLevels;
} SnapshotCaptureRecovery;

/// Materializes one bounded tree; retries bounded native acquisition failures and re-roots withheld
/// children. `nativeLevelsHint` (0 for none) caps only the first request's native levels; the
/// delivered depth, node bounds, and completeness rules are unchanged.
NSDictionary *_Nullable captureSnapshotTree(id element, NSUInteger maxDepth, NSUInteger maxNodes,
SnapshotElementReader reader, BOOL *truncated, NSError **error);
NSUInteger nativeLevelsHint, SnapshotElementReader reader,
BOOL *truncated, SnapshotCaptureRecovery *_Nullable recovery,
NSError **error);

NS_ASSUME_NONNULL_END
13 changes: 11 additions & 2 deletions apple/snapshot-bridge/SnapshotBridgeCapture.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ @interface SnapshotTreeCapture : NSObject
@property(nonatomic) NSUInteger remainingNodes;
@property(nonatomic) NSUInteger maximumNodes;
@property(nonatomic) NSUInteger requests;
@property(nonatomic) NSUInteger rejected;
@property(nonatomic) BOOL truncated;
- (nullable NSDictionary *)read:(id)element depth:(NSUInteger)depth error:(NSError **)error;
- (nullable NSDictionary *)materialize:(NSDictionary *)tree depth:(NSUInteger)depth nativeLevels:(NSUInteger)nativeLevels error:(NSError **)error;
Expand All @@ -34,6 +35,7 @@ - (nullable NSDictionary *)read:(id)element depth:(NSUInteger)depth error:(NSErr
NSNumber *nativeCode = failure.userInfo[@"accessibility-error"];
BOOL rejected = ([nativeCode isKindOfClass:NSNumber.class] && nativeCode.integerValue == -25201) ||
([failure.domain isEqualToString:@"com.apple.dt.xctest.automation-support.error"] && failure.code == 5);
if (rejected) self.rejected++;
if (!rejected || attemptDepth <= 1 || retries >= 2) {
if (error) *error = failure;
return nil;
Expand Down Expand Up @@ -115,15 +117,22 @@ - (nullable NSDictionary *)materialize:(NSDictionary *)tree depth:(NSUInteger)de
@end

NSDictionary *captureSnapshotTree(id element, NSUInteger maxDepth, NSUInteger maxNodes,
SnapshotElementReader reader, BOOL *truncated, NSError **error)
NSUInteger nativeLevelsHint, SnapshotElementReader reader,
BOOL *truncated, SnapshotCaptureRecovery *recovery, NSError **error)
{
SnapshotTreeCapture *capture = [SnapshotTreeCapture new];
capture.reader = reader;
capture.acceptedDepth = maxDepth + 1;
capture.acceptedDepth = nativeLevelsHint > 0 ? MIN(maxDepth + 1, nativeLevelsHint) : maxDepth + 1;
capture.remainingNodes = maxNodes;
capture.maximumNodes = maxNodes;
NSDictionary *tree = [capture read:element depth:maxDepth + 1 error:error];
NSDictionary *result = tree ? [capture materialize:tree depth:maxDepth + 1 nativeLevels:capture.acceptedDepth error:error] : nil;
*truncated = capture.truncated;
if (recovery) {
recovery->requests = capture.requests;
recovery->rejected = capture.rejected;
recovery->continuations = capture.requests - capture.rejected - (tree ? 1 : 0);
recovery->acceptedLevels = tree ? capture.acceptedDepth : 0;
}
return result;
}
1 change: 1 addition & 0 deletions apple/snapshot-bridge/SnapshotBridgeRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ NSDictionary *failureResponse(NSString *requestId,
- (nullable NSDictionary *)snapshotForProcess:(pid_t)pid
maxDepth:(NSUInteger)maxDepth
maxNodes:(NSUInteger)maxNodes
nativeLevelsHint:(NSUInteger)nativeLevelsHint
requestId:(NSString *)requestId
generation:(NSString *)generation
maxDurationMs:(NSUInteger)maxDurationMs
Expand Down
14 changes: 11 additions & 3 deletions apple/snapshot-bridge/SnapshotBridgeRuntime.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
NSString *const kProtocolVersionKey = @"protocolVersion";
NSString *const kSourceVersionKey = @"sourceVersion";
NSString *const kRequestIdKey = @"requestId";
NSString *const kSourceVersion = @"agent-device-simulator-ax-v1.5.4";
NSString *const kSourceVersion = @"agent-device-simulator-ax-v1.5.5";
const NSUInteger kProtocolVersion = 1;
const uint32_t kMaximumFrameBytes = 16 * 1024 * 1024;
const NSUInteger kMaximumDepth = 128;
Expand Down Expand Up @@ -262,6 +262,7 @@ - (nullable NSDictionary *)nodeFromSnapshot:(id)snapshot
- (nullable NSDictionary *)snapshotForProcess:(pid_t)pid
maxDepth:(NSUInteger)maxDepth
maxNodes:(NSUInteger)maxNodes
nativeLevelsHint:(NSUInteger)nativeLevelsHint
requestId:(NSString *)requestId
generation:(NSString *)generation
maxDurationMs:(NSUInteger)maxDurationMs
Expand Down Expand Up @@ -313,13 +314,14 @@ - (nullable NSDictionary *)snapshotForProcess:(pid_t)pid
NSError *runtimeError = nil;
id snapshot = nil;
BOOL acquisitionTruncated = NO;
SnapshotCaptureRecovery recovery = {0, 0, 0, 0};
@try {
if (![self isPrimaryForegroundProcess:pid]) {
if (error) *error = failureResponse(requestId, @"unsupported", @"foreground-owner-unverified", @"target app is not the primary foreground accessibility owner");
finishRequestWatchdog(watchdog, watchdogState);
return nil;
}
snapshot = captureSnapshotTree((__bridge id)raw, maxDepth, maxNodes,
snapshot = captureSnapshotTree((__bridge id)raw, maxDepth, maxNodes, nativeLevelsHint,
^id(id element, NSUInteger depth, NSUInteger nodes, NSError **captureError) {
if (![self isPrimaryForegroundProcess:pid]) {
if (captureError) *captureError = [NSError errorWithDomain:@"agent-device.snapshot" code:5
Expand All @@ -331,7 +333,7 @@ - (nullable NSDictionary *)snapshotForProcess:(pid_t)pid
bounded[@"maxChildren"] = @(nodes);
bounded[@"maxArrayCount"] = @(nodes);
return [_framework userTestingSnapshotForElement:element options:bounded error:captureError];
}, &acquisitionTruncated, &runtimeError);
}, &acquisitionTruncated, &recovery, &runtimeError);
if (![self isPrimaryForegroundProcess:pid]) {
if (error) *error = failureResponse(requestId, @"unsupported", @"foreground-owner-changed", @"foreground accessibility ownership changed during acquisition");
finishRequestWatchdog(watchdog, watchdogState);
Expand Down Expand Up @@ -385,6 +387,12 @@ - (nullable NSDictionary *)snapshotForProcess:(pid_t)pid
@"tree" : tree,
@"truncated" : @((BOOL)(truncated || acquisitionTruncated)),
@"automationEnabled" : @(automationEnabled),
@"recovery" : @{
@"requests" : @(recovery.requests),
@"rejected" : @(recovery.rejected),
@"continuations" : @(recovery.continuations),
@"acceptedLevels" : @(recovery.acceptedLevels),
},
};
}
@end
Expand Down
Loading
Loading