Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Crisp/Models/DDCOperationQueuePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class DDCOperationQueuePool: @unchecked Sendable {
/// Parks every per-display queue, present and future, until the returned closure
/// runs; `onIdle` fires once queued work drains, so the caller knows I2C is free.
/// Each queue gives up after `timeout` so a lost release cannot wedge DDC.
func hold(timeout: TimeInterval, onIdle: @escaping () -> Void) -> () -> Void {
func hold(timeout: TimeInterval, onIdle: @escaping () -> Void) -> @Sendable () -> Void {
let hold = Hold(timeout: timeout)
lock.lock()
activeHolds.append(hold)
Expand Down
21 changes: 21 additions & 0 deletions Crisp/Models/DisplayConfigurationGate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

/// Unlike the UI timeout, this lease ends only when the blocking transaction finishes.
final class DisplayConfigurationGate: @unchecked Sendable {
private let lock = NSLock()
private var pending = false

var isPending: Bool { lock.withLock { pending } }

func begin() -> Bool {
lock.withLock {
guard !pending else { return false }
pending = true
return true
}
}

func finish() {
lock.withLock { pending = false }
}
}
19 changes: 19 additions & 0 deletions Crisp/Models/DisplayConnectionQueue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@MainActor
final class DisplayConnectionQueue {
private var tail: Task<Void, Never>?
private var generation: UInt64 = 0

func run<Output: Sendable>(_ operation: @escaping @MainActor () async -> Output) async -> Output {
let previous = tail
let task = Task { @MainActor in
await previous?.value
return await operation()
}
generation &+= 1
let requestGeneration = generation
tail = Task { @MainActor in _ = await task.value }
let result = await task.value
if generation == requestGeneration { tail = nil }
return result
}
}
34 changes: 33 additions & 1 deletion Crisp/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,22 @@
}
}
},
"Display configuration is still in progress. Wait for it to finish." : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示器配置仍在进行,请等待完成。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "顯示器設定仍在進行,請等待完成。"
}
}
}
},
"Display configuration timed out." : {
"localizations" : {
"zh-Hans" : {
Expand Down Expand Up @@ -1706,6 +1722,22 @@
}
}
},
"Display Connection" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示器连接"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "顯示器連線"
}
}
}
},
"Display: %@" : {
"localizations" : {
"zh-Hans" : {
Expand Down Expand Up @@ -4303,4 +4335,4 @@
}
},
"version" : "1.0"
}
}
5 changes: 4 additions & 1 deletion Crisp/Services/CGHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ enum CGHelpers {
static func runWithTimeout<T: Sendable>(
seconds: Double,
fallback: T,
onOperationFinished: (@Sendable (T, Bool) -> Void)? = nil,
operation: @escaping @Sendable () -> T
) async -> T {
await withCheckedContinuation { cont in
let once = ResumeOnce()

DispatchQueue.global(qos: .userInitiated).async {
let result = operation()
if once.claim() { cont.resume(returning: result) }
let completedInTime = once.claim()
onOperationFinished?(result, !completedInTime)
if completedInTime { cont.resume(returning: result) }
}

DispatchQueue.global().asyncAfter(deadline: .now() + seconds) {
Expand Down
2 changes: 1 addition & 1 deletion Crisp/Services/DDCService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ final class DDCService: ObservableObject, @unchecked Sendable {
/// cap). Do not skip this around a display enable/disable: WindowServer's enable waits
/// behind an in-flight I2C transaction and freezes the whole Mac with it. See
/// docs/ddc-notes.md (the hold around enable and disable).
func hold() async -> () -> Void {
func hold() async -> @Sendable () -> Void {
let idle = DispatchSemaphore(value: 0)
let release = operationQueues.hold(timeout: 15) { idle.signal() }
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
Expand Down
18 changes: 14 additions & 4 deletions Crisp/Services/InputSwitchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,23 @@ final class InputSwitchService: ObservableObject {
let returnInput = toggle.disconnected.first { $0.uuid == uuid }?.returnInput
let result = await toggle.reconnect(uuid: uuid)
guard case .success = result, let returnInput else { return result }
// One write: the monitor re-links to this Mac (a 1.5 s hot plug drop) when it lands.
guard let displayID = Self.onlineDisplayID(uuid: uuid) else { return result }
let written = await DDCService.shared.writeInputAfterReconnect(displayID: displayID, value: returnInput)
Self.log.notice("display \(uuid, privacy: .public) reconnected, input \(returnInput, privacy: .public) written: \(written, privacy: .public)")
await restoreInputAfterReconnect(uuid: uuid, input: returnInput)
return result
}

func restoreInputAfterReconnect(uuid: String, input: UInt16) async {
var target: CGDirectDisplayID?
for _ in 0..<20 {
target = Self.onlineDisplayID(uuid: uuid)
if target != nil { break }
try? await Task.sleep(nanoseconds: 100_000_000)
}
// One write: the monitor re-links to this Mac (a 1.5 s hot plug drop) when it lands.
guard let displayID = target else { return }
let written = await DDCService.shared.writeInputAfterReconnect(displayID: displayID, value: input)
Self.log.notice("display \(uuid, privacy: .public) reconnected, input \(input, privacy: .public) written: \(written, privacy: .public)")
}

// MARK: - Private

private static func onlineDisplayID(uuid: String) -> CGDirectDisplayID? {
Expand Down
Loading