import AppKit
|
import ApplicationServices
|
import Foundation
|
|
struct WindowState: Codable {
|
let windowID: Int64?
|
let x: Double
|
let y: Double
|
let width: Double
|
let height: Double
|
let minimized: Bool
|
let source: Bool
|
}
|
|
struct RestoreSummary {
|
let mutableRecorded: Int
|
let restored: Int
|
let mutationFailures: Int
|
|
var exitCode: Int32 {
|
restored == mutableRecorded && mutationFailures == 0 ? 0 : 75
|
}
|
}
|
|
enum AXReadFailure: Error {
|
case nonBenign(AXError)
|
case traversalLimit
|
}
|
|
enum BindingPlanFailure: Error {
|
case invalidSource
|
case duplicateNonSourceID
|
case missingNonSource
|
case readError
|
}
|
|
struct WindowBindingPlan {
|
let states: [WindowState]
|
let windowIndices: [Int]
|
}
|
|
enum IsolationFailureStage: String, Equatable {
|
case otherWindowMinimizeSetFailed = "OTHER_WINDOW_MINIMIZE_SET_FAILED"
|
case sourceUnminimizeSetFailed = "SOURCE_UNMINIMIZE_SET_FAILED"
|
case sourceSizeSetFailed = "SOURCE_SIZE_SET_FAILED"
|
case sourcePositionSetFailed = "SOURCE_POSITION_SET_FAILED"
|
case sourceRaiseFailed = "SOURCE_RAISE_FAILED"
|
case sourceReadbackAXFailed = "SOURCE_READBACK_AX_FAILED"
|
case sourceReadbackMismatch = "SOURCE_READBACK_MISMATCH"
|
}
|
|
enum PreStateFailureStage: String, Equatable {
|
case finderNotRunning = "FINDER_NOT_RUNNING"
|
case finderWindowsReadFailed = "FINDER_WINDOWS_READ_FAILED"
|
case stateEncodeFailed = "STATE_ENCODE_FAILED"
|
case stateAtomicWriteFailed = "STATE_ATOMIC_WRITE_FAILED"
|
}
|
|
enum StatePersistenceFailure: Error, Equatable {
|
case encode
|
case atomicWrite
|
|
var stage: PreStateFailureStage {
|
switch self {
|
case .encode: return .stateEncodeFailed
|
case .atomicWrite: return .stateAtomicWriteFailed
|
}
|
}
|
}
|
|
enum IsolationMutationResult: Equatable {
|
case success
|
case failure(IsolationFailureStage)
|
}
|
|
struct IsolationFailureResult {
|
let line: String
|
let exitCode: Int32
|
}
|
|
func preStateFailureLine(_ stage: PreStateFailureStage) -> String {
|
"stage=\(stage.rawValue) mutation=0"
|
}
|
|
func persistStateSnapshot(
|
encode: () throws -> Data,
|
atomicWrite: (Data) throws -> Void
|
) -> Result<Void, StatePersistenceFailure> {
|
let data: Data
|
do { data = try encode() } catch { return .failure(.encode) }
|
do { try atomicWrite(data) } catch { return .failure(.atomicWrite) }
|
return .success(())
|
}
|
|
func isBenignAbsence(_ error: AXError) -> Bool {
|
error == .noValue || error == .attributeUnsupported
|
}
|
|
func uniqueSourceWindow(_ result: Result<[Int], AXReadFailure>) throws -> Int? {
|
let perWindowMatchCounts = try result.get()
|
guard perWindowMatchCounts.reduce(0, +) == 1 else { return nil }
|
return perWindowMatchCounts.firstIndex(of: 1)
|
}
|
|
func buildIsolationPlan(
|
sourceIndex: Int,
|
windowCount: Int,
|
windowID: (Int) -> Result<Int64?, AXReadFailure>,
|
rect: (Int) -> Result<CGRect?, AXReadFailure>,
|
minimized: (Int) -> Result<Bool?, AXReadFailure>
|
) -> Result<WindowBindingPlan, BindingPlanFailure> {
|
guard (0..<windowCount).contains(sourceIndex) else { return .failure(.invalidSource) }
|
let sourceRect: CGRect
|
switch rect(sourceIndex) {
|
case .failure: return .failure(.readError)
|
case .success(let value):
|
guard let value, validRect(value) else { return .failure(.invalidSource) }
|
sourceRect = value
|
}
|
let sourceMinimized: Bool
|
switch minimized(sourceIndex) {
|
case .failure: return .failure(.readError)
|
case .success(let value):
|
guard let value else { return .failure(.invalidSource) }
|
sourceMinimized = value
|
}
|
var states = [WindowState(
|
windowID: nil,
|
x: sourceRect.origin.x,
|
y: sourceRect.origin.y,
|
width: sourceRect.width,
|
height: sourceRect.height,
|
minimized: sourceMinimized,
|
source: true
|
)]
|
var indices = [sourceIndex]
|
var usedIDs: Set<Int64> = []
|
for index in 0..<windowCount where index != sourceIndex {
|
let id: Int64
|
switch windowID(index) {
|
case .failure: return .failure(.readError)
|
case .success(nil): continue
|
case .success(let value): id = value!
|
}
|
guard usedIDs.insert(id).inserted else { return .failure(.duplicateNonSourceID) }
|
let windowRect: CGRect
|
switch rect(index) {
|
case .failure: return .failure(.readError)
|
case .success(let value):
|
guard let value, validRect(value) else { continue }
|
windowRect = value
|
}
|
let windowMinimized: Bool
|
switch minimized(index) {
|
case .failure: return .failure(.readError)
|
case .success(let value):
|
guard let value else { continue }
|
windowMinimized = value
|
}
|
states.append(WindowState(
|
windowID: id,
|
x: windowRect.origin.x,
|
y: windowRect.origin.y,
|
width: windowRect.width,
|
height: windowRect.height,
|
minimized: windowMinimized,
|
source: false
|
))
|
indices.append(index)
|
}
|
return .success(WindowBindingPlan(states: states, windowIndices: indices))
|
}
|
|
func buildRestorePlan(
|
states: [WindowState],
|
sourceIndex: Int?,
|
windowCount: Int,
|
windowID: (Int) -> Result<Int64?, AXReadFailure>,
|
sourceRect: () -> Result<CGRect?, AXReadFailure>,
|
sourceMinimized: () -> Result<Bool?, AXReadFailure>
|
) -> Result<WindowBindingPlan, BindingPlanFailure> {
|
guard let sourceIndex, (0..<windowCount).contains(sourceIndex),
|
states.filter(\.source).count == 1,
|
let sourceState = states.first(where: \.source) else { return .failure(.invalidSource) }
|
switch sourceRect() {
|
case .failure: return .failure(.readError)
|
case .success(let value): guard let value, validRect(value) else { return .failure(.invalidSource) }
|
}
|
switch sourceMinimized() {
|
case .failure: return .failure(.readError)
|
case .success(let value): guard value != nil else { return .failure(.invalidSource) }
|
}
|
var currentByID: [Int64: Int] = [:]
|
for index in 0..<windowCount where index != sourceIndex {
|
switch windowID(index) {
|
case .failure: return .failure(.readError)
|
case .success(nil): continue
|
case .success(let value):
|
let id = value!
|
guard currentByID[id] == nil else { return .failure(.duplicateNonSourceID) }
|
currentByID[id] = index
|
}
|
}
|
var stateIDs: Set<Int64> = []
|
var planStates = [sourceState]
|
var indices = [sourceIndex]
|
for state in states where !state.source {
|
guard let id = state.windowID, stateIDs.insert(id).inserted,
|
let index = currentByID[id] else { return .failure(.missingNonSource) }
|
planStates.append(state)
|
indices.append(index)
|
}
|
return .success(WindowBindingPlan(states: planStates, windowIndices: indices))
|
}
|
|
func restoreRecorded(
|
_ plan: WindowBindingPlan,
|
restore: (WindowState, Int) -> Bool
|
) -> Bool {
|
zip(plan.states, plan.windowIndices).map(restore).allSatisfy { $0 }
|
}
|
|
func validRect(_ rect: CGRect?) -> Bool {
|
guard let rect else { return false }
|
return [rect.origin.x, rect.origin.y, rect.size.width, rect.size.height].allSatisfy(\.isFinite)
|
&& rect.size.width > 0 && rect.size.height > 0
|
}
|
|
func isIsolationReadbackCompatible(
|
exactUniqueBound: Bool,
|
minimized: Bool?,
|
windowRect: CGRect?,
|
displayBounds: [CGRect]
|
) -> Bool {
|
guard exactUniqueBound, minimized == false,
|
let windowRect, validRect(windowRect), !displayBounds.isEmpty,
|
displayBounds.allSatisfy({ validRect($0) }) else { return false }
|
let center = CGPoint(x: windowRect.midX, y: windowRect.midY)
|
return displayBounds.contains(where: { $0.contains(center) })
|
}
|
|
func postMutationReadbackExit(
|
_ readback: Result<Bool, AXReadFailure>,
|
validationFailure: Int32,
|
cleanupFailure: Int32,
|
rollback: () -> Bool
|
) -> Int32? {
|
if case .success(true) = readback { return nil }
|
return rollback() ? validationFailure : cleanupFailure
|
}
|
|
func runIsolationMutationCore(
|
otherWindowCount: Int,
|
minimizeOther: (Int) -> Bool,
|
unminimizeSource: () -> Bool,
|
setSourceSize: () -> Bool,
|
setSourcePosition: () -> Bool,
|
raiseSource: () -> Bool,
|
beforeReadback: () -> Void,
|
readback: () -> Result<Bool, AXReadFailure>
|
) -> IsolationMutationResult {
|
for index in 0..<otherWindowCount {
|
guard minimizeOther(index) else { return .failure(.otherWindowMinimizeSetFailed) }
|
}
|
guard unminimizeSource() else { return .failure(.sourceUnminimizeSetFailed) }
|
guard setSourceSize() else { return .failure(.sourceSizeSetFailed) }
|
guard setSourcePosition() else { return .failure(.sourcePositionSetFailed) }
|
guard raiseSource() else { return .failure(.sourceRaiseFailed) }
|
beforeReadback()
|
switch readback() {
|
case .failure: return .failure(.sourceReadbackAXFailed)
|
case .success(true): return .success
|
case .success(false): return .failure(.sourceReadbackMismatch)
|
}
|
}
|
|
func finishIsolationFailure(
|
_ stage: IsolationFailureStage,
|
rollback: () -> Bool
|
) -> IsolationFailureResult {
|
let rollbackComplete = rollback()
|
return IsolationFailureResult(
|
line: "stage=\(stage.rawValue) rollback=\(rollbackComplete ? "PASS" : "FAIL")",
|
exitCode: rollbackComplete ? 72 : 73
|
)
|
}
|
|
func countExactSourceCore<Node>(
|
_ node: Node,
|
depth: Int = 0,
|
depthLimit: Int,
|
matches: (Node) throws -> Bool,
|
children: (Node) throws -> [Node]
|
) throws -> Int {
|
var count = try matches(node) ? 1 : 0
|
let childNodes = try children(node)
|
if depth == depthLimit {
|
guard childNodes.isEmpty else { throw AXReadFailure.traversalLimit }
|
return count
|
}
|
guard depth < depthLimit else { throw AXReadFailure.traversalLimit }
|
for child in childNodes {
|
count += try countExactSourceCore(
|
child,
|
depth: depth + 1,
|
depthLimit: depthLimit,
|
matches: matches,
|
children: children
|
)
|
}
|
return count
|
}
|
|
func selfTest() -> Int32 {
|
enum StateFixtureError: Error { case expected }
|
func isTraversalLimit(_ result: Result<Int, Error>) -> Bool {
|
guard case .failure(let error) = result,
|
let failure = error as? AXReadFailure else { return false }
|
if case .traversalLimit = failure { return true }
|
return false
|
}
|
struct TraversalNode {
|
let matches: Bool
|
let children: [Int]
|
}
|
let limit = 14
|
var shallowNodes = [TraversalNode(matches: false, children: [1])]
|
shallowNodes.append(TraversalNode(matches: true, children: []))
|
let shallowUnique = try? countExactSourceCore(
|
0,
|
depthLimit: limit,
|
matches: { shallowNodes[$0].matches },
|
children: { shallowNodes[$0].children }
|
)
|
let boundaryLeafNodes = (0...limit).map { depth in
|
TraversalNode(matches: depth == limit, children: depth == limit ? [] : [depth + 1])
|
}
|
let boundaryLeaf = try? countExactSourceCore(
|
0,
|
depthLimit: limit,
|
matches: { boundaryLeafNodes[$0].matches },
|
children: { boundaryLeafNodes[$0].children }
|
)
|
var truncatedNodes = boundaryLeafNodes
|
truncatedNodes[limit] = TraversalNode(matches: false, children: [limit + 1])
|
truncatedNodes.append(TraversalNode(matches: true, children: []))
|
var truncatedReads = 0
|
let truncatedResult = Result {
|
try countExactSourceCore(
|
0,
|
depthLimit: limit,
|
matches: { truncatedReads += 1; return truncatedNodes[$0].matches },
|
children: { truncatedReads += 1; return truncatedNodes[$0].children }
|
)
|
}
|
var hiddenDuplicateNodes = [TraversalNode(matches: false, children: [1, 2])]
|
hiddenDuplicateNodes.append(TraversalNode(matches: true, children: []))
|
let chainStart = hiddenDuplicateNodes.count
|
for depth in 1...limit {
|
hiddenDuplicateNodes.append(TraversalNode(matches: false, children: [chainStart + depth]))
|
}
|
hiddenDuplicateNodes.append(TraversalNode(matches: true, children: []))
|
var hiddenDuplicateTerminalReads = 0
|
let hiddenDuplicateResult = Result {
|
try countExactSourceCore(
|
0,
|
depthLimit: limit,
|
matches: { hiddenDuplicateTerminalReads += 1; return hiddenDuplicateNodes[$0].matches },
|
children: { hiddenDuplicateTerminalReads += 1; return hiddenDuplicateNodes[$0].children }
|
)
|
}
|
let expectedStageCases: [(String, IsolationFailureStage, [String])] = [
|
("other-1", .otherWindowMinimizeSetFailed, ["other-0", "other-1"]),
|
("unminimize", .sourceUnminimizeSetFailed, ["other-0", "other-1", "unminimize"]),
|
("size", .sourceSizeSetFailed, ["other-0", "other-1", "unminimize", "size"]),
|
("position", .sourcePositionSetFailed, ["other-0", "other-1", "unminimize", "size", "position"]),
|
("raise", .sourceRaiseFailed, ["other-0", "other-1", "unminimize", "size", "position", "raise"])
|
]
|
var stageCasesPass = true
|
for (failedOperation, expectedStage, expectedCalls) in expectedStageCases {
|
var calls: [String] = []
|
let result = runIsolationMutationCore(
|
otherWindowCount: 2,
|
minimizeOther: { index in calls.append("other-\(index)"); return "other-\(index)" != failedOperation },
|
unminimizeSource: { calls.append("unminimize"); return failedOperation != "unminimize" },
|
setSourceSize: { calls.append("size"); return failedOperation != "size" },
|
setSourcePosition: { calls.append("position"); return failedOperation != "position" },
|
raiseSource: { calls.append("raise"); return failedOperation != "raise" },
|
beforeReadback: { calls.append("wait") },
|
readback: { calls.append("readback"); return .success(true) }
|
)
|
stageCasesPass = stageCasesPass
|
&& result == .failure(expectedStage)
|
&& calls == expectedCalls
|
}
|
var readErrorCalls: [String] = []
|
let readErrorStage = runIsolationMutationCore(
|
otherWindowCount: 0,
|
minimizeOther: { _ in readErrorCalls.append("other"); return true },
|
unminimizeSource: { readErrorCalls.append("unminimize"); return true },
|
setSourceSize: { readErrorCalls.append("size"); return true },
|
setSourcePosition: { readErrorCalls.append("position"); return true },
|
raiseSource: { readErrorCalls.append("raise"); return true },
|
beforeReadback: { readErrorCalls.append("wait") },
|
readback: { readErrorCalls.append("readback"); return .failure(.nonBenign(.cannotComplete)) }
|
)
|
var mismatchCalls: [String] = []
|
let mismatchStage = runIsolationMutationCore(
|
otherWindowCount: 0,
|
minimizeOther: { _ in mismatchCalls.append("other"); return true },
|
unminimizeSource: { mismatchCalls.append("unminimize"); return true },
|
setSourceSize: { mismatchCalls.append("size"); return true },
|
setSourcePosition: { mismatchCalls.append("position"); return true },
|
raiseSource: { mismatchCalls.append("raise"); return true },
|
beforeReadback: { mismatchCalls.append("wait") },
|
readback: { mismatchCalls.append("readback"); return .success(false) }
|
)
|
var successCalls: [String] = []
|
let successStage = runIsolationMutationCore(
|
otherWindowCount: 1,
|
minimizeOther: { index in successCalls.append("other-\(index)"); return true },
|
unminimizeSource: { successCalls.append("unminimize"); return true },
|
setSourceSize: { successCalls.append("size"); return true },
|
setSourcePosition: { successCalls.append("position"); return true },
|
raiseSource: { successCalls.append("raise"); return true },
|
beforeReadback: { successCalls.append("wait") },
|
readback: { successCalls.append("readback"); return .success(true) }
|
)
|
var rollbackPassCalls = 0
|
let rollbackPass = finishIsolationFailure(.sourcePositionSetFailed) {
|
rollbackPassCalls += 1
|
return true
|
}
|
var rollbackFailCalls = 0
|
let rollbackFail = finishIsolationFailure(.sourceReadbackAXFailed) {
|
rollbackFailCalls += 1
|
return false
|
}
|
let displayBounds = [CGRect(x: 0, y: 0, width: 1920, height: 1080)]
|
let finderNormalizedRect = CGRect(x: 18, y: 62, width: 497, height: 325)
|
let invalidReadbackRects: [CGRect?] = [
|
nil,
|
.zero,
|
CGRect(x: 10, y: 70, width: 0, height: 320),
|
CGRect(x: 10, y: 70, width: 500, height: -1),
|
CGRect(x: CGFloat.nan, y: 70, width: 500, height: 320),
|
CGRect(x: 10, y: CGFloat.infinity, width: 500, height: 320),
|
CGRect(x: 2500, y: 70, width: 500, height: 320),
|
CGRect(x: -499, y: 70, width: 500, height: 320),
|
]
|
let normalizedReadbackAccepted = isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: finderNormalizedRect,
|
displayBounds: displayBounds
|
)
|
let tinyPositiveOnscreenAccepted = isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: CGRect(x: 1, y: 1, width: 1, height: 1),
|
displayBounds: displayBounds
|
)
|
let invalidReadbacksRejected = invalidReadbackRects.allSatisfy {
|
!isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: $0,
|
displayBounds: displayBounds
|
)
|
}
|
let invalidDisplayBounds = [
|
CGRect.zero,
|
CGRect(x: 0, y: 0, width: -1, height: 100),
|
CGRect(x: CGFloat.nan, y: 0, width: 100, height: 100),
|
CGRect(x: 0, y: CGFloat.infinity, width: 100, height: 100),
|
]
|
let mixedInvalidDisplaysRejected = invalidDisplayBounds.allSatisfy { invalidDisplay in
|
!isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: finderNormalizedRect,
|
displayBounds: [invalidDisplay, displayBounds[0]]
|
)
|
}
|
let pureInvalidDisplaysRejected = invalidDisplayBounds.allSatisfy { invalidDisplay in
|
!isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: finderNormalizedRect,
|
displayBounds: [invalidDisplay]
|
)
|
}
|
let fourDirectionDisplays = [
|
CGRect(x: -100, y: 0, width: 100, height: 100),
|
CGRect(x: 0, y: 0, width: 100, height: 100),
|
CGRect(x: 100, y: 0, width: 100, height: 100),
|
CGRect(x: 0, y: -100, width: 100, height: 100),
|
CGRect(x: 0, y: 100, width: 100, height: 100),
|
]
|
let fourDirectionCentersAccepted = [
|
CGPoint(x: -50, y: 50),
|
CGPoint(x: 150, y: 50),
|
CGPoint(x: 50, y: -50),
|
CGPoint(x: 50, y: 150),
|
].allSatisfy { center in
|
isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: CGRect(x: center.x - 10, y: center.y - 10, width: 20, height: 20),
|
displayBounds: fourDirectionDisplays
|
)
|
}
|
let validDisplaysOffscreenRejected = !isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: false,
|
windowRect: CGRect(x: 300, y: 300, width: 20, height: 20),
|
displayBounds: fourDirectionDisplays
|
)
|
let sourceWithoutID = WindowState(
|
windowID: nil,
|
x: 10,
|
y: 20,
|
width: 500,
|
height: 320,
|
minimized: false,
|
source: true
|
)
|
let goodRect = CGRect(x: 10, y: 20, width: 500, height: 320)
|
var sourceIDReads = 0
|
let isolationResult = buildIsolationPlan(
|
sourceIndex: 1,
|
windowCount: 3,
|
windowID: { index in
|
if index == 1 { sourceIDReads += 1; return .success(nil) }
|
return index == 0 ? .success(10) : .success(nil)
|
},
|
rect: { _ in .success(goodRect) },
|
minimized: { _ in .success(false) }
|
)
|
guard case .success(let isolationPlan) = isolationResult else { return 1 }
|
let duplicateResult = buildIsolationPlan(
|
sourceIndex: 1,
|
windowCount: 3,
|
windowID: { $0 == 1 ? .success(nil) : .success(10) },
|
rect: { _ in .success(goodRect) },
|
minimized: { _ in .success(false) }
|
)
|
let restoreResult = buildRestorePlan(
|
states: isolationPlan.states,
|
sourceIndex: 1,
|
windowCount: 3,
|
windowID: { $0 == 0 ? .success(10) : .success(nil) },
|
sourceRect: { .success(goodRect) },
|
sourceMinimized: { .success(false) }
|
)
|
guard case .success(let restorePlan) = restoreResult else { return 1 }
|
var restoreCalls: [Int] = []
|
let restoreComplete = restoreRecorded(restorePlan) { _, index in restoreCalls.append(index); return true }
|
var failedRestoreCalls: [Int] = []
|
let restoreIncomplete = restoreRecorded(restorePlan) { _, index in
|
failedRestoreCalls.append(index)
|
return index != 1
|
}
|
var terminalIDReads = 0
|
let terminalResult = buildIsolationPlan(
|
sourceIndex: 0,
|
windowCount: 4,
|
windowID: { index in
|
terminalIDReads += 1
|
if index == 1 || index == 2 { return .success(10) }
|
return .success(11)
|
},
|
rect: { _ in .success(goodRect) },
|
minimized: { _ in .success(false) }
|
)
|
var restoreTerminalIDReads = 0
|
let restoreTerminalResult = buildRestorePlan(
|
states: [sourceWithoutID],
|
sourceIndex: 0,
|
windowCount: 4,
|
windowID: { index in
|
restoreTerminalIDReads += 1
|
if index == 1 || index == 2 { return .success(10) }
|
return .success(11)
|
},
|
sourceRect: { .success(goodRect) },
|
sourceMinimized: { .success(false) }
|
)
|
let traversalErrorRejected: Bool
|
do {
|
_ = try uniqueSourceWindow(.failure(.nonBenign(.cannotComplete)))
|
traversalErrorRejected = false
|
} catch {
|
traversalErrorRejected = true
|
}
|
var rollbackCalled = 0
|
let readErrorCleanupSuccess = postMutationReadbackExit(
|
.failure(.nonBenign(.cannotComplete)), validationFailure: 72, cleanupFailure: 73
|
) { rollbackCalled += 1; return true }
|
let readErrorCleanupFailure = postMutationReadbackExit(
|
.failure(.nonBenign(.cannotComplete)), validationFailure: 72, cleanupFailure: 73
|
) { rollbackCalled += 1; return false }
|
var persistedBytes = Data()
|
let persistenceSuccess = persistStateSnapshot(
|
encode: { Data("state".utf8) },
|
atomicWrite: { persistedBytes = $0 }
|
)
|
let persistenceEncodeFailure = persistStateSnapshot(
|
encode: { throw StateFixtureError.expected },
|
atomicWrite: { _ in fatalError("write must not run after encode failure") }
|
)
|
let persistenceWriteFailure = persistStateSnapshot(
|
encode: { Data("state".utf8) },
|
atomicWrite: { _ in throw StateFixtureError.expected }
|
)
|
guard mixedInvalidDisplaysRejected,
|
pureInvalidDisplaysRejected,
|
fourDirectionCentersAccepted,
|
validDisplaysOffscreenRejected,
|
normalizedReadbackAccepted,
|
tinyPositiveOnscreenAccepted,
|
invalidReadbacksRejected,
|
!isIsolationReadbackCompatible(exactUniqueBound: false, minimized: false, windowRect: finderNormalizedRect, displayBounds: displayBounds),
|
!isIsolationReadbackCompatible(exactUniqueBound: true, minimized: nil, windowRect: finderNormalizedRect, displayBounds: displayBounds),
|
!isIsolationReadbackCompatible(exactUniqueBound: true, minimized: true, windowRect: finderNormalizedRect, displayBounds: displayBounds),
|
!isIsolationReadbackCompatible(exactUniqueBound: true, minimized: false, windowRect: finderNormalizedRect, displayBounds: []),
|
stageCasesPass,
|
readErrorStage == .failure(.sourceReadbackAXFailed),
|
readErrorCalls == ["unminimize", "size", "position", "raise", "wait", "readback"],
|
mismatchStage == .failure(.sourceReadbackMismatch),
|
mismatchCalls == ["unminimize", "size", "position", "raise", "wait", "readback"],
|
successStage == .success,
|
successCalls == ["other-0", "unminimize", "size", "position", "raise", "wait", "readback"],
|
rollbackPassCalls == 1,
|
rollbackPass.exitCode == 72,
|
rollbackPass.line == "stage=SOURCE_POSITION_SET_FAILED rollback=PASS",
|
rollbackFailCalls == 1,
|
rollbackFail.exitCode == 73,
|
rollbackFail.line == "stage=SOURCE_READBACK_AX_FAILED rollback=FAIL",
|
shallowUnique == 1,
|
boundaryLeaf == 1,
|
isTraversalLimit(truncatedResult),
|
truncatedReads == 30,
|
isTraversalLimit(hiddenDuplicateResult),
|
hiddenDuplicateTerminalReads == 32,
|
sourceWithoutID.windowID == nil,
|
sourceIDReads == 0,
|
isolationPlan.states.count == 2,
|
isolationPlan.states[0].source,
|
isolationPlan.states[0].windowID == nil,
|
isolationPlan.windowIndices == [1, 0],
|
isolationPlan.states[1].windowID == 10,
|
{ if case .failure(.duplicateNonSourceID) = duplicateResult { return true }; return false }(),
|
restorePlan.windowIndices == [1, 0],
|
restorePlan.states.map(\.source) == [true, false],
|
restoreComplete, restoreCalls == [1, 0],
|
!restoreIncomplete, failedRestoreCalls == [1, 0],
|
{ if case .failure(.duplicateNonSourceID) = terminalResult { return true }; return false }(),
|
terminalIDReads == 2,
|
{ if case .failure(.duplicateNonSourceID) = restoreTerminalResult { return true }; return false }(),
|
restoreTerminalIDReads == 2,
|
try! uniqueSourceWindow(.success([0, 1, 0])) == 1,
|
try! uniqueSourceWindow(.success([0, 0])) == nil,
|
try! uniqueSourceWindow(.success([1, 1])) == nil,
|
traversalErrorRejected,
|
isBenignAbsence(.noValue), isBenignAbsence(.attributeUnsupported),
|
!isBenignAbsence(.cannotComplete), !isBenignAbsence(.invalidUIElement), !isBenignAbsence(.apiDisabled),
|
validRect(CGRect(x: 1, y: 2, width: 3, height: 4)),
|
!validRect(.zero),
|
RestoreSummary(mutableRecorded: 2, restored: 2, mutationFailures: 0).exitCode == 0,
|
RestoreSummary(mutableRecorded: 2, restored: 1, mutationFailures: 0).exitCode != 0,
|
RestoreSummary(mutableRecorded: 2, restored: 2, mutationFailures: 1).exitCode != 0,
|
rollbackCalled == 2, readErrorCleanupSuccess == 72, readErrorCleanupFailure == 73,
|
postMutationReadbackExit(.success(true), validationFailure: 72, cleanupFailure: 73, rollback: { false }) == nil,
|
preStateFailureLine(.finderNotRunning) == "stage=FINDER_NOT_RUNNING mutation=0",
|
preStateFailureLine(.finderWindowsReadFailed) == "stage=FINDER_WINDOWS_READ_FAILED mutation=0",
|
{ if case .success = persistenceSuccess { return true }; return false }(),
|
persistedBytes == Data("state".utf8),
|
{ if case .failure(.encode) = persistenceEncodeFailure { return true }; return false }(),
|
{ if case .failure(.atomicWrite) = persistenceWriteFailure { return true }; return false }(),
|
StatePersistenceFailure.encode.stage == .stateEncodeFailed,
|
StatePersistenceFailure.atomicWrite.stage == .stateAtomicWriteFailed else { return 1 }
|
print("self_test=PASS exact_unique_window=true traversal_read_error_rejected=true post_mutation_read_error_rollback=true cleanup_status_distinct=true pre_state_observability=true state_write_classification=true")
|
return 0
|
}
|
|
if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] { exit(selfTest()) }
|
guard CommandLine.arguments.count == 4,
|
["isolate", "restore"].contains(CommandLine.arguments[1]),
|
!CommandLine.arguments[2].isEmpty,
|
!CommandLine.arguments[3].isEmpty else {
|
fputs("usage: FinderWindowIsolation isolate|restore state.json exactSourceIdentifier\n", stderr)
|
exit(64)
|
}
|
let mode = CommandLine.arguments[1]
|
let stateURL = URL(fileURLWithPath: CommandLine.arguments[2])
|
let exactSourceIdentifier = CommandLine.arguments[3]
|
let axWindowNumberAttribute = "AXWindowNumber"
|
|
func attr(_ element: AXUIElement, _ name: String) throws -> CFTypeRef? {
|
var value: CFTypeRef?
|
let error = AXUIElementCopyAttributeValue(element, name as CFString, &value)
|
if error == .success { return value }
|
if isBenignAbsence(error) { return nil }
|
throw AXReadFailure.nonBenign(error)
|
}
|
func text(_ element: AXUIElement, _ name: String) throws -> String? {
|
guard let value = try attr(element, name), CFGetTypeID(value) == CFStringGetTypeID() else { return nil }
|
return value as? String
|
}
|
func bool(_ element: AXUIElement, _ name: String) throws -> Bool? {
|
guard let value = try attr(element, name), CFGetTypeID(value) == CFBooleanGetTypeID() else { return nil }
|
return CFBooleanGetValue((value as! CFBoolean))
|
}
|
func number(_ element: AXUIElement, _ name: String) throws -> Int64? {
|
guard let value = try attr(element, name), CFGetTypeID(value) == CFNumberGetTypeID() else { return nil }
|
var result: Int64 = 0
|
return CFNumberGetValue((value as! CFNumber), .sInt64Type, &result) ? result : nil
|
}
|
func rect(_ element: AXUIElement) throws -> CGRect? {
|
guard let pv = try attr(element, kAXPositionAttribute), CFGetTypeID(pv) == AXValueGetTypeID(),
|
let sv = try attr(element, kAXSizeAttribute), CFGetTypeID(sv) == AXValueGetTypeID() else { return nil }
|
var point = CGPoint.zero
|
var size = CGSize.zero
|
guard AXValueGetValue(pv as! AXValue, .cgPoint, &point),
|
AXValueGetValue(sv as! AXValue, .cgSize, &size) else { return nil }
|
return CGRect(origin: point, size: size)
|
}
|
func activeDisplayBounds() -> [CGRect]? {
|
var count: UInt32 = 0
|
guard CGGetActiveDisplayList(0, nil, &count) == .success, count > 0 else { return nil }
|
var identifiers = [CGDirectDisplayID](repeating: 0, count: Int(count))
|
guard CGGetActiveDisplayList(count, &identifiers, &count) == .success else { return nil }
|
return identifiers.prefix(Int(count)).map(CGDisplayBounds)
|
}
|
func countExactSource(_ element: AXUIElement, depth: Int = 0) throws -> Int {
|
try countExactSourceCore(
|
element,
|
depth: depth,
|
depthLimit: 14,
|
matches: {
|
try text($0, kAXRoleAttribute) == (kAXTextFieldRole as String)
|
&& text($0, kAXValueAttribute) == exactSourceIdentifier
|
},
|
children: { try attr($0, kAXChildrenAttribute) as? [AXUIElement] ?? [] }
|
)
|
}
|
func readResult<Value>(_ body: () throws -> Value) -> Result<Value, AXReadFailure> {
|
do { return .success(try body()) }
|
catch let failure as AXReadFailure { return .failure(failure) }
|
catch { return .failure(.nonBenign(.failure)) }
|
}
|
func setBool(_ element: AXUIElement, _ name: String, _ value: Bool) -> AXError {
|
AXUIElementSetAttributeValue(element, name as CFString, value as CFBoolean)
|
}
|
func setPoint(_ element: AXUIElement, _ value: CGPoint) -> AXError {
|
var value = value
|
return AXUIElementSetAttributeValue(element, kAXPositionAttribute as CFString, AXValueCreate(.cgPoint, &value)!)
|
}
|
func setSize(_ element: AXUIElement, _ value: CGSize) -> AXError {
|
var value = value
|
return AXUIElementSetAttributeValue(element, kAXSizeAttribute as CFString, AXValueCreate(.cgSize, &value)!)
|
}
|
func restore(_ state: WindowState, to window: AXUIElement) -> Bool {
|
var success = true
|
if state.source {
|
success = setSize(window, CGSize(width: state.width, height: state.height)) == .success && success
|
success = setPoint(window, CGPoint(x: state.x, y: state.y)) == .success && success
|
}
|
success = setBool(window, kAXMinimizedAttribute, state.minimized) == .success && success
|
return success
|
}
|
|
guard let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else {
|
fputs("\(preStateFailureLine(.finderNotRunning))\n", stderr)
|
exit(66)
|
}
|
let app = AXUIElementCreateApplication(finder.processIdentifier)
|
let windows: [AXUIElement]
|
do {
|
guard let value = try attr(app, kAXWindowsAttribute) as? [AXUIElement] else {
|
fputs("\(preStateFailureLine(.finderWindowsReadFailed))\n", stderr)
|
exit(67)
|
}
|
windows = value
|
} catch {
|
fputs("\(preStateFailureLine(.finderWindowsReadFailed))\n", stderr)
|
exit(67)
|
}
|
|
if mode == "isolate" {
|
let matchCounts: Result<[Int], AXReadFailure>
|
do { matchCounts = .success(try windows.map { try countExactSource($0) }) }
|
catch let failure as AXReadFailure { matchCounts = .failure(failure) }
|
guard let sourceIndex = try? uniqueSourceWindow(matchCounts) else {
|
fputs("exact source must bind to exactly one Finder window\n", stderr)
|
exit(68)
|
}
|
|
let planResult = buildIsolationPlan(
|
sourceIndex: sourceIndex,
|
windowCount: windows.count,
|
windowID: { index in readResult { try number(windows[index], axWindowNumberAttribute) } },
|
rect: { index in readResult { try rect(windows[index]) } },
|
minimized: { index in readResult { try bool(windows[index], kAXMinimizedAttribute) } }
|
)
|
guard case .success(let plan) = planResult else {
|
fputs("source binding or non-source identity failed closed\n", stderr)
|
exit(69)
|
}
|
let source = windows[sourceIndex]
|
let persistence = persistStateSnapshot(
|
encode: { try JSONEncoder().encode(plan.states) },
|
atomicWrite: { try $0.write(to: stateURL, options: .atomic) }
|
)
|
if case .failure(let failure) = persistence {
|
fputs("\(preStateFailureLine(failure.stage))\n", stderr)
|
exit(70)
|
}
|
|
let otherWindowIndices = zip(plan.states, plan.windowIndices).compactMap { state, index in
|
state.source ? nil : index
|
}
|
let mutationResult = runIsolationMutationCore(
|
otherWindowCount: otherWindowIndices.count,
|
minimizeOther: { index in
|
setBool(windows[otherWindowIndices[index]], kAXMinimizedAttribute, true) == .success
|
},
|
unminimizeSource: { setBool(source, kAXMinimizedAttribute, false) == .success },
|
setSourceSize: { setSize(source, CGSize(width: 500, height: 320)) == .success },
|
setSourcePosition: { setPoint(source, CGPoint(x: 10, y: 70)) == .success },
|
raiseSource: { AXUIElementPerformAction(source, kAXRaiseAction as CFString) == .success },
|
beforeReadback: { Thread.sleep(forTimeInterval: 0.8) },
|
readback: {
|
readResult {
|
isIsolationReadbackCompatible(
|
exactUniqueBound: true,
|
minimized: try bool(source, kAXMinimizedAttribute),
|
windowRect: try rect(source),
|
displayBounds: activeDisplayBounds() ?? []
|
)
|
}
|
}
|
)
|
if case .failure(let stage) = mutationResult {
|
let failure = finishIsolationFailure(stage) {
|
restoreRecorded(plan) { state, index in restore(state, to: windows[index]) }
|
}
|
fputs("\(failure.line)\n", stderr)
|
exit(failure.exitCode)
|
}
|
print("isolated_mutable_windows=\(plan.states.count) skipped_immutable_windows=\(windows.count - plan.states.count) source=<QA_WINDOW> readback=PASS")
|
} else {
|
let states = try JSONDecoder().decode([WindowState].self, from: Data(contentsOf: stateURL))
|
let matchCounts: Result<[Int], AXReadFailure>
|
do { matchCounts = .success(try windows.map { try countExactSource($0) }) }
|
catch let failure as AXReadFailure { matchCounts = .failure(failure) }
|
let sourceIndex = try? uniqueSourceWindow(matchCounts)
|
let planResult = buildRestorePlan(
|
states: states,
|
sourceIndex: sourceIndex,
|
windowCount: windows.count,
|
windowID: { index in readResult { try number(windows[index], axWindowNumberAttribute) } },
|
sourceRect: {
|
guard let sourceIndex else { return .success(nil) }
|
return readResult { try rect(windows[sourceIndex]) }
|
},
|
sourceMinimized: {
|
guard let sourceIndex else { return .success(nil) }
|
return readResult { try bool(windows[sourceIndex], kAXMinimizedAttribute) }
|
}
|
)
|
guard case .success(let plan) = planResult else {
|
fputs("Finder restore binding failed closed\n", stderr)
|
exit(75)
|
}
|
var restored = 0
|
var failures = 0
|
for (state, index) in zip(plan.states, plan.windowIndices) {
|
if restore(state, to: windows[index]) { restored += 1 } else { failures += 1 }
|
}
|
let summary = RestoreSummary(mutableRecorded: plan.states.count, restored: restored, mutationFailures: failures)
|
print("restored=\(summary.restored) mutable_recorded=\(summary.mutableRecorded) failures=\(summary.mutationFailures)")
|
exit(summary.exitCode)
|
}
|