mac: stop leaking ssh processes on quit

This commit is contained in:
Peter Steinberger
2025-12-09 02:50:40 +00:00
parent 7aefcab8b0
commit e7cdac90f5
3 changed files with 32 additions and 3 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -14,6 +14,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
private var baseEndpoint: URL?
private let remotePort: Int
private var reachabilityTask: Task<Void, Never>?
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 = """
<html><body style='font-family:-apple-system;padding:24px;color:#c00'>Web chat failed to connect.<br><br>\(text)</body></html>
@@ -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()
}
}