From c28b100fd0c193b65f4bd1890d026978bcaea180 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 23 Oct 2026 07:09:37 -0700 Subject: [PATCH] Reconnect at once when the Mac wakes or its network changes --- mac/Sources/Byre/EventStream.swift | 25 ++++--------- mac/Sources/Byre/NetworkWatch.swift | 54 +++++++++++++++++++++++++++++ mac/Sources/Byre/Remote.swift | 19 +++++++++- 3 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 mac/Sources/Byre/NetworkWatch.swift diff --git a/mac/Sources/Byre/EventStream.swift b/mac/Sources/Byre/EventStream.swift index ea13ebc..912d3cb 100644 --- a/mac/Sources/Byre/EventStream.swift +++ b/mac/Sources/Byre/EventStream.swift @@ -1,5 +1,4 @@ import Foundation -import Network import SwiftUI private let staleAfter: TimeInterval = 40 @@ -26,8 +25,7 @@ final class EventStream { private var retry: Task? private var checker: Task? private var generation = 0 - private let monitor = NWPathMonitor() - private var online: Bool? + private var networkObserver: (any NSObjectProtocol)? private var activeObserver: NotificationCenter.ObservationToken? init(api: API, after: Int64, lead: Bool, onEvent: @escaping (ByreEvent) -> Void, onState: @escaping (Bool) -> Void, onReady: @escaping () -> Void) { @@ -43,13 +41,9 @@ final class EventStream { self?.check() } } - monitor.pathUpdateHandler = { [weak self] path in - let up = path.status == .satisfied - Task { @MainActor in - self?.pathChanged(up) - } + networkObserver = NetworkWatch.shared.observe { [weak self] in + self?.connect() } - monitor.start(queue: .main) #if os(macOS) activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in self?.check() @@ -67,21 +61,16 @@ final class EventStream { checker?.cancel() retry?.cancel() reading?.cancel() - monitor.cancel() + if let networkObserver { + NotificationCenter.default.removeObserver(networkObserver) + } + networkObserver = nil if let activeObserver { NotificationCenter.default.removeObserver(activeObserver) } activeObserver = nil } - private func pathChanged(_ up: Bool) { - let was = online - online = up - if was == false && up { - connect() - } - } - private func check() { if Date().timeIntervalSince(heard) < staleAfter { return diff --git a/mac/Sources/Byre/NetworkWatch.swift b/mac/Sources/Byre/NetworkWatch.swift new file mode 100644 index 0000000..ff2cde8 --- /dev/null +++ b/mac/Sources/Byre/NetworkWatch.swift @@ -0,0 +1,54 @@ +import Foundation +import Network +#if os(macOS) +import AppKit +#endif + +private let networkChanged = Notification.Name("run.fc.byre.networkChanged") + +@MainActor +final class NetworkWatch { + static let shared = NetworkWatch() + + private let monitor = NWPathMonitor() + private var seen = false + + private init() { + monitor.pathUpdateHandler = { [weak self] path in + let summary = "\(path.status) \(path.availableInterfaces.map(\.name).joined(separator: " "))" + Task { @MainActor in + self?.pathChanged(summary) + } + } + monitor.start(queue: .main) + #if os(macOS) + NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { [weak self] _ in + Task { @MainActor in + log.notice("woke from sleep, reconnecting") + self?.post() + } + } + #endif + } + + func observe(_ handler: @escaping @MainActor () -> Void) -> any NSObjectProtocol { + NotificationCenter.default.addObserver(forName: networkChanged, object: self, queue: .main) { _ in + MainActor.assumeIsolated { + handler() + } + } + } + + private func pathChanged(_ summary: String) { + guard seen else { + seen = true + return + } + log.notice("network changed (\(summary, privacy: .public)), reconnecting") + post() + } + + private func post() { + NotificationCenter.default.post(name: networkChanged, object: self) + } +} diff --git a/mac/Sources/Byre/Remote.swift b/mac/Sources/Byre/Remote.swift index f734a93..7048bf1 100644 --- a/mac/Sources/Byre/Remote.swift +++ b/mac/Sources/Byre/Remote.swift @@ -38,6 +38,7 @@ final class Remote { private var stopped = false private var heard = Date() private var checker: Task? + private var networkObserver: (any NSObjectProtocol)? init(api: API) { self.api = api @@ -69,16 +70,30 @@ final class Remote { self?.check() } } + networkObserver = NetworkWatch.shared.observe { [weak self] in + self?.restart() + } } } func stop() { stopped = true checker?.cancel() + if let networkObserver { + NotificationCenter.default.removeObserver(networkObserver) + } + networkObserver = nil ws?.cancel(with: .normalClosure, reason: nil) ws = nil } + private func restart() { + let old = ws + ws = nil + old?.cancel(with: .goingAway, reason: nil) + start() + } + // A socket byre closed while this Mac slept stays open here until something is sent on it. private func check() { guard let task = ws else { @@ -122,7 +137,9 @@ final class Remote { log.notice("client connection closed:\(errorText(error), privacy: .public)") self.ws = nil try? await Task.sleep(for: retryAfter) - self.start() + if self.ws == nil { + self.start() + } } } }