LinkRecovery.swift (4937B)
1 import Foundation 2 import Darwin 3 4 /// All deadlines and helper operations run independently of WindowServer's main-thread waits. 5 final class LinkRecovery { 6 private let queue = DispatchQueue(label: "Headset link recovery", qos: .userInitiated) 7 private let helper: URL 8 private let wake: (() throws -> Void)? 9 private var process: Process? 10 private var timeout: DispatchSourceTimer? 11 private var pending: DispatchWorkItem? 12 private var attempts: [TimeInterval] = [] 13 private var target: UInt32? 14 private var waiting = false 15 private var stopped = false 16 private var generation = 0 17 18 init(helper: URL, wake: (() throws -> Void)? = nil) { 19 self.helper = helper; self.wake = wake 20 } 21 22 func trackDisplay(_ id: UInt32) { 23 queue.async { self.target = id; self.cancelWaiting() } 24 } 25 26 func displayRemoved(_ id: UInt32) { 27 queue.async { if self.target == id { self.beginWaiting() } } 28 } 29 30 func displayAdded(_ id: UInt32) { 31 queue.async { self.target = id; self.cancelWaiting() } 32 } 33 34 func requestRecovery() { queue.async { self.beginWaiting() } } 35 36 private func cancelWaiting() { 37 waiting = false; generation += 1 38 pending?.cancel(); pending = nil 39 } 40 41 private func beginWaiting() { 42 guard !stopped, !waiting else { return } 43 waiting = true 44 scheduleAttempt(after: 2) 45 } 46 47 private func scheduleAttempt(after seconds: Double) { 48 let expected = generation 49 let work = DispatchWorkItem { [weak self] in 50 guard let self, !self.stopped, self.waiting, self.generation == expected else { return } 51 do { try self.wake?() } catch { 52 log("recovery: USB wake failed: \(error)") 53 self.scheduleAttempt(after: 10) 54 return 55 } 56 log("recovery: display absent after grace period; resetting DP adapter") 57 self.startReset { [weak self] _ in 58 guard let self, self.waiting, !self.stopped, self.generation == expected else { return } 59 self.scheduleAttempt(after: 10) 60 } 61 } 62 pending = work 63 queue.asyncAfter(deadline: .now() + seconds, execute: work) 64 } 65 66 func reset(onCompletion: @escaping (Bool) -> Void) { 67 queue.async { 68 self.startReset { accepted in onMainRunLoop { onCompletion(accepted) } } 69 } 70 } 71 72 /// Called only on queue. ADD, rather than helper exit status, proves video recovery. 73 private func startReset(onCompletion: @escaping (Bool) -> Void) { 74 guard !stopped, process == nil else { onCompletion(false); return } 75 let now = ProcessInfo.processInfo.systemUptime 76 attempts.removeAll { now - $0 >= 60 } 77 guard attempts.count < 3 else { 78 log("recovery rate limit: at most 3 resets per minute") 79 onCompletion(false); return 80 } 81 guard FileManager.default.isExecutableFile(atPath: helper.path) else { 82 log("recovery helper missing: run tools/build.sh (expected \(helper.path))") 83 onCompletion(false); return 84 } 85 let command = Process() 86 command.executableURL = helper 87 command.arguments = ["1", "recover"] 88 command.standardOutput = FileHandle.standardOutput 89 command.standardError = FileHandle.standardError 90 var timedOut = false 91 command.terminationHandler = { [weak self] finished in 92 guard let self else { return } 93 self.queue.async { 94 guard self.process === finished else { return } 95 self.timeout?.cancel(); self.timeout = nil 96 self.process = nil 97 let accepted = !timedOut && finished.terminationStatus == 0 98 log("recovery helper \(accepted ? "accepted DRST; waiting for display ADD" : "failed (exit \(finished.terminationStatus))")") 99 onCompletion(accepted) 100 } 101 } 102 do { 103 try command.run() 104 process = command 105 attempts.append(now) 106 let timer = DispatchSource.makeTimerSource(queue: queue) 107 timer.schedule(deadline: .now() + 3) 108 timer.setEventHandler { [weak self] in 109 guard let self, self.process === command, command.isRunning else { return } 110 timedOut = true 111 log("recovery helper exceeded 3s; terminating") 112 kill(command.processIdentifier, SIGKILL) 113 } 114 timeout = timer 115 timer.resume() 116 } catch { 117 log("cannot launch recovery helper: \(error)") 118 onCompletion(false) 119 } 120 } 121 122 func stop() { 123 queue.sync { 124 stopped = true 125 cancelWaiting() 126 timeout?.cancel(); timeout = nil 127 if let process, process.isRunning { process.terminate() } 128 process = nil 129 } 130 } 131 }