agent-device 0.21.11 → 0.21.12
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/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.11.apk → agent-device-android-ime-helper-0.21.12.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.12.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.11.manifest.json → agent-device-android-ime-helper-0.21.12.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.11.apk → agent-device-android-snapshot-helper-0.21.12.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.12.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.11.manifest.json → agent-device-android-snapshot-helper-0.21.12.manifest.json} +6 -6
- package/apple/fold-helper/Fold.m +97 -0
- package/apple/macos-helper/Package.swift +1 -8
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +0 -2
- package/dist/src/back-mode.js +1 -1
- package/dist/src/cli-help.js +4 -2
- package/dist/src/client-types.d.ts +2 -4
- package/dist/src/device-input-state.d.ts +1 -1
- package/dist/src/device-rotation.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/interactor.js +3 -3
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +2 -2
- package/dist/src/macos-facade.js +1 -1
- package/dist/src/provider-webdriver.js +1 -1
- package/dist/src/registry.js +35 -35
- package/dist/src/runtime4.js +4 -4
- package/dist/src/script.js +1 -1
- package/dist/src/sdk-batch.js +1 -1
- package/dist/src/sdk-selectors.d.ts +12 -1
- package/dist/src/simulator-facade.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.11.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.11.apk.sha256 +0 -1
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +0 -47
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +0 -65
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +0 -225
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +0 -57
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +0 -108
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +0 -30
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +0 -65
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import AgentDeviceMacOSDeviceHub
|
|
2
|
-
import AppKit
|
|
3
|
-
import ApplicationServices
|
|
4
|
-
import Darwin
|
|
5
|
-
import Foundation
|
|
6
|
-
|
|
7
|
-
/// How long sidebar selection and pose-control discovery may each wait before refusing a press.
|
|
8
|
-
private let deviceHubSettleDeadline: TimeInterval = 8
|
|
9
|
-
private let deviceHubPoll: TimeInterval = 0.25
|
|
10
|
-
|
|
11
|
-
struct DeviceHubPoseResponse: Encodable {
|
|
12
|
-
let pose: String
|
|
13
|
-
let control: String
|
|
14
|
-
let windowTitle: String
|
|
15
|
-
/** Whether Device Hub had to be asked to reopen a window before the device could be selected. */
|
|
16
|
-
let reopened: Bool
|
|
17
|
-
/** Whether the window was switched to the device through its sidebar row. */
|
|
18
|
-
let selected: Bool
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func handleDeviceHub(arguments: [String]) throws -> any Encodable {
|
|
22
|
-
guard arguments.first == "pose" else {
|
|
23
|
-
throw HelperError.invalidArgs("device-hub requires pose")
|
|
24
|
-
}
|
|
25
|
-
let rest = Array(arguments.dropFirst())
|
|
26
|
-
guard let udid = helperOptionValue(arguments: rest, name: "--udid")?
|
|
27
|
-
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
28
|
-
!udid.isEmpty
|
|
29
|
-
else {
|
|
30
|
-
throw HelperError.invalidArgs("device-hub pose requires --udid <udid>")
|
|
31
|
-
}
|
|
32
|
-
guard let deviceName = helperOptionValue(arguments: rest, name: "--device-name")?
|
|
33
|
-
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
34
|
-
!deviceName.isEmpty
|
|
35
|
-
else {
|
|
36
|
-
throw HelperError.invalidArgs("device-hub pose requires --device-name <name>")
|
|
37
|
-
}
|
|
38
|
-
guard let pose = helperOptionValue(arguments: rest, name: "--pose").flatMap(DeviceHubPose.init(argument:))
|
|
39
|
-
else {
|
|
40
|
-
throw HelperError.invalidArgs("device-hub pose requires --pose <closed|book|open>")
|
|
41
|
-
}
|
|
42
|
-
let control = pose.controlDescription
|
|
43
|
-
guard AXIsProcessTrusted() else {
|
|
44
|
-
throw HelperError.commandFailed(
|
|
45
|
-
"fold needs Accessibility permission to press the Device Hub pose control",
|
|
46
|
-
details: ["reason": "accessibility-permission", "permission": "accessibility"]
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
let discovery = try discoverDeviceHubWindow(udid: udid, deviceName: deviceName)
|
|
50
|
-
let window = discovery.window
|
|
51
|
-
defer {
|
|
52
|
-
if discovery.revealedSidebar {
|
|
53
|
-
_ = setDeviceHubSidebar(visible: false, window: window,
|
|
54
|
-
deadline: ProcessInfo.processInfo.systemUptime + 0.5)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
let selected = try selectDevice(row: discovery.row, deviceName: deviceName, in: window)
|
|
58
|
-
let windowTitle = stringAttribute(window, attribute: kAXTitleAttribute as String) ?? ""
|
|
59
|
-
guard let button = awaitPoseButton(in: window, description: control) else {
|
|
60
|
-
throw HelperError.commandFailed(
|
|
61
|
-
"Device Hub shows no \(control) pose control for \(deviceName)",
|
|
62
|
-
details: [
|
|
63
|
-
"reason": "device-hub-pose-control-missing",
|
|
64
|
-
"windowTitle": windowTitle,
|
|
65
|
-
"control": control,
|
|
66
|
-
]
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
let status = AXUIElementPerformAction(button, kAXPressAction as CFString)
|
|
70
|
-
guard status == .success else {
|
|
71
|
-
throw HelperError.commandFailed(
|
|
72
|
-
"Device Hub refused the \(control) pose press",
|
|
73
|
-
details: ["reason": "device-hub-press-failed", "status": "\(status.rawValue)"]
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
return SuccessEnvelope(
|
|
77
|
-
data: DeviceHubPoseResponse(
|
|
78
|
-
pose: pose.rawValue,
|
|
79
|
-
control: control,
|
|
80
|
-
windowTitle: windowTitle,
|
|
81
|
-
reopened: discovery.reopened,
|
|
82
|
-
selected: selected
|
|
83
|
-
)
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private func windowShows(deviceName: String, _ window: AXUIElement) -> Bool {
|
|
88
|
-
return deviceHubWindowShows(
|
|
89
|
-
deviceName: deviceName,
|
|
90
|
-
title: stringAttribute(window, attribute: kAXTitleAttribute as String)
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/// `kAEReopenApplication` is what the Dock sends when an app with no open windows is clicked, and
|
|
95
|
-
/// it is the one event Device Hub answers by restoring the device window. Sent to the process
|
|
96
|
-
/// directly, because LaunchServices cannot address a trampolined app by bundle identifier.
|
|
97
|
-
func sendDeviceHubReopenEvent(to pid: pid_t, timeout: TimeInterval) throws {
|
|
98
|
-
let target = NSAppleEventDescriptor(processIdentifier: pid)
|
|
99
|
-
let event = NSAppleEventDescriptor(
|
|
100
|
-
eventClass: AEEventClass(kCoreEventClass),
|
|
101
|
-
eventID: AEEventID(kAEReopenApplication),
|
|
102
|
-
targetDescriptor: target,
|
|
103
|
-
returnID: AEReturnID(kAutoGenerateReturnID),
|
|
104
|
-
transactionID: AETransactionID(kAnyTransactionID)
|
|
105
|
-
)
|
|
106
|
-
do {
|
|
107
|
-
_ = try event.sendEvent(options: [.noReply], timeout: min(5, max(0.001, timeout)))
|
|
108
|
-
} catch {
|
|
109
|
-
let code = (error as NSError).code
|
|
110
|
-
throw HelperError.commandFailed(
|
|
111
|
-
"fold could not ask Device Hub to reopen its device window",
|
|
112
|
-
details: [
|
|
113
|
-
"reason": code == -1743 ? "automation-permission" : "device-hub-reopen-failed",
|
|
114
|
-
"error": String(describing: error),
|
|
115
|
-
]
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/// Switches the window to the device through its sidebar row, whose accessibility identifier is
|
|
121
|
-
/// `TableRow.Device.<udid>` — the one place Device Hub exposes a device identity that two
|
|
122
|
-
/// simulators sharing a name cannot confuse. A window already titled with the device still gets
|
|
123
|
-
/// the selection, because the title alone cannot tell such twins apart.
|
|
124
|
-
private func selectDevice(
|
|
125
|
-
row: AXUIElement,
|
|
126
|
-
deviceName: String,
|
|
127
|
-
in window: AXUIElement
|
|
128
|
-
) throws -> Bool {
|
|
129
|
-
let deadline = ProcessInfo.processInfo.systemUptime + deviceHubSettleDeadline
|
|
130
|
-
AXUIElementSetMessagingTimeout(row, 0.25)
|
|
131
|
-
AXUIElementSetMessagingTimeout(window, 0.25)
|
|
132
|
-
let status = AXUIElementSetAttributeValue(row, kAXSelectedAttribute as CFString, kCFBooleanTrue)
|
|
133
|
-
guard status == .success || status == .cannotComplete else {
|
|
134
|
-
throw HelperError.commandFailed(
|
|
135
|
-
"Device Hub refused to select \(deviceName) in its sidebar",
|
|
136
|
-
details: ["reason": "device-hub-select-failed", "status": "\(status.rawValue)"]
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
while ProcessInfo.processInfo.systemUptime < deadline {
|
|
140
|
-
var selected: CFTypeRef?
|
|
141
|
-
if AXUIElementCopyAttributeValue(row, kAXSelectedAttribute as CFString, &selected) == .success,
|
|
142
|
-
(selected as? Bool) == true, windowShows(deviceName: deviceName, window) { return true }
|
|
143
|
-
Thread.sleep(forTimeInterval: deviceHubPoll)
|
|
144
|
-
}
|
|
145
|
-
throw HelperError.commandFailed(
|
|
146
|
-
"Device Hub did not confirm selection of \(deviceName)",
|
|
147
|
-
details: ["reason": "device-hub-select-unconfirmed", "status": "\(status.rawValue)"]
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
func deviceHubDeviceRow(udid: String, in window: AXUIElement, deadline: TimeInterval) -> AXUIElement? {
|
|
152
|
-
guard let label = findDeviceHubElement(root: window, depth: 0, deadline: deadline, where: {
|
|
153
|
-
stringAttribute($0, attribute: "AXIdentifier") == deviceHubDeviceRowIdentifier(udid: udid)
|
|
154
|
-
}) else {
|
|
155
|
-
return nil
|
|
156
|
-
}
|
|
157
|
-
var current: AXUIElement? = label
|
|
158
|
-
while let element = current, ProcessInfo.processInfo.systemUptime < deadline {
|
|
159
|
-
AXUIElementSetMessagingTimeout(element, Float(max(0.001, min(0.25, deadline - ProcessInfo.processInfo.systemUptime))))
|
|
160
|
-
if stringAttribute(element, attribute: kAXRoleAttribute as String) == "AXRow" {
|
|
161
|
-
return element
|
|
162
|
-
}
|
|
163
|
-
current = elementAttribute(element, attribute: kAXParentAttribute as String)
|
|
164
|
-
}
|
|
165
|
-
return nil
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/// The action bar is rebuilt for the device the window shows, so right after a sidebar selection
|
|
169
|
-
/// the window already carries the new title while the pose controls are still being laid out.
|
|
170
|
-
/// The controls are therefore awaited, not looked up once.
|
|
171
|
-
private func awaitPoseButton(in window: AXUIElement, description: String) -> AXUIElement? {
|
|
172
|
-
let deadline = ProcessInfo.processInfo.systemUptime + deviceHubSettleDeadline
|
|
173
|
-
while true {
|
|
174
|
-
if let button = poseButton(in: window, description: description, deadline: deadline) {
|
|
175
|
-
return button
|
|
176
|
-
}
|
|
177
|
-
guard ProcessInfo.processInfo.systemUptime < deadline else { return nil }
|
|
178
|
-
Thread.sleep(forTimeInterval: deviceHubPoll)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/// The pose controls sit in the window's action bar as `AXButton`s described by their preset
|
|
183
|
-
/// name; the simulated screen inside the same window is an iOS content group whose own buttons
|
|
184
|
-
/// carry app labels, never these three.
|
|
185
|
-
private func poseButton(in window: AXUIElement, description: String, deadline: TimeInterval) -> AXUIElement? {
|
|
186
|
-
return findDeviceHubElement(root: window, depth: 0, deadline: deadline) {
|
|
187
|
-
stringAttribute($0, attribute: kAXRoleAttribute as String) == "AXButton"
|
|
188
|
-
&& stringAttribute($0, attribute: kAXDescriptionAttribute as String) == description
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
func findDeviceHubElement(
|
|
193
|
-
root: AXUIElement,
|
|
194
|
-
depth: Int,
|
|
195
|
-
deadline: TimeInterval,
|
|
196
|
-
where matches: (AXUIElement) -> Bool
|
|
197
|
-
) -> AXUIElement? {
|
|
198
|
-
let remaining = deadline - ProcessInfo.processInfo.systemUptime
|
|
199
|
-
if depth > 14 || remaining <= 0 {
|
|
200
|
-
return nil
|
|
201
|
-
}
|
|
202
|
-
AXUIElementSetMessagingTimeout(root, Float(min(0.25, remaining)))
|
|
203
|
-
for child in children(of: root) {
|
|
204
|
-
let remaining = deadline - ProcessInfo.processInfo.systemUptime
|
|
205
|
-
guard remaining > 0 else { return nil }
|
|
206
|
-
AXUIElementSetMessagingTimeout(child, Float(min(0.25, remaining)))
|
|
207
|
-
if matches(child) {
|
|
208
|
-
return child
|
|
209
|
-
}
|
|
210
|
-
if stringAttribute(child, attribute: kAXSubroleAttribute as String) == "iOSContentGroup" {
|
|
211
|
-
continue
|
|
212
|
-
}
|
|
213
|
-
if let nested = findDeviceHubElement(root: child, depth: depth + 1, deadline: deadline, where: matches) {
|
|
214
|
-
return nested
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return nil
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
func helperOptionValue(arguments: [String], name: String) -> String? {
|
|
221
|
-
guard let index = arguments.firstIndex(of: name), arguments.indices.contains(index + 1) else {
|
|
222
|
-
return nil
|
|
223
|
-
}
|
|
224
|
-
return arguments[index + 1]
|
|
225
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import AgentDeviceMacOSDeviceHub
|
|
2
|
-
import ApplicationServices
|
|
3
|
-
import Foundation
|
|
4
|
-
|
|
5
|
-
func identifyDeviceHubRow(
|
|
6
|
-
udid: String,
|
|
7
|
-
candidate: DeviceHubWindowCandidate,
|
|
8
|
-
deadline: TimeInterval
|
|
9
|
-
) -> DeviceHubRowLookup {
|
|
10
|
-
let now = ProcessInfo.processInfo.systemUptime
|
|
11
|
-
let visibleRowDeadline = now + max(0, deadline - now) / 3
|
|
12
|
-
if let row = deviceHubDeviceRow(udid: udid, in: candidate.window, deadline: visibleRowDeadline) {
|
|
13
|
-
return .identified(row, restoreSidebar: false)
|
|
14
|
-
}
|
|
15
|
-
let searchDeadline = deadline - 0.25
|
|
16
|
-
guard let button = deviceHubSidebarButton(visible: true, window: candidate.window, deadline: searchDeadline) else {
|
|
17
|
-
return ProcessInfo.processInfo.systemUptime >= searchDeadline ? .timedOut : .sidebarUnavailable
|
|
18
|
-
}
|
|
19
|
-
var transferCleanup = false
|
|
20
|
-
defer {
|
|
21
|
-
if !transferCleanup {
|
|
22
|
-
_ = setDeviceHubSidebar(visible: false, window: candidate.window, deadline: deadline)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
let status = pressDeviceHubSidebarButton(button, deadline: searchDeadline)
|
|
26
|
-
guard status == .success || status == .cannotComplete else { return .unconfirmed }
|
|
27
|
-
repeat {
|
|
28
|
-
if let row = deviceHubDeviceRow(udid: udid, in: candidate.window, deadline: searchDeadline) {
|
|
29
|
-
transferCleanup = true
|
|
30
|
-
return .identified(row, restoreSidebar: true)
|
|
31
|
-
}
|
|
32
|
-
let remaining = searchDeadline - ProcessInfo.processInfo.systemUptime
|
|
33
|
-
if remaining <= 0 { return .timedOut }
|
|
34
|
-
Thread.sleep(forTimeInterval: min(0.05, remaining))
|
|
35
|
-
} while ProcessInfo.processInfo.systemUptime < searchDeadline
|
|
36
|
-
return .timedOut
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func setDeviceHubSidebar(visible: Bool, window: AXUIElement, deadline: TimeInterval) -> Bool {
|
|
40
|
-
guard let button = deviceHubSidebarButton(visible: visible, window: window, deadline: deadline) else { return false }
|
|
41
|
-
return pressDeviceHubSidebarButton(button, deadline: deadline) == .success
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private func deviceHubSidebarButton(visible: Bool, window: AXUIElement, deadline: TimeInterval) -> AXUIElement? {
|
|
45
|
-
let description = visible ? "Show Sidebar" : "Hide Sidebar"
|
|
46
|
-
return findDeviceHubElement(root: window, depth: 0, deadline: deadline) {
|
|
47
|
-
stringAttribute($0, attribute: kAXRoleAttribute as String) == "AXButton"
|
|
48
|
-
&& stringAttribute($0, attribute: kAXDescriptionAttribute as String) == description
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private func pressDeviceHubSidebarButton(_ button: AXUIElement, deadline: TimeInterval) -> AXError {
|
|
53
|
-
let remaining = deadline - ProcessInfo.processInfo.systemUptime
|
|
54
|
-
guard remaining > 0 else { return .cannotComplete }
|
|
55
|
-
AXUIElementSetMessagingTimeout(button, Float(min(0.1, remaining)))
|
|
56
|
-
return AXUIElementPerformAction(button, kAXPressAction as CFString)
|
|
57
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import AgentDeviceMacOSDeviceHub
|
|
2
|
-
import AppKit
|
|
3
|
-
import ApplicationServices
|
|
4
|
-
import Darwin
|
|
5
|
-
|
|
6
|
-
struct DiscoveredDeviceHubWindow {
|
|
7
|
-
let window: AXUIElement
|
|
8
|
-
let reopened: Bool
|
|
9
|
-
let row: AXUIElement
|
|
10
|
-
let revealedSidebar: Bool
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
func discoverDeviceHubWindow(udid: String, deviceName: String) throws -> DiscoveredDeviceHubWindow {
|
|
14
|
-
let deadline = ProcessInfo.processInfo.systemUptime + 8
|
|
15
|
-
var reopened = Set<pid_t>()
|
|
16
|
-
var reopenFailures: [pid_t: String] = [:]
|
|
17
|
-
var inventory = DeviceHubWindowInventory()
|
|
18
|
-
var processIDs: [pid_t] = []
|
|
19
|
-
repeat {
|
|
20
|
-
processIDs = deviceHubProcessIdentifiers()
|
|
21
|
-
if processIDs.isEmpty {
|
|
22
|
-
throw HelperError.commandFailed(
|
|
23
|
-
"Xcode Device Hub is not running",
|
|
24
|
-
details: ["reason": "device-hub-not-running", "bundleId": "com.apple.dt.Devices"]
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
inventory = DeviceHubWindowInventory()
|
|
28
|
-
for pid in processIDs {
|
|
29
|
-
let remaining = deadline - ProcessInfo.processInfo.systemUptime
|
|
30
|
-
guard remaining > 0 else { inventory.expireBudget(); break }
|
|
31
|
-
let app = AXUIElementCreateApplication(pid)
|
|
32
|
-
AXUIElementSetMessagingTimeout(app, Float(min(0.5, remaining)))
|
|
33
|
-
var value: CFTypeRef?
|
|
34
|
-
let status = AXUIElementCopyAttributeValue(app, kAXWindowsAttribute as CFString, &value)
|
|
35
|
-
if status == .success, let windows = value as? [AXUIElement] {
|
|
36
|
-
for window in windows { AXUIElementSetMessagingTimeout(window, 0.5) }
|
|
37
|
-
inventory.record(processID: pid, read: .available(windows))
|
|
38
|
-
} else {
|
|
39
|
-
inventory.record(processID: pid, read: .failed(
|
|
40
|
-
status == .success ? AXError.illegalArgument.rawValue : status.rawValue
|
|
41
|
-
))
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
for (index, candidate) in inventory.candidates.enumerated() {
|
|
45
|
-
let now = ProcessInfo.processInfo.systemUptime
|
|
46
|
-
guard now < deadline else { inventory.expireBudget(); break }
|
|
47
|
-
let candidateDeadline = now + (deadline - now) / Double(inventory.candidates.count - index) / 2
|
|
48
|
-
let outcome = identifyDeviceHubRow(udid: udid, candidate: candidate, deadline: candidateDeadline)
|
|
49
|
-
inventory.record(candidate: index, outcome: outcome)
|
|
50
|
-
if case .identified(let row, let restoreSidebar) = outcome {
|
|
51
|
-
return DiscoveredDeviceHubWindow(window: candidate.window,
|
|
52
|
-
reopened: reopened.contains(candidate.processID), row: row, revealedSidebar: restoreSidebar)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
let attempted = reopened.union(reopenFailures.keys)
|
|
56
|
-
for pid in inventory.processesToReopen(attempted: attempted) {
|
|
57
|
-
guard ProcessInfo.processInfo.systemUptime < deadline else { inventory.expireBudget(); break }
|
|
58
|
-
do {
|
|
59
|
-
try sendDeviceHubReopenEvent(to: pid, timeout: deadline - ProcessInfo.processInfo.systemUptime)
|
|
60
|
-
reopened.insert(pid)
|
|
61
|
-
} catch let error as HelperError {
|
|
62
|
-
guard case .commandFailed(_, let details) = error else { throw error }
|
|
63
|
-
reopenFailures[pid] = details["reason"] ?? "device-hub-reopen-failed"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
let remaining = deadline - ProcessInfo.processInfo.systemUptime
|
|
67
|
-
if remaining <= 0 { inventory.expireBudget(); break }
|
|
68
|
-
Thread.sleep(forTimeInterval: min(0.25, remaining))
|
|
69
|
-
} while ProcessInfo.processInfo.systemUptime < deadline
|
|
70
|
-
|
|
71
|
-
let statuses = inventory.failures.sorted { $0.key < $1.key }
|
|
72
|
-
.map { "\($0.key):\($0.value)" }.joined(separator: ",")
|
|
73
|
-
throw HelperError.commandFailed(
|
|
74
|
-
"Could not confirm a Device Hub window for the requested device",
|
|
75
|
-
details: [
|
|
76
|
-
"reason": inventory.failureReason(reopenFailures: reopenFailures),
|
|
77
|
-
"reopenFailures": reopenFailures.sorted { $0.key < $1.key }.map { "\($0.key):\($0.value)" }.joined(separator: ","),
|
|
78
|
-
"candidateOutcomes": inventory.candidateOutcomes.joined(separator: ","),
|
|
79
|
-
"udid": udid,
|
|
80
|
-
"deviceName": deviceName,
|
|
81
|
-
"processIDs": processIDs.map(String.init).joined(separator: ","),
|
|
82
|
-
"axWindowReadStatuses": statuses,
|
|
83
|
-
"hostDisplayIDs": deviceHubHostDisplays().map { String($0) }.joined(separator: ","),
|
|
84
|
-
]
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private func deviceHubProcessIdentifiers() -> [pid_t] {
|
|
89
|
-
let byteCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
|
|
90
|
-
guard byteCount > 0 else { return [] }
|
|
91
|
-
var pids = [pid_t](repeating: 0, count: Int(byteCount) / MemoryLayout<pid_t>.size + 64)
|
|
92
|
-
let filled = proc_listpids(UInt32(PROC_ALL_PIDS), 0, &pids, Int32(pids.count * MemoryLayout<pid_t>.size))
|
|
93
|
-
guard filled > 0 else { return [] }
|
|
94
|
-
return pids.prefix(Int(filled) / MemoryLayout<pid_t>.size).filter { pid in
|
|
95
|
-
guard pid > 0 else { return false }
|
|
96
|
-
var path = [CChar](repeating: 0, count: 4096)
|
|
97
|
-
guard proc_pidpath(pid, &path, UInt32(path.count)) > 0 else { return false }
|
|
98
|
-
return String(cString: path).hasSuffix(deviceHubExecutableSuffix)
|
|
99
|
-
}.sorted()
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private func deviceHubHostDisplays() -> [CGDirectDisplayID] {
|
|
103
|
-
var count: UInt32 = 0
|
|
104
|
-
guard CGGetActiveDisplayList(0, nil, &count) == .success else { return [] }
|
|
105
|
-
var displays = [CGDirectDisplayID](repeating: 0, count: Int(count))
|
|
106
|
-
guard CGGetActiveDisplayList(count, &displays, &count) == .success else { return [] }
|
|
107
|
-
return Array(displays.prefix(Int(count)))
|
|
108
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
|
|
3
|
-
@testable import AgentDeviceMacOSDeviceHub
|
|
4
|
-
|
|
5
|
-
final class DeviceHubPoseTests: XCTestCase {
|
|
6
|
-
func testEveryPoseNamesTheActionBarControlDeviceHubDescribes() {
|
|
7
|
-
XCTAssertEqual(DeviceHubPose(argument: "closed")?.controlDescription, "Closed")
|
|
8
|
-
XCTAssertEqual(DeviceHubPose(argument: "book")?.controlDescription, "Book")
|
|
9
|
-
XCTAssertEqual(DeviceHubPose(argument: "OPEN")?.controlDescription, "Open")
|
|
10
|
-
XCTAssertNil(DeviceHubPose(argument: "half-open"), "the helper takes Device Hub's names, not the CLI's")
|
|
11
|
-
XCTAssertEqual(Set(DeviceHubPose.allCases.map(\.controlDescription)), ["Closed", "Book", "Open"])
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Device Hub titles the window `<name> – iOS 27.1`; a simulator whose name merely starts
|
|
15
|
-
// with another's must not claim that window.
|
|
16
|
-
func testWindowTitleMatchesTheDeviceNameAsAPrefixWord() {
|
|
17
|
-
XCTAssertTrue(deviceHubWindowShows(deviceName: "iPhone Duo", title: "iPhone Duo – iOS 27.1"))
|
|
18
|
-
XCTAssertTrue(deviceHubWindowShows(deviceName: "iPhone Duo", title: "iPhone Duo"))
|
|
19
|
-
XCTAssertFalse(deviceHubWindowShows(deviceName: "iPhone Duo", title: "iPhone Duo Lab – iOS 27.1"))
|
|
20
|
-
XCTAssertFalse(deviceHubWindowShows(deviceName: "iPhone Duo", title: "bench-golden-v1 – iOS 27.0"))
|
|
21
|
-
XCTAssertFalse(deviceHubWindowShows(deviceName: "iPhone Duo", title: nil))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func testSidebarRowIsKeyedByUdid() {
|
|
25
|
-
XCTAssertEqual(
|
|
26
|
-
deviceHubDeviceRowIdentifier(udid: "4F879835-4AB3-4046-B033-5AB769209DD4"),
|
|
27
|
-
"TableRow.Device.4F879835-4AB3-4046-B033-5AB769209DD4"
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
}
|
package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import ApplicationServices
|
|
2
|
-
import XCTest
|
|
3
|
-
@testable import AgentDeviceMacOSDeviceHub
|
|
4
|
-
|
|
5
|
-
final class DeviceHubWindowInventoryTests: XCTestCase {
|
|
6
|
-
func testLaterWindowKeepsItsProcessAndDoesNotPreventEmptyProcessReopen() {
|
|
7
|
-
var inventory = DeviceHubWindowInventory()
|
|
8
|
-
let window = AXUIElementCreateApplication(20)
|
|
9
|
-
inventory.record(processID: 10, read: .available([]))
|
|
10
|
-
inventory.record(processID: 20, read: .available([window]))
|
|
11
|
-
XCTAssertEqual(inventory.candidates.map(\.processID), [20])
|
|
12
|
-
XCTAssertTrue(CFEqual(inventory.candidates[0].window, window))
|
|
13
|
-
inventory.record(candidate: 0, outcome: .sidebarUnavailable)
|
|
14
|
-
XCTAssertEqual(inventory.processesToReopen(attempted: []), [10])
|
|
15
|
-
XCTAssertEqual(inventory.failureReason(), "device-hub-identity-unconfirmed")
|
|
16
|
-
XCTAssertEqual(inventory.candidateOutcomes, ["20:0:sidebar-unavailable"])
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func testFailedReadsAreNotReopenedOrMisreportedAsMissingWindows() {
|
|
20
|
-
var inventory = DeviceHubWindowInventory()
|
|
21
|
-
inventory.record(processID: 10, read: .failed(-25204))
|
|
22
|
-
inventory.record(processID: 20, read: .available([]))
|
|
23
|
-
XCTAssertEqual(inventory.processesToReopen(attempted: []), [20])
|
|
24
|
-
XCTAssertEqual(inventory.failures, [10: -25204])
|
|
25
|
-
XCTAssertEqual(inventory.failureReason(), "device-hub-window-read-failed")
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
func testSuccessfulEmptyReadIsMissingAndReopenIsAttemptedOnlyOnce() {
|
|
29
|
-
var inventory = DeviceHubWindowInventory()
|
|
30
|
-
inventory.record(processID: 10, read: .available([]))
|
|
31
|
-
XCTAssertEqual(inventory.failureReason(), "device-hub-window-missing")
|
|
32
|
-
XCTAssertEqual(inventory.processesToReopen(attempted: []), [10])
|
|
33
|
-
XCTAssertEqual(inventory.processesToReopen(attempted: [10]), [])
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func testSharedBudgetExhaustionStopsReopenWithoutClaimingMissingDevice() {
|
|
37
|
-
var inventory = DeviceHubWindowInventory()
|
|
38
|
-
inventory.record(processID: 10, read: .available([]))
|
|
39
|
-
inventory.expireBudget()
|
|
40
|
-
XCTAssertEqual(inventory.processesToReopen(attempted: []), [])
|
|
41
|
-
XCTAssertEqual(inventory.failureReason(), "device-hub-discovery-timeout")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
func testCandidateOutcomesRetainUncertaintyAndOwningProcess() {
|
|
45
|
-
var inventory = DeviceHubWindowInventory()
|
|
46
|
-
inventory.record(processID: 10, read: .available([AXUIElementCreateApplication(10)]))
|
|
47
|
-
inventory.record(processID: 20, read: .available([AXUIElementCreateApplication(20)]))
|
|
48
|
-
inventory.record(candidate: 0, outcome: .timedOut)
|
|
49
|
-
inventory.record(candidate: 1, outcome: .unconfirmed)
|
|
50
|
-
XCTAssertEqual(inventory.candidateOutcomes, ["10:0:timed-out", "20:1:row-unconfirmed"])
|
|
51
|
-
XCTAssertEqual(inventory.failureReason(), "device-hub-identity-unconfirmed")
|
|
52
|
-
}
|
|
53
|
-
func testUnrelatedReopenFailureDoesNotMaskCandidateUncertainty() {
|
|
54
|
-
var inventory = DeviceHubWindowInventory()
|
|
55
|
-
inventory.record(processID: 10, read: .available([]))
|
|
56
|
-
let failures: [Int32: String] = [10: "automation-permission"]
|
|
57
|
-
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "automation-permission")
|
|
58
|
-
inventory.record(processID: 20, read: .available([AXUIElementCreateApplication(20)]))
|
|
59
|
-
inventory.record(candidate: 0, outcome: .timedOut)
|
|
60
|
-
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "device-hub-identity-unconfirmed")
|
|
61
|
-
inventory.expireBudget()
|
|
62
|
-
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "device-hub-discovery-timeout")
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
}
|