From e7cdac90f59e1db7c0e197f26ff1676382dfeed0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 02:50:40 +0000 Subject: [PATCH] mac: stop leaking ssh processes on quit --- apps/macos/Sources/Clawdis/AgentRPC.swift | 8 +++++- apps/macos/Sources/Clawdis/MenuBar.swift | 2 ++ .../macos/Sources/Clawdis/WebChatWindow.swift | 25 +++++++++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/apps/macos/Sources/Clawdis/AgentRPC.swift b/apps/macos/Sources/Clawdis/AgentRPC.swift index f04242039..f9e79ccd9 100644 --- a/apps/macos/Sources/Clawdis/AgentRPC.swift +++ b/apps/macos/Sources/Clawdis/AgentRPC.swift @@ -234,9 +234,15 @@ actor AgentRPC { } } + func shutdown() async { + await self.stop() + } + private func stop() async { self.stdoutHandle?.readabilityHandler = nil - self.process?.terminate() + let proc = self.process + proc?.terminate() + proc?.waitUntilExit() self.process = nil self.stdinHandle = nil self.stdoutHandle = nil diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift index 5d536e7ff..e2835bc50 100644 --- a/apps/macos/Sources/Clawdis/MenuBar.swift +++ b/apps/macos/Sources/Clawdis/MenuBar.swift @@ -111,6 +111,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate func applicationWillTerminate(_ notification: Notification) { RelayProcessManager.shared.stop() PresenceReporter.shared.stop() + WebChatManager.shared.close() + Task { await AgentRPC.shared.shutdown() } } @MainActor diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index c1a61037c..b6e4d405f 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -14,6 +14,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private var baseEndpoint: URL? private let remotePort: Int private var reachabilityTask: Task? + private var tunnelRestartEnabled = false init(sessionKey: String) { webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)") @@ -46,7 +47,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { @MainActor deinit { self.reachabilityTask?.cancel() - self.tunnel?.terminate() + self.stopTunnel(allowRestart: false) } private func loadPlaceholder() { @@ -125,14 +126,16 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private func startOrRestartTunnel() async throws -> URL { // Kill existing tunnel if any - self.tunnel?.terminate() + self.stopTunnel(allowRestart: false) let tunnel = try await WebChatTunnel.create(remotePort: self.remotePort, preferredLocalPort: 18_788) self.tunnel = tunnel + self.tunnelRestartEnabled = true // Auto-restart on unexpected termination while window lives tunnel.process.terminationHandler = { [weak self] _ in guard let self else { return } + guard self.tunnelRestartEnabled else { return } webChatLogger.error("webchat tunnel terminated; restarting") Task { @MainActor [weak self] in guard let self else { return } @@ -152,6 +155,12 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { return URL(string: "http://127.0.0.1:\(port)/")! } + private func stopTunnel(allowRestart: Bool) { + self.tunnelRestartEnabled = allowRestart + self.tunnel?.terminate() + self.tunnel = nil + } + private func showError(_ text: String) { let html = """ Web chat failed to connect.

\(text) @@ -159,6 +168,11 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { self.webView.loadHTMLString(html, baseURL: nil) } + func shutdown() { + self.reachabilityTask?.cancel() + self.stopTunnel(allowRestart: false) + } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { webChatLogger.debug("didFinish navigation url=\(webView.url?.absoluteString ?? "nil", privacy: .public)") } @@ -189,6 +203,12 @@ final class WebChatManager { self.controller?.window?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } + + func close() { + self.controller?.shutdown() + self.controller?.close() + self.controller = nil + } } // MARK: - Port forwarding tunnel @@ -209,6 +229,7 @@ final class WebChatTunnel { func terminate() { if self.process.isRunning { self.process.terminate() + self.process.waitUntilExit() } }