‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

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 }