fix(computer-use): prevent repeated screen recording prompts (#13427)
This commit is contained in:
parent
bdd763188f
commit
6e63bbbb52
|
|
@ -1044,11 +1044,7 @@ private func screenCaptureTrusted() -> Bool {
|
|||
}
|
||||
|
||||
private func screenCaptureTrustedSettled() -> Bool {
|
||||
PermissionTrustSettling.settleWithFallback(
|
||||
finalTimeoutMs: 500,
|
||||
probe: screenCaptureTrusted,
|
||||
fallbackProbe: screenCaptureTrustedByCaptureProbe
|
||||
)
|
||||
PermissionTrustSettling.settle(timeoutMs: 2_000, probe: screenCaptureTrusted).settled
|
||||
}
|
||||
|
||||
private func permissionStatusSnapshotSettled() -> PermissionStatusSnapshot {
|
||||
|
|
@ -1058,40 +1054,6 @@ private func permissionStatusSnapshotSettled() -> PermissionStatusSnapshot {
|
|||
)
|
||||
}
|
||||
|
||||
private func screenCaptureTrustedByCaptureProbe() -> Bool {
|
||||
guard let infos = CGWindowListCopyWindowInfo(
|
||||
[.optionOnScreenOnly],
|
||||
kCGNullWindowID
|
||||
) as? [[String: Any]] else {
|
||||
return false
|
||||
}
|
||||
let windows = infos.compactMap { info -> ScreenCaptureProbeWindow? in
|
||||
guard let layer = info[kCGWindowLayer as String] as? Int,
|
||||
let ownerPid = info[kCGWindowOwnerPID as String] as? NSNumber,
|
||||
let number = info[kCGWindowNumber as String] as? NSNumber
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return ScreenCaptureProbeWindow(
|
||||
layer: layer,
|
||||
ownerPid: ownerPid.int32Value,
|
||||
windowId: number.uint32Value
|
||||
)
|
||||
}
|
||||
guard let windowId = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
|
||||
ownPid: ProcessInfo.processInfo.processIdentifier,
|
||||
windows: windows
|
||||
) else {
|
||||
return false
|
||||
}
|
||||
return CGWindowListCreateImage(
|
||||
.null,
|
||||
[.optionIncludingWindow],
|
||||
CGWindowID(windowId),
|
||||
[.boundsIgnoreFraming]
|
||||
) != nil
|
||||
}
|
||||
|
||||
private func requestScreenCaptureAccess() -> Bool {
|
||||
CGRequestScreenCaptureAccess()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,33 +41,4 @@ public enum PermissionTrustSettling {
|
|||
waitedMs += interval
|
||||
}
|
||||
}
|
||||
|
||||
public static func settleWithFallback(
|
||||
initialTimeoutMs: Int = defaultTimeoutMs,
|
||||
finalTimeoutMs: Int,
|
||||
intervalMs: Int = defaultIntervalMs,
|
||||
sleepMs: (Int) -> Void = { interval in
|
||||
Thread.sleep(forTimeInterval: TimeInterval(interval) / 1_000)
|
||||
},
|
||||
probe: () -> Bool,
|
||||
fallbackProbe: () -> Bool
|
||||
) -> Bool {
|
||||
if settle(
|
||||
timeoutMs: initialTimeoutMs,
|
||||
intervalMs: intervalMs,
|
||||
sleepMs: sleepMs,
|
||||
probe: probe
|
||||
).settled {
|
||||
return true
|
||||
}
|
||||
if fallbackProbe() {
|
||||
return true
|
||||
}
|
||||
return settle(
|
||||
timeoutMs: finalTimeoutMs,
|
||||
intervalMs: intervalMs,
|
||||
sleepMs: sleepMs,
|
||||
probe: probe
|
||||
).settled
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
public struct ScreenCaptureProbeWindow: Equatable {
|
||||
public let layer: Int
|
||||
public let ownerPid: Int32
|
||||
public let windowId: UInt32
|
||||
|
||||
public init(layer: Int, ownerPid: Int32, windowId: UInt32) {
|
||||
self.layer = layer
|
||||
self.ownerPid = ownerPid
|
||||
self.windowId = windowId
|
||||
}
|
||||
}
|
||||
|
||||
public enum ScreenCaptureProbeWindowSelection {
|
||||
public static func firstCrossProcessNormalWindow(
|
||||
ownPid: Int32,
|
||||
windows: [ScreenCaptureProbeWindow]
|
||||
) -> UInt32? {
|
||||
windows.first { window in
|
||||
window.layer == 0 && window.ownerPid != ownPid
|
||||
}?.windowId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import XCTest
|
||||
|
||||
final class ScreenCapturePermissionPreflightSafetyTests: XCTestCase {
|
||||
func testPermissionPreflightCannotRequestOrCaptureTheScreen() throws {
|
||||
let source = try computerUseSource()
|
||||
let start = try XCTUnwrap(source.range(of: "private func screenCaptureTrusted()"))
|
||||
let end = try XCTUnwrap(
|
||||
source.range(of: "private func requestScreenCaptureAccess()", range: start.upperBound..<source.endIndex)
|
||||
)
|
||||
let preflightSource = source[start.lowerBound..<end.lowerBound]
|
||||
|
||||
XCTAssertTrue(preflightSource.contains("CGPreflightScreenCaptureAccess()"))
|
||||
XCTAssertTrue(preflightSource.contains("timeoutMs: 2_000"))
|
||||
XCTAssertFalse(preflightSource.contains("CGRequestScreenCaptureAccess()"))
|
||||
XCTAssertFalse(preflightSource.contains("CGWindowListCreateImage"))
|
||||
XCTAssertFalse(preflightSource.contains("SCScreenshotManager"))
|
||||
}
|
||||
|
||||
private func computerUseSource() throws -> String {
|
||||
let testFile = URL(fileURLWithPath: #filePath)
|
||||
let packageRoot = testFile
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
let mainPath = packageRoot
|
||||
.appendingPathComponent("Sources")
|
||||
.appendingPathComponent("OrcaComputerUseMacOS")
|
||||
.appendingPathComponent("main.swift")
|
||||
return try String(contentsOf: mainPath, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import Testing
|
||||
@testable import OrcaComputerUseMacOSCore
|
||||
|
||||
@Suite("Screen Capture permission settling")
|
||||
struct ScreenCapturePermissionSettlingTests {
|
||||
@Test("initial preflight success skips fallback")
|
||||
func initialSuccess() {
|
||||
var fallbackCalls = 0
|
||||
let trusted = PermissionTrustSettling.settleWithFallback(
|
||||
finalTimeoutMs: 500,
|
||||
sleepMs: { _ in },
|
||||
probe: { true },
|
||||
fallbackProbe: {
|
||||
fallbackCalls += 1
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
#expect(trusted)
|
||||
#expect(fallbackCalls == 0)
|
||||
}
|
||||
|
||||
@Test("capture fallback can establish trust")
|
||||
func fallbackSuccess() {
|
||||
var fallbackCalls = 0
|
||||
let trusted = PermissionTrustSettling.settleWithFallback(
|
||||
initialTimeoutMs: 200,
|
||||
finalTimeoutMs: 200,
|
||||
intervalMs: 100,
|
||||
sleepMs: { _ in },
|
||||
probe: { false },
|
||||
fallbackProbe: {
|
||||
fallbackCalls += 1
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
#expect(trusted)
|
||||
#expect(fallbackCalls == 1)
|
||||
}
|
||||
|
||||
@Test("preflight is retried after fallback failure")
|
||||
func finalPreflightSuccess() {
|
||||
var probeCalls = 0
|
||||
let trusted = PermissionTrustSettling.settleWithFallback(
|
||||
initialTimeoutMs: 100,
|
||||
finalTimeoutMs: 200,
|
||||
intervalMs: 100,
|
||||
sleepMs: { _ in },
|
||||
probe: {
|
||||
probeCalls += 1
|
||||
return probeCalls == 4
|
||||
},
|
||||
fallbackProbe: { false }
|
||||
)
|
||||
|
||||
#expect(trusted)
|
||||
#expect(probeCalls == 4)
|
||||
}
|
||||
|
||||
@Test("persistent denial remains denied")
|
||||
func persistentDenial() {
|
||||
var fallbackCalls = 0
|
||||
let trusted = PermissionTrustSettling.settleWithFallback(
|
||||
initialTimeoutMs: 100,
|
||||
finalTimeoutMs: 100,
|
||||
intervalMs: 100,
|
||||
sleepMs: { _ in },
|
||||
probe: { false },
|
||||
fallbackProbe: {
|
||||
fallbackCalls += 1
|
||||
return false
|
||||
}
|
||||
)
|
||||
|
||||
#expect(!trusted)
|
||||
#expect(fallbackCalls == 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import Testing
|
||||
@testable import OrcaComputerUseMacOSCore
|
||||
|
||||
@Suite("Screen capture probe window selection")
|
||||
struct ScreenCaptureProbeWindowSelectionTests {
|
||||
@Test("own process windows cannot prove cross-app capture permission")
|
||||
func excludesOwnWindows() {
|
||||
let selected = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
|
||||
ownPid: 42,
|
||||
windows: [
|
||||
.init(layer: 0, ownerPid: 42, windowId: 1),
|
||||
.init(layer: 0, ownerPid: 84, windowId: 2)
|
||||
]
|
||||
)
|
||||
|
||||
#expect(selected == 2)
|
||||
}
|
||||
|
||||
@Test("non-normal windows are skipped")
|
||||
func excludesNonNormalWindows() {
|
||||
let selected = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
|
||||
ownPid: 42,
|
||||
windows: [
|
||||
.init(layer: 1, ownerPid: 84, windowId: 1),
|
||||
.init(layer: 0, ownerPid: 84, windowId: 2)
|
||||
]
|
||||
)
|
||||
|
||||
#expect(selected == 2)
|
||||
}
|
||||
|
||||
@Test("no cross-process normal window returns nil")
|
||||
func noCandidate() {
|
||||
let selected = ScreenCaptureProbeWindowSelection.firstCrossProcessNormalWindow(
|
||||
ownPid: 42,
|
||||
windows: [
|
||||
.init(layer: 0, ownerPid: 42, windowId: 1),
|
||||
.init(layer: 1, ownerPid: 84, windowId: 2)
|
||||
]
|
||||
)
|
||||
|
||||
#expect(selected == nil)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue