2.5.2 #12

Merged
rune merged 23 commits from 2.5.2 into main 2026-08-17 08:37:04 +02:00
2 changed files with 35 additions and 0 deletions
Showing only changes of commit 5f39448896 - Show all commits
+24
View File
@@ -41,8 +41,12 @@ final class CLIServerService {
/// never needs a lock Network.framework callbacks don't run on the main thread.
private let queue = DispatchQueue(label: "com.oai.Confab.cliserver")
private var listener: NWListener?
private var isListenerReady = false
private var activeConnections: [ObjectIdentifier: NWConnection] = [:]
/// How long to wait for `NWListener` to report `.ready` before giving up. See `scheduleBindTimeout`.
private static let bindTimeout: TimeInterval = 8
static let socketPath: String = {
(("~/Library/Application Support/oAI" as NSString).expandingTildeInPath as NSString)
.appendingPathComponent("cli.sock")
@@ -78,6 +82,7 @@ final class CLIServerService {
// Remove a stale socket file left behind by an unclean shutdown (bind() fails on an
// existing path even if nothing is listening on it anymore).
try? FileManager.default.removeItem(atPath: Self.socketPath)
isListenerReady = false
let params = NWParameters()
params.defaultProtocolStack.transportProtocol = NWProtocolTCP.Options()
@@ -92,20 +97,39 @@ final class CLIServerService {
newListener.stateUpdateHandler = { [weak self] state in
switch state {
case .ready:
self?.isListenerReady = true
self?.log.info("CLI server listening at \(Self.socketPath)")
case .failed(let error):
self?.log.error("CLI server listener failed: \(error.localizedDescription)")
self?.listener = nil
default:
break
}
}
newListener.start(queue: queue)
listener = newListener
scheduleBindTimeout(for: newListener)
} catch {
log.error("Failed to start CLI server: \(error.localizedDescription)")
}
}
/// `NWListener`'s state transitions are asynchronous and normally reach `.ready` or `.failed`
/// within milliseconds but a real incident (2026-08-14) showed a state where `lsof`
/// confirmed Confab held the socket open, yet every connection attempt was refused and
/// nothing was ever logged, with no crash and no error. Whatever the exact cause, a listener
/// that never resolves either way is indistinguishable from a working one without this: it
/// bounds the wait so a stuck bind becomes a visible log line and a cleared listener slot,
/// instead of silently pretending to work forever.
private func scheduleBindTimeout(for candidate: NWListener) {
queue.asyncAfter(deadline: .now() + Self.bindTimeout) { [weak self] in
guard let self, self.listener === candidate, !self.isListenerReady else { return }
self.log.error("CLI server did not become ready within \(Int(Self.bindTimeout))s — giving up")
candidate.cancel()
self.listener = nil
}
}
// MARK: - Connection handling
private func handle(connection: NWConnection) {
+11
View File
@@ -91,6 +91,17 @@ struct oAIApp: App {
// Check for updates in the background
UpdateCheckService.shared.checkForUpdates()
// Periodic heartbeat: on its own, a quiet Confab.log is ambiguous it could mean the
// user just wasn't chatting, or that the app silently froze (confirmed possible on
// 2026-08-14: a stuck process left the CLI socket open per `lsof` yet refused every
// connection, with the log not written to at all for the rest of that session no
// crash, no error, nothing to distinguish "idle" from "wedged" after the fact). A
// regular, otherwise-meaningless log line turns that ambiguity into a plain read: if the
// *next* freeze happens, the gap since the last heartbeat pins down roughly when.
Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { _ in
Log.general.info("heartbeat")
}
}
var body: some Scene {