agent-device 0.21.7 → 0.21.9
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.7.apk → agent-device-android-ime-helper-0.21.9.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.9.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.7.manifest.json → agent-device-android-ime-helper-0.21.9.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.7.apk → agent-device-android-snapshot-helper-0.21.9.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.9.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.7.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
- package/apple/macos-helper/Package.swift +16 -1
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +47 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +225 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +66 -13
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickDelivery.swift +116 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickSchedule.swift +61 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +30 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSInputTests/MouseClickScheduleTests.swift +58 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +175 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +42 -15
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +8 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +45 -22
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +39 -734
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +20 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +63 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +13 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +25 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +5 -562
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +566 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +5 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +594 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +35 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +160 -0
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +3 -0
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-frames.swift +203 -0
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +5 -0
- package/dist/src/absence-observation-errors.js +1 -1
- package/dist/src/adapter.js +1 -1
- package/dist/src/agent-browser-lifecycle.js +1 -1
- package/dist/src/agent-browser-process-record.js +1 -1
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +1 -1
- package/dist/src/android.js +1 -1
- package/dist/src/app-launch.js +1 -1
- package/dist/src/app-log-runtime2.js +3 -3
- package/dist/src/app-resolution.js +2 -2
- package/dist/src/apple-runner-platform.js +1 -1
- package/dist/src/application-lifecycle-interaction.js +1 -1
- package/dist/src/artifact-paths.js +1 -0
- package/dist/src/back-mode.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +15 -11
- package/dist/src/cli.js +4 -4
- package/dist/src/client-types.d.ts +51 -1
- package/dist/src/command-tools.js +3 -3
- package/dist/src/config.js +1 -1
- package/dist/src/connection-runtime.js +1 -1
- package/dist/src/device-claim-inspection.js +1 -1
- package/dist/src/device-claim-rule.js +2 -2
- package/dist/src/device-input-state.d.ts +1 -1
- package/dist/src/device-rotation.js +1 -1
- package/dist/src/device-selection-resolver.js +1 -1
- package/dist/src/device-session.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +1 -1
- package/dist/src/devicectl.js +1 -1
- package/dist/src/dispatch-resolve.js +1 -1
- package/dist/src/display-inventory.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/durable-resource.js +1 -1
- package/dist/src/exec.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/fold-runtime.js +1 -0
- package/dist/src/gesture-admission.js +1 -1
- package/dist/src/gesture-runtime.js +1 -1
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/host.js +1 -0
- package/dist/src/interaction-positionals.js +1 -1
- package/dist/src/interaction-ref-policy.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interaction2.js +1 -1
- package/dist/src/interactor-operation-binding.js +1 -0
- package/dist/src/interactor-operation-catalog.js +1 -1
- package/dist/src/interactor.js +4 -5
- package/dist/src/interactor2.js +1 -1
- package/dist/src/interactors.js +1 -1
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +2 -2
- package/dist/src/internal/png-worker.d.ts +5 -0
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/inventory2.js +1 -1
- package/dist/src/ios-snapshot-acquisition.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/local-interactor-operation-set.js +1 -1
- package/dist/src/location-coordinates.js +1 -1
- package/dist/src/macos-facade.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +17 -16
- package/dist/src/mobile-snapshot-semantics.js +1 -1
- package/dist/src/mp4-duration.js +1 -0
- package/dist/src/multitouch-support.js +1 -1
- package/dist/src/owner-identity.js +1 -1
- package/dist/src/parameterized-recorded-fill.js +1 -1
- package/dist/src/perf-facade.js +2 -2
- package/dist/src/perf-runtime-plan.js +1 -1
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-request-scope.js +1 -1
- package/dist/src/platform-runtime-managed-owner.js +1 -1
- package/dist/src/platform-runtime-open-target.js +1 -1
- package/dist/src/platform-runtime-operation-host.js +2 -2
- package/dist/src/platform-runtime-runtime-hints.js +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
- package/dist/src/platform-runtime-unavailable.js +1 -1
- package/dist/src/platform-runtime.js +1 -1
- package/dist/src/platform-runtime2.js +1 -1
- package/dist/src/png-crop-bytes.js +1 -1
- package/dist/src/png-encode.js +1 -0
- package/dist/src/png-size.js +1 -0
- package/dist/src/png-worker-client.js +1 -1
- package/dist/src/provider-webdriver.js +1 -1
- package/dist/src/quality-warnings.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-runtime.js +1 -1
- package/dist/src/recording-export-quality.js +1 -1
- package/dist/src/recording-scope.js +1 -0
- package/dist/src/recording.js +3 -0
- package/dist/src/registry.js +35 -35
- package/dist/src/replay.js +1 -1
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/result-serialization.js +1 -1
- package/dist/src/runner-cache-metadata.js +2 -2
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-contract.js +3 -0
- package/dist/src/runner-dev-tools-security.js +1 -0
- package/dist/src/runner-device-readiness.js +1 -0
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-presentation.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime.js +2 -2
- package/dist/src/runtime10.js +2 -1
- package/dist/src/runtime2.js +4 -4
- package/dist/src/runtime4.js +5 -5
- package/dist/src/runtime5.js +1 -1
- package/dist/src/runtime6.js +1 -1
- package/dist/src/runtime7.js +1 -1
- package/dist/src/runtime8.js +1 -1
- package/dist/src/runtime9.js +1 -1
- package/dist/src/screen-recording-admission-ledger.js +1 -1
- package/dist/src/screen-recording-resource-recovery.js +1 -1
- package/dist/src/screen-recording-transport.js +1 -1
- package/dist/src/screenshot-density.js +1 -1
- package/dist/src/screenshot-image.js +1 -0
- package/dist/src/screenshot-overlay-draw.js +1 -0
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-gesture.js +1 -1
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +7 -5
- package/dist/src/sdk-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-selectors.d.ts +58 -6
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/semantic-index.js +1 -1
- package/dist/src/session-routing.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session-store.js +3 -3
- package/dist/src/session2.js +3 -3
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-boot.js +1 -1
- package/dist/src/simulator-facade.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-runtime-binding.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -1
- package/dist/src/snapshot-state.js +1 -1
- package/dist/src/snapshot4.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/src8.js +1 -1
- package/dist/src/stop-sequence.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/touch-runtime.js +1 -1
- package/dist/src/tree.js +1 -1
- package/dist/src/type-text-runtime.js +1 -1
- package/dist/src/video.js +2 -2
- package/dist/src/web2.js +1 -1
- package/package.json +3 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.7.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.7.apk.sha256 +0 -1
- package/dist/src/png-transcode.js +0 -1
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abf226740c34ca523708302fae272010cc2616b651b6073737c50f909f672b38 agent-device-android-ime-helper-0.21.9.apk
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-ime-helper",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"assetName": "agent-device-android-ime-helper-0.21.
|
|
5
|
-
"sha256": "
|
|
3
|
+
"version": "0.21.9",
|
|
4
|
+
"assetName": "agent-device-android-ime-helper-0.21.9.apk",
|
|
5
|
+
"sha256": "abf226740c34ca523708302fae272010cc2616b651b6073737c50f909f672b38",
|
|
6
6
|
"packageName": "com.callstack.agentdevice.imehelper",
|
|
7
|
-
"versionCode":
|
|
7
|
+
"versionCode": 21009,
|
|
8
8
|
"serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
|
|
9
9
|
"broadcastProtocol": "android-ime-helper-v1"
|
|
10
10
|
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c73eed5a190579623e2c6b5baa2b3f7e760a5d41d4630e05d970826007d4ae55 agent-device-android-snapshot-helper-0.21.9.apk
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-snapshot-helper",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"releaseTag": "v0.21.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.21.
|
|
3
|
+
"version": "0.21.9",
|
|
4
|
+
"releaseTag": "v0.21.9",
|
|
5
|
+
"assetName": "agent-device-android-snapshot-helper-0.21.9.apk",
|
|
6
6
|
"apkUrl": null,
|
|
7
|
-
"sha256": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.21.
|
|
7
|
+
"sha256": "c73eed5a190579623e2c6b5baa2b3f7e760a5d41d4630e05d970826007d4ae55",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.21.9.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
10
|
+
"versionCode": 21009,
|
|
11
11
|
"instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
|
|
12
12
|
"minSdk": 23,
|
|
13
13
|
"targetSdk": 36,
|
|
@@ -11,8 +11,23 @@ let package = Package(
|
|
|
11
11
|
),
|
|
12
12
|
],
|
|
13
13
|
targets: [
|
|
14
|
+
.target(
|
|
15
|
+
name: "AgentDeviceMacOSInput"
|
|
16
|
+
),
|
|
17
|
+
.target(
|
|
18
|
+
name: "AgentDeviceMacOSDeviceHub"
|
|
19
|
+
),
|
|
14
20
|
.executableTarget(
|
|
15
|
-
name: "AgentDeviceMacOSHelper"
|
|
21
|
+
name: "AgentDeviceMacOSHelper",
|
|
22
|
+
dependencies: ["AgentDeviceMacOSInput", "AgentDeviceMacOSDeviceHub"]
|
|
23
|
+
),
|
|
24
|
+
.testTarget(
|
|
25
|
+
name: "AgentDeviceMacOSInputTests",
|
|
26
|
+
dependencies: ["AgentDeviceMacOSInput"]
|
|
27
|
+
),
|
|
28
|
+
.testTarget(
|
|
29
|
+
name: "AgentDeviceMacOSDeviceHubTests",
|
|
30
|
+
dependencies: ["AgentDeviceMacOSDeviceHub"]
|
|
16
31
|
),
|
|
17
32
|
]
|
|
18
33
|
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The decisions the Device Hub pose press makes without a window server: which control a pose
|
|
4
|
+
/// names, which window belongs to a device, and which sidebar row identifies it. They live apart
|
|
5
|
+
/// from the helper executable so the tests can exercise them without an accessibility session.
|
|
6
|
+
|
|
7
|
+
/// Device Hub's three pose presets for a foldable simulator, named as the helper's `--pose`
|
|
8
|
+
/// argument spells them. Each maps to the accessibility description of its action-bar button.
|
|
9
|
+
public enum DeviceHubPose: String, CaseIterable, Sendable {
|
|
10
|
+
case closed
|
|
11
|
+
case book
|
|
12
|
+
case open
|
|
13
|
+
|
|
14
|
+
public init?(argument: String) {
|
|
15
|
+
self.init(rawValue: argument.lowercased())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// The `AXDescription` Device Hub gives the button for this pose.
|
|
19
|
+
public var controlDescription: String {
|
|
20
|
+
switch self {
|
|
21
|
+
case .closed: return "Closed"
|
|
22
|
+
case .book: return "Book"
|
|
23
|
+
case .open: return "Open"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Device Hub is launched through a trampoline, so LaunchServices registers it with no process
|
|
29
|
+
/// identifier; the process table is matched on the executable path instead.
|
|
30
|
+
public let deviceHubExecutableSuffix = "/DeviceHub.app/Contents/MacOS/DeviceHub"
|
|
31
|
+
|
|
32
|
+
/// The separator Device Hub puts between the device name and the OS in a window title.
|
|
33
|
+
public let deviceHubWindowTitleSeparator = " – "
|
|
34
|
+
|
|
35
|
+
/// Device Hub titles a device window `<device name> – <OS> <version>`, so a window shows the
|
|
36
|
+
/// device when its title is the name or the name followed by that exact separator. A bare
|
|
37
|
+
/// space would also match a simulator whose name merely extends this one ("iPhone Duo Lab").
|
|
38
|
+
public func deviceHubWindowShows(deviceName: String, title: String?) -> Bool {
|
|
39
|
+
guard let title else { return false }
|
|
40
|
+
return title == deviceName || title.hasPrefix(deviceName + deviceHubWindowTitleSeparator)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The accessibility identifier of the sidebar row for one simulator. The UDID is the one device
|
|
44
|
+
/// identity Device Hub exposes that two simulators sharing a name cannot confuse.
|
|
45
|
+
public func deviceHubDeviceRowIdentifier(udid: String) -> String {
|
|
46
|
+
return "TableRow.Device.\(udid)"
|
|
47
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import ApplicationServices
|
|
2
|
+
|
|
3
|
+
package enum DeviceHubWindowRead {
|
|
4
|
+
case available([AXUIElement])
|
|
5
|
+
case failed(Int32)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
package struct DeviceHubWindowCandidate {
|
|
9
|
+
package let processID: Int32
|
|
10
|
+
package let window: AXUIElement
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
package enum DeviceHubRowLookup {
|
|
14
|
+
case identified(AXUIElement, restoreSidebar: Bool)
|
|
15
|
+
case sidebarUnavailable
|
|
16
|
+
case unconfirmed
|
|
17
|
+
case timedOut
|
|
18
|
+
|
|
19
|
+
package var diagnostic: String {
|
|
20
|
+
switch self {
|
|
21
|
+
case .identified: return "identified"
|
|
22
|
+
case .sidebarUnavailable: return "sidebar-unavailable"
|
|
23
|
+
case .unconfirmed: return "row-unconfirmed"
|
|
24
|
+
case .timedOut: return "timed-out"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
package struct DeviceHubWindowInventory {
|
|
30
|
+
package private(set) var candidates: [DeviceHubWindowCandidate] = []
|
|
31
|
+
package private(set) var emptyProcessIDs: [Int32] = []
|
|
32
|
+
package private(set) var failures: [Int32: Int32] = [:]
|
|
33
|
+
package private(set) var candidateOutcomes: [String] = []
|
|
34
|
+
package private(set) var budgetExpired = false
|
|
35
|
+
|
|
36
|
+
package init() {}
|
|
37
|
+
|
|
38
|
+
package mutating func record(processID: Int32, read: DeviceHubWindowRead) {
|
|
39
|
+
switch read {
|
|
40
|
+
case .available(let windows):
|
|
41
|
+
if windows.isEmpty { emptyProcessIDs.append(processID) }
|
|
42
|
+
candidates.append(contentsOf: windows.map { DeviceHubWindowCandidate(processID: processID, window: $0) })
|
|
43
|
+
case .failed(let status):
|
|
44
|
+
failures[processID] = status
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
package mutating func record(candidate: Int, outcome: DeviceHubRowLookup) {
|
|
49
|
+
candidateOutcomes.append("\(candidates[candidate].processID):\(candidate):\(outcome.diagnostic)")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
package mutating func expireBudget() { budgetExpired = true }
|
|
53
|
+
|
|
54
|
+
package func processesToReopen(attempted: Set<Int32>) -> [Int32] {
|
|
55
|
+
budgetExpired ? [] : emptyProcessIDs.filter { !attempted.contains($0) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
package func failureReason(reopenFailures: [Int32: String] = [:]) -> String {
|
|
59
|
+
if !failures.isEmpty { return "device-hub-window-read-failed" }
|
|
60
|
+
if budgetExpired { return "device-hub-discovery-timeout" }
|
|
61
|
+
if !candidates.isEmpty { return "device-hub-identity-unconfirmed" }
|
|
62
|
+
if let reason = reopenFailures.sorted(by: { $0.key < $1.key }).first?.value { return reason }
|
|
63
|
+
return "device-hub-window-missing"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
}
|