simframe 0.4.2 → 0.6.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -85
- package/native/simframed/Package.swift +16 -0
- package/native/simframed/Sources/PrivateAPI/AccessibilityBridge.swift +379 -0
- package/native/simframed/Sources/PrivateAPI/CoreSimulatorPlatform.swift +523 -0
- package/native/simframed/Sources/PrivateAPI/HIDKeyboard.swift +70 -0
- package/native/simframed/Sources/PrivateAPI/IndigoHID.swift +121 -0
- package/native/simframed/Sources/PrivateAPI/PrivateAPI.swift +149 -0
- package/native/simframed/Sources/PrivateAPI/StubPlatform.swift +112 -0
- package/native/simframed/Sources/SimframeCore/Bitmap.swift +61 -0
- package/native/simframed/Sources/SimframeCore/ControlSocket.swift +122 -0
- package/native/simframed/Sources/SimframeCore/CoreGraphicsScaler.swift +70 -0
- package/native/simframed/Sources/SimframeCore/Element.swift +148 -0
- package/native/simframed/Sources/SimframeCore/FrameStore.swift +303 -0
- package/native/simframed/Sources/SimframeCore/Hashing.swift +119 -0
- package/native/simframed/Sources/SimframeCore/Motion.swift +431 -0
- package/native/simframed/Sources/SimframeCore/PNGWriter.swift +40 -0
- package/native/simframed/Sources/SimframeCore/VisionOCR.swift +75 -0
- package/native/simframed/Sources/simframed/main.swift +485 -0
- package/native/simframed/Tests/SimframeCoreTests/HashingTests.swift +270 -0
- package/package.json +12 -4
- package/scripts/bench-flow.mjs +54 -0
- package/scripts/bench.sh +98 -0
- package/scripts/check-package.mjs +99 -0
- package/scripts/ci-memory.mjs +416 -0
- package/scripts/eval-fingerprint.mjs +192 -0
- package/scripts/smoke.mjs +76 -0
- package/scripts/sync-server-version.mjs +39 -0
- package/scripts/verify-baseline.mjs +65 -0
- package/skills/simframe/SKILL.md +173 -0
- package/src/actions.js +264 -18
- package/src/cli.js +561 -89
- package/src/control.js +77 -0
- package/src/daemon.js +8 -1
- package/src/engine.js +99 -0
- package/src/fingerprint.js +183 -0
- package/src/graph.js +411 -0
- package/src/index.js +351 -24
- package/src/input.js +179 -2
- package/src/matching.js +265 -0
- package/src/mcp.js +425 -112
- package/src/navigate.js +120 -0
- package/src/refs.js +141 -0
- package/src/regions.js +267 -0
- package/src/screenmap.js +119 -22
- package/src/simctl.js +74 -5
- package/src/store.js +8 -0
- package/src/view.js +342 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// One node of the simulator's live accessibility tree, read from the host.
|
|
5
|
+
///
|
|
6
|
+
/// Frames are in **points**, in the device's own top-left coordinate space —
|
|
7
|
+
/// the same space input speaks, so a node's centre is a tap point with no
|
|
8
|
+
/// conversion.
|
|
9
|
+
public struct AXNode: Sendable {
|
|
10
|
+
public var role: String
|
|
11
|
+
public var subrole: String?
|
|
12
|
+
public var label: String?
|
|
13
|
+
public var value: String?
|
|
14
|
+
public var identifier: String?
|
|
15
|
+
public var enabled: Bool?
|
|
16
|
+
public var selected: Bool?
|
|
17
|
+
public var focused: Bool?
|
|
18
|
+
public var frame: CGRect
|
|
19
|
+
public var depth: Int
|
|
20
|
+
|
|
21
|
+
public init(role: String, subrole: String? = nil, label: String? = nil, value: String? = nil,
|
|
22
|
+
identifier: String? = nil, enabled: Bool? = nil, selected: Bool? = nil,
|
|
23
|
+
focused: Bool? = nil, frame: CGRect, depth: Int) {
|
|
24
|
+
self.role = role
|
|
25
|
+
self.subrole = subrole
|
|
26
|
+
self.label = label
|
|
27
|
+
self.value = value
|
|
28
|
+
self.identifier = identifier
|
|
29
|
+
self.enabled = enabled
|
|
30
|
+
self.selected = selected
|
|
31
|
+
self.focused = focused
|
|
32
|
+
self.frame = frame
|
|
33
|
+
self.depth = depth
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// A tree, and whether it is all of one.
|
|
38
|
+
///
|
|
39
|
+
/// The walk has three ways to stop early and the bridge has a fourth, and every
|
|
40
|
+
/// one of them produces something that looks exactly like a small screen. A
|
|
41
|
+
/// partial tree is still useful — it is not, however, authoritative, and the
|
|
42
|
+
/// layer above merges accessibility elements as the real hit targets and then
|
|
43
|
+
/// writes them into screen memory. So the shortfall travels with the nodes.
|
|
44
|
+
public struct AXTree: Sendable {
|
|
45
|
+
public let nodes: [AXNode]
|
|
46
|
+
/// Nil when the whole tree was read; otherwise why it was not.
|
|
47
|
+
public let truncated: String?
|
|
48
|
+
|
|
49
|
+
public init(nodes: [AXNode], truncated: String? = nil) {
|
|
50
|
+
self.nodes = nodes
|
|
51
|
+
self.truncated = truncated
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public enum AccessibilityError: Error, CustomStringConvertible {
|
|
56
|
+
case unavailable(String)
|
|
57
|
+
case noFrontmostApplication
|
|
58
|
+
|
|
59
|
+
public var description: String {
|
|
60
|
+
switch self {
|
|
61
|
+
case .unavailable(let d): return "accessibility unavailable: \(d)"
|
|
62
|
+
case .noFrontmostApplication: return "no frontmost application on the device"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Reads the simulator's accessibility tree from the host, with nothing
|
|
68
|
+
/// injected into the guest and no `NSView`.
|
|
69
|
+
///
|
|
70
|
+
/// The shape that works, established against the live runtime:
|
|
71
|
+
///
|
|
72
|
+
/// * `AXPTranslator.sharedInstance` on the host is the **macOS** translator.
|
|
73
|
+
/// Its job is turning iOS accessibility data into mac platform elements,
|
|
74
|
+
/// which is exactly what a host-side reader wants.
|
|
75
|
+
/// * The bridge delegate is held **weakly**, so it must be retained here.
|
|
76
|
+
/// A delegate nobody retains is deallocated at once and the translator
|
|
77
|
+
/// answers nil, looking precisely like a broken private API.
|
|
78
|
+
/// * The delegate token is not ours to invent: `SimDevice` publishes its own
|
|
79
|
+
/// as `accessibilityPlatformTranslationToken`, and that is what routes a
|
|
80
|
+
/// request to the right guest.
|
|
81
|
+
/// * The element to read is **not** the translation object. Passing that to
|
|
82
|
+
/// `processTranslatorRequest:` returns a response whose `resultData` is nil.
|
|
83
|
+
/// `AXPMacPlatformElement.platformElementWithTranslationObject:` wraps it in
|
|
84
|
+
/// something that answers ordinary `accessibilityAttributeValue:` calls,
|
|
85
|
+
/// and the whole tree walks from there.
|
|
86
|
+
///
|
|
87
|
+
/// Every call must run off the main queue: the bridge blocks waiting on the
|
|
88
|
+
/// device, and blocking main is a deadlock that presents as silence.
|
|
89
|
+
public final class AccessibilityBridge {
|
|
90
|
+
private let device: NSObject
|
|
91
|
+
private let token: Any?
|
|
92
|
+
private let translator: NSObject
|
|
93
|
+
private let elementClass: NSObject.Type
|
|
94
|
+
/// The translator holds this weakly. Releasing it breaks every later read.
|
|
95
|
+
private let delegate: BridgeDelegate
|
|
96
|
+
|
|
97
|
+
/// A tree this deep is a runaway, not a screen.
|
|
98
|
+
private static let maxDepth = 40
|
|
99
|
+
/// Enough for any real screen; a cap so a cycle cannot hang the daemon.
|
|
100
|
+
private static let maxNodes = 4000
|
|
101
|
+
|
|
102
|
+
// MARK: - Construction
|
|
103
|
+
|
|
104
|
+
public init(device: NSObject, requestTimeout: TimeInterval = 5) throws {
|
|
105
|
+
guard let handle = dlopen(Self.frameworkPath, RTLD_NOW), handle != nil else {
|
|
106
|
+
throw AccessibilityError.unavailable(
|
|
107
|
+
"AccessibilityPlatformTranslation did not load: \(String(cString: dlerror()))")
|
|
108
|
+
}
|
|
109
|
+
guard let translatorClass = NSClassFromString("AXPTranslator") as? NSObject.Type,
|
|
110
|
+
let shared = translatorClass.perform(NSSelectorFromString("sharedInstance"))?
|
|
111
|
+
.takeUnretainedValue() as? NSObject else {
|
|
112
|
+
throw AccessibilityError.unavailable("AXPTranslator.sharedInstance missing")
|
|
113
|
+
}
|
|
114
|
+
guard let elementClass = NSClassFromString("AXPMacPlatformElement") as? NSObject.Type,
|
|
115
|
+
elementClass.responds(to: NSSelectorFromString("platformElementWithTranslationObject:")) else {
|
|
116
|
+
throw AccessibilityError.unavailable("AXPMacPlatformElement missing")
|
|
117
|
+
}
|
|
118
|
+
guard shared.responds(to: NSSelectorFromString("frontmostApplicationWithDisplayId:bridgeDelegateToken:")) else {
|
|
119
|
+
throw AccessibilityError.unavailable("frontmostApplicationWithDisplayId: missing")
|
|
120
|
+
}
|
|
121
|
+
guard device.responds(to: NSSelectorFromString("sendAccessibilityRequestAsync:completionQueue:completionHandler:")) else {
|
|
122
|
+
throw AccessibilityError.unavailable("this SimDevice cannot carry accessibility requests")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
self.device = device
|
|
126
|
+
self.translator = shared
|
|
127
|
+
self.elementClass = elementClass
|
|
128
|
+
self.delegate = BridgeDelegate(device: device, timeout: requestTimeout)
|
|
129
|
+
self.token = device.responds(to: NSSelectorFromString("accessibilityPlatformTranslationToken"))
|
|
130
|
+
? device.value(forKey: "accessibilityPlatformTranslationToken")
|
|
131
|
+
: nil
|
|
132
|
+
|
|
133
|
+
shared.setValue(delegate, forKey: "bridgeTokenDelegate")
|
|
134
|
+
if shared.responds(to: NSSelectorFromString("setSupportsDelegateTokens:")) {
|
|
135
|
+
shared.setValue(true, forKey: "supportsDelegateTokens")
|
|
136
|
+
}
|
|
137
|
+
if shared.responds(to: NSSelectorFromString("setAccessibilityEnabled:")) {
|
|
138
|
+
shared.setValue(true, forKey: "accessibilityEnabled")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static let frameworkPath =
|
|
143
|
+
"/System/Library/PrivateFrameworks/AccessibilityPlatformTranslation.framework/AccessibilityPlatformTranslation"
|
|
144
|
+
|
|
145
|
+
// MARK: - Reading
|
|
146
|
+
|
|
147
|
+
/// The frontmost application's tree, flattened depth-first.
|
|
148
|
+
///
|
|
149
|
+
/// An app that is still launching genuinely has no tree yet — the read
|
|
150
|
+
/// returns the application node alone. That is reported as it is, rather
|
|
151
|
+
/// than retried into looking like a populated screen.
|
|
152
|
+
public func tree(budget: TimeInterval = 3) throws -> AXTree {
|
|
153
|
+
guard let app = frontmostApplication() else { throw AccessibilityError.noFrontmostApplication }
|
|
154
|
+
guard let root = elementClass
|
|
155
|
+
.perform(NSSelectorFromString("platformElementWithTranslationObject:"), with: app)?
|
|
156
|
+
.takeUnretainedValue() as? NSObject else {
|
|
157
|
+
throw AccessibilityError.unavailable("the frontmost application did not translate to an element")
|
|
158
|
+
}
|
|
159
|
+
delegate.resetTimeouts()
|
|
160
|
+
var out: [AXNode] = []
|
|
161
|
+
var cut: String?
|
|
162
|
+
let deadline = Date().addingTimeInterval(budget)
|
|
163
|
+
walk(root, depth: 0, into: &out, deadline: deadline, cut: &cut)
|
|
164
|
+
// A guest that missed the deadline answers `emptyResponse`, which makes
|
|
165
|
+
// the subtree below it look genuinely childless. Nothing in the nodes
|
|
166
|
+
// can show that, so the count has to.
|
|
167
|
+
let missed = delegate.timeouts
|
|
168
|
+
if cut == nil, missed > 0 {
|
|
169
|
+
cut = "\(missed) request(s) to the device timed out, so part of the tree is missing"
|
|
170
|
+
}
|
|
171
|
+
return AXTree(nodes: out, truncated: cut)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Read one attribute off the frontmost application.
|
|
175
|
+
///
|
|
176
|
+
/// Constructing the bridge proves only that the classes and selectors are
|
|
177
|
+
/// there. The failure this whole path took two attempts to get past was a
|
|
178
|
+
/// bridge that constructed perfectly and then read nothing, so "available"
|
|
179
|
+
/// has to mean a value came back, not that the symbols exist.
|
|
180
|
+
public func probe() throws {
|
|
181
|
+
guard let app = frontmostApplication() else { throw AccessibilityError.noFrontmostApplication }
|
|
182
|
+
guard let root = elementClass
|
|
183
|
+
.perform(NSSelectorFromString("platformElementWithTranslationObject:"), with: app)?
|
|
184
|
+
.takeUnretainedValue() as? NSObject else {
|
|
185
|
+
throw AccessibilityError.unavailable("the frontmost application did not translate to an element")
|
|
186
|
+
}
|
|
187
|
+
guard attribute(root, "AXRole") is String else {
|
|
188
|
+
throw AccessibilityError.unavailable("the translator answered nil for the application's role")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The pid of the app currently frontmost, or nil when the bridge cannot say.
|
|
193
|
+
public func frontmostPid() -> Int32? {
|
|
194
|
+
guard let app = frontmostApplication() else { return nil }
|
|
195
|
+
return (app.value(forKey: "pid") as? NSNumber)?.int32Value
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private func frontmostApplication() -> NSObject? {
|
|
199
|
+
let sel = NSSelectorFromString("frontmostApplicationWithDisplayId:bridgeDelegateToken:")
|
|
200
|
+
guard let imp = translator.method(for: sel) else { return nil }
|
|
201
|
+
typealias FrontFn = @convention(c) (AnyObject, Selector, UInt32, AnyObject?) -> AnyObject?
|
|
202
|
+
return unsafeBitCast(imp, to: FrontFn.self)(translator, sel, 0, token as AnyObject?) as? NSObject
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func walk(_ element: NSObject, depth: Int, into out: inout [AXNode],
|
|
206
|
+
deadline: Date, cut: inout String?) {
|
|
207
|
+
if depth > Self.maxDepth {
|
|
208
|
+
cut = cut ?? "the tree is deeper than \(Self.maxDepth) levels"
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
if out.count >= Self.maxNodes {
|
|
212
|
+
cut = cut ?? "the tree has more than \(Self.maxNodes) nodes, which is a cycle rather than a screen"
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
if Date() > deadline {
|
|
216
|
+
cut = cut ?? "the read ran out of time"
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
out.append(node(from: element, depth: depth))
|
|
220
|
+
guard let children = attribute(element, "AXChildren") as? [NSObject] else { return }
|
|
221
|
+
for child in children {
|
|
222
|
+
walk(child, depth: depth + 1, into: &out, deadline: deadline, cut: &cut)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Everything scalar about a node, asked for in one go.
|
|
227
|
+
private static let batched = ["AXRole", "AXSubrole", "AXDescription", "AXValue",
|
|
228
|
+
"AXIdentifier", "AXEnabled", "AXSelected", "AXFocused"]
|
|
229
|
+
|
|
230
|
+
private func node(from element: NSObject, depth: Int) -> AXNode {
|
|
231
|
+
// One bridge round trip for eight attributes, not eight.
|
|
232
|
+
//
|
|
233
|
+
// Each `accessibilityAttributeValue:` is a synchronous hop into the
|
|
234
|
+
// guest, so a per-attribute walk costs eleven of them per node. That is
|
|
235
|
+
// 25 ms for fourteen nodes on this machine and invisible — and on a
|
|
236
|
+
// slow one it is the whole read. A CI runner took 28 s over it and
|
|
237
|
+
// returned nothing. `accessibilityMultipleAttributes:` answers the same
|
|
238
|
+
// eight in a single hop: 10 ms for the same fourteen nodes here, and
|
|
239
|
+
// one eighth of the round trips wherever a round trip is what costs.
|
|
240
|
+
let bag = multiple(element, Self.batched)
|
|
241
|
+
func value(_ name: String) -> Any? { bag?[name] ?? attribute(element, name) }
|
|
242
|
+
|
|
243
|
+
let role = value("AXRole") as? String ?? "AXUnknown"
|
|
244
|
+
// The label is the app's own name for the control. AXDescription is
|
|
245
|
+
// where UIKit puts an accessibility label that has no visible title,
|
|
246
|
+
// so it is the fallback rather than a separate field.
|
|
247
|
+
let label = string(element.responds(to: NSSelectorFromString("accessibilityLabel"))
|
|
248
|
+
? element.value(forKey: "accessibilityLabel") : nil)
|
|
249
|
+
?? string(value("AXDescription"))
|
|
250
|
+
return AXNode(
|
|
251
|
+
role: Self.shortRole(role),
|
|
252
|
+
subrole: Self.shortRole(value("AXSubrole") as? String),
|
|
253
|
+
label: label,
|
|
254
|
+
value: string(value("AXValue")),
|
|
255
|
+
identifier: string(value("AXIdentifier")),
|
|
256
|
+
enabled: (value("AXEnabled") as? NSNumber)?.boolValue,
|
|
257
|
+
selected: (value("AXSelected") as? NSNumber)?.boolValue,
|
|
258
|
+
focused: (value("AXFocused") as? NSNumber)?.boolValue,
|
|
259
|
+
frame: frame(of: element),
|
|
260
|
+
depth: depth)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Several attributes in one call, or nil if this element will not batch —
|
|
264
|
+
/// in which case the caller falls back to asking one at a time, because a
|
|
265
|
+
/// slower correct answer beats a missing one.
|
|
266
|
+
private func multiple(_ element: NSObject, _ names: [String]) -> [String: Any]? {
|
|
267
|
+
let sel = NSSelectorFromString("accessibilityMultipleAttributes:")
|
|
268
|
+
guard element.responds(to: sel) else { return nil }
|
|
269
|
+
let answer = element.perform(sel, with: names as NSArray)?.takeUnretainedValue()
|
|
270
|
+
guard let dictionary = answer as? [String: Any] else { return nil }
|
|
271
|
+
return dictionary
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private func attribute(_ element: NSObject, _ name: String) -> Any? {
|
|
275
|
+
let sel = NSSelectorFromString("accessibilityAttributeValue:")
|
|
276
|
+
guard element.responds(to: sel) else { return nil }
|
|
277
|
+
return element.perform(sel, with: name as NSString)?.takeUnretainedValue()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func frame(of element: NSObject) -> CGRect {
|
|
281
|
+
let sel = NSSelectorFromString("accessibilityFrame")
|
|
282
|
+
guard element.responds(to: sel), let imp = element.method(for: sel) else { return .zero }
|
|
283
|
+
typealias RectFn = @convention(c) (AnyObject, Selector) -> CGRect
|
|
284
|
+
return unsafeBitCast(imp, to: RectFn.self)(element, sel)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// A value may be a string, a number, or something with no useful text.
|
|
288
|
+
private func string(_ value: Any?) -> String? {
|
|
289
|
+
switch value {
|
|
290
|
+
case let s as String:
|
|
291
|
+
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
292
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
293
|
+
case let n as NSNumber:
|
|
294
|
+
return n.stringValue
|
|
295
|
+
default:
|
|
296
|
+
return nil
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// `AXButton` is the mac vocabulary; the rest of simframe speaks `Button`.
|
|
301
|
+
private static func shortRole(_ role: String?) -> String? {
|
|
302
|
+
guard let role, !role.isEmpty else { return nil }
|
|
303
|
+
return role.hasPrefix("AX") ? String(role.dropFirst(2)) : role
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private static func shortRole(_ role: String) -> String {
|
|
307
|
+
shortRole(Optional(role)) ?? role
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Carries translator requests to the device and the answers back.
|
|
312
|
+
///
|
|
313
|
+
/// The translator asks for a block, calls it whenever it needs guest data, and
|
|
314
|
+
/// expects the answer synchronously — so this bridges async to sync on a queue
|
|
315
|
+
/// that is never main.
|
|
316
|
+
private final class BridgeDelegate: NSObject {
|
|
317
|
+
private let device: NSObject
|
|
318
|
+
private let timeout: TimeInterval
|
|
319
|
+
private let queue = DispatchQueue(label: "simframe.accessibility.bridge")
|
|
320
|
+
private let counter = NSLock()
|
|
321
|
+
private var timedOut = 0
|
|
322
|
+
|
|
323
|
+
init(device: NSObject, timeout: TimeInterval) {
|
|
324
|
+
self.device = device
|
|
325
|
+
self.timeout = timeout
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// How many requests the device failed to answer since the last reset.
|
|
329
|
+
var timeouts: Int {
|
|
330
|
+
counter.lock(); defer { counter.unlock() }
|
|
331
|
+
return timedOut
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func resetTimeouts() {
|
|
335
|
+
counter.lock(); defer { counter.unlock() }
|
|
336
|
+
timedOut = 0
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fileprivate func recordTimeout() {
|
|
340
|
+
counter.lock(); defer { counter.unlock() }
|
|
341
|
+
timedOut += 1
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@objc(accessibilityTranslationDelegateBridgeCallbackWithToken:)
|
|
345
|
+
func bridgeCallback(token: NSString?) -> Any? {
|
|
346
|
+
let device = self.device, queue = self.queue, timeout = self.timeout
|
|
347
|
+
let block: @convention(block) (Any?) -> Any? = { [self] request in
|
|
348
|
+
guard let request else { return nil }
|
|
349
|
+
let sel = NSSelectorFromString("sendAccessibilityRequestAsync:completionQueue:completionHandler:")
|
|
350
|
+
guard let imp = device.method(for: sel) else { return nil }
|
|
351
|
+
var answer: Any?
|
|
352
|
+
let waited = DispatchSemaphore(value: 0)
|
|
353
|
+
let handler: @convention(block) (Any?) -> Void = { response in
|
|
354
|
+
answer = response
|
|
355
|
+
waited.signal()
|
|
356
|
+
}
|
|
357
|
+
typealias SendFn = @convention(c) (AnyObject, Selector, AnyObject, AnyObject, AnyObject) -> Void
|
|
358
|
+
unsafeBitCast(imp, to: SendFn.self)(device, sel, request as AnyObject, queue, handler as AnyObject)
|
|
359
|
+
if waited.wait(timeout: .now() + timeout) == .timedOut {
|
|
360
|
+
self.recordTimeout()
|
|
361
|
+
// An empty response is what the translator expects when the
|
|
362
|
+
// guest does not answer. Returning nil crashes it.
|
|
363
|
+
return (NSClassFromString("AXPTranslatorResponse") as? NSObject.Type)?
|
|
364
|
+
.perform(NSSelectorFromString("emptyResponse"))?.takeUnretainedValue()
|
|
365
|
+
}
|
|
366
|
+
return answer
|
|
367
|
+
}
|
|
368
|
+
return block
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// Frames arrive in the device's own point space, which is where they
|
|
372
|
+
/// belong: converting them to host screen coordinates would make them
|
|
373
|
+
/// useless for input.
|
|
374
|
+
@objc(accessibilityTranslationConvertPlatformFrameToSystem:withToken:)
|
|
375
|
+
func convertFrame(_ rect: CGRect, token: NSString?) -> CGRect { rect }
|
|
376
|
+
|
|
377
|
+
@objc(accessibilityTranslationRootParentWithToken:)
|
|
378
|
+
func rootParent(token: NSString?) -> Any? { nil }
|
|
379
|
+
}
|