macos: stabilize control connection wait
This commit is contained in:
@@ -13,6 +13,44 @@ struct ControlHeartbeatEvent: Codable {
|
|||||||
let reason: String?
|
let reason: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles single-shot continuation resumption without Sendable capture issues
|
||||||
|
actor ConnectionWaiter {
|
||||||
|
private var cont: CheckedContinuation<Void, Error>?
|
||||||
|
private var resumed = false
|
||||||
|
private var pendingResult: Result<Void, Error>?
|
||||||
|
|
||||||
|
func wait() async throws {
|
||||||
|
try await withCheckedThrowingContinuation { (c: CheckedContinuation<Void, Error>) in
|
||||||
|
if let pending = pendingResult {
|
||||||
|
pendingResult = nil
|
||||||
|
resumed = true
|
||||||
|
c.resume(with: pending)
|
||||||
|
} else {
|
||||||
|
cont = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func succeed() {
|
||||||
|
resume(.success(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(_ error: Error) {
|
||||||
|
resume(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resume(_ result: Result<Void, Error>) {
|
||||||
|
if resumed { return }
|
||||||
|
if let c = cont {
|
||||||
|
resumed = true
|
||||||
|
cont = nil
|
||||||
|
c.resume(with: result)
|
||||||
|
} else {
|
||||||
|
pendingResult = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ControlHealthSnapshot: Codable {
|
struct ControlHealthSnapshot: Codable {
|
||||||
struct Web: Codable {
|
struct Web: Codable {
|
||||||
let linked: Bool
|
let linked: Bool
|
||||||
@@ -169,29 +207,36 @@ final class ControlChannel: ObservableObject {
|
|||||||
let conn = NWConnection(host: host, port: port, using: .tcp)
|
let conn = NWConnection(host: host, port: port, using: .tcp)
|
||||||
self.connection = conn
|
self.connection = conn
|
||||||
|
|
||||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
let waiter = ConnectionWaiter()
|
||||||
conn.stateUpdateHandler = { [weak self, weak conn] state in
|
|
||||||
guard let self else { return }
|
conn.stateUpdateHandler = { [weak self, weak conn] state in
|
||||||
switch state {
|
switch state {
|
||||||
case .ready:
|
case .ready:
|
||||||
Task { @MainActor in self.state = .connected }
|
Task { @MainActor in self?.state = .connected }
|
||||||
|
Task {
|
||||||
|
await waiter.succeed()
|
||||||
conn?.stateUpdateHandler = nil
|
conn?.stateUpdateHandler = nil
|
||||||
cont.resume(returning: ())
|
|
||||||
case let .failed(err):
|
|
||||||
Task { @MainActor in self.state = .degraded(err.localizedDescription) }
|
|
||||||
conn?.stateUpdateHandler = nil
|
|
||||||
cont.resume(throwing: err)
|
|
||||||
case let .waiting(err):
|
|
||||||
Task { @MainActor in self.state = .degraded(err.localizedDescription) }
|
|
||||||
conn?.stateUpdateHandler = nil
|
|
||||||
cont.resume(throwing: err)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
case let .failed(err):
|
||||||
|
Task { @MainActor in self?.state = .degraded(err.localizedDescription) }
|
||||||
|
Task {
|
||||||
|
await waiter.fail(err)
|
||||||
|
conn?.stateUpdateHandler = nil
|
||||||
|
}
|
||||||
|
case let .waiting(err):
|
||||||
|
Task { @MainActor in self?.state = .degraded(err.localizedDescription) }
|
||||||
|
Task {
|
||||||
|
await waiter.fail(err)
|
||||||
|
conn?.stateUpdateHandler = nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
conn.start(queue: .global())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn.start(queue: .global())
|
||||||
|
try await waiter.wait()
|
||||||
|
|
||||||
self.listenTask = Task.detached { [weak self] in
|
self.listenTask = Task.detached { [weak self] in
|
||||||
await self?.listen()
|
await self?.listen()
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -552,26 +552,6 @@ private final class ClickCatcher: NSView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct CloseHoverButton: View {
|
|
||||||
var onClose: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: self.onClose) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.font(.system(size: 12, weight: .bold))
|
|
||||||
.foregroundColor(Color.white.opacity(0.85))
|
|
||||||
.frame(width: 22, height: 22)
|
|
||||||
.background(Color.black.opacity(0.35))
|
|
||||||
.clipShape(Circle())
|
|
||||||
.shadow(color: Color.black.opacity(0.35), radius: 6, y: 2)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.focusable(false)
|
|
||||||
.contentShape(Circle())
|
|
||||||
.padding(6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct CloseButtonOverlay: View {
|
private struct CloseButtonOverlay: View {
|
||||||
var isVisible: Bool
|
var isVisible: Bool
|
||||||
var onClose: () -> Void
|
var onClose: () -> Void
|
||||||
|
|||||||
Reference in New Issue
Block a user