Files

165 lines
5.4 KiB
Swift

import AppKit
import CoreGraphics
final class BackgroundActivationSession: @unchecked Sendable {
private final class TapContext {
let suppressFocusMessages: Bool
init(suppressFocusMessages: Bool) {
self.suppressFocusMessages = suppressFocusMessages
}
}
private struct TapRef {
let tap: CFMachPort
let source: CFRunLoopSource
let context: TapContext
}
private let previousPID: pid_t
private let targetPID: pid_t
private var taps: [TapRef] = []
private var runLoop: CFRunLoop?
private var thread: Thread?
private var startupError: Error?
private var started = false
private var lastTarget: WindowTarget?
init(previousPID: pid_t, targetPID: pid_t) {
self.previousPID = previousPID
self.targetPID = targetPID
}
deinit {
stop()
}
func start() throws {
guard !started else { return }
let ready = DispatchSemaphore(value: 0)
let thread = Thread { [weak self] in
guard let self else {
ready.signal()
return
}
self.runLoop = CFRunLoopGetCurrent()
do {
try self.installTapsOnCurrentRunLoop()
} catch {
self.startupError = error
}
ready.signal()
if self.startupError == nil {
CFRunLoopRun()
}
}
thread.name = "OpenWorkBackgroundActivationSession"
self.thread = thread
thread.start()
ready.wait()
if let startupError {
throw startupError
}
started = true
}
func activate(target: WindowTarget) async throws {
guard let windowNumber = target.windowNumber else {
throw ComputerUseError.strictModeViolation("target window has no CG window number")
}
postAppKitDefined(subtype: 1, target: target, windowNumber: windowNumber)
try await Task.sleep(nanoseconds: 25_000_000)
try await BackgroundInputDispatcher.click(pid: target.pid, windowNumber: windowNumber, point: target.center)
try await Task.sleep(nanoseconds: 80_000_000)
lastTarget = target
}
func stop() {
if let target = lastTarget, let windowNumber = target.windowNumber {
postAppKitDefined(subtype: 2, target: target, windowNumber: windowNumber)
}
for tapRef in taps {
CFMachPortInvalidate(tapRef.tap)
}
taps.removeAll()
if let runLoop {
CFRunLoopStop(runLoop)
}
runLoop = nil
thread = nil
started = false
}
private func installTapsOnCurrentRunLoop() throws {
if previousPID != targetPID {
taps.append(try installTap(pid: previousPID, suppressFocusMessages: true))
}
taps.append(try installTap(pid: targetPID, suppressFocusMessages: false))
}
private func installTap(pid: pid_t, suppressFocusMessages: Bool) throws -> TapRef {
let context = TapContext(suppressFocusMessages: suppressFocusMessages)
let userInfo = Unmanaged.passUnretained(context).toOpaque()
guard let tap = CGEvent.tapCreateForPid(
pid: pid,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask.max,
callback: BackgroundActivationSession.eventTapCallback,
userInfo: userInfo
) else {
throw ComputerUseError.strictModeViolation("could not install per-process event tap for pid \(pid)")
}
guard let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else {
CFMachPortInvalidate(tap)
throw ComputerUseError.strictModeViolation("could not create run loop source for pid \(pid) event tap")
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
return TapRef(tap: tap, source: source, context: context)
}
private func postAppKitDefined(subtype: Int16, target: WindowTarget, windowNumber: Int) {
guard let event = NSEvent.otherEvent(
with: .appKitDefined,
location: target.center,
modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: windowNumber,
context: nil,
subtype: subtype,
data1: 0,
data2: 0
)?.cgEvent else {
return
}
BackgroundInputDispatcher.address(event, pid: target.pid, windowNumber: windowNumber)
event.postToPid(target.pid)
}
private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
return Unmanaged.passUnretained(event)
}
guard let userInfo else {
return Unmanaged.passUnretained(event)
}
let context = Unmanaged<TapContext>.fromOpaque(userInfo).takeUnretainedValue()
if context.suppressFocusMessages && BackgroundActivationSession.isFocusMessage(type) {
return nil
}
return Unmanaged.passUnretained(event)
}
private static func isFocusMessage(_ type: CGEventType) -> Bool {
let raw = Int(type.rawValue)
return raw == 13 || raw == 19 || raw == 20
}
}