diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift index 604edb68b..5ed309c37 100644 --- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift @@ -138,7 +138,15 @@ final class GatewayProcessManager: ObservableObject { if let snap = decodeHealthSnapshot(from: data) { let linked = snap.web.linked ? "linked" : "not linked" let authAge = snap.web.authAgeMs.flatMap(msToAge) ?? "unknown age" - details = "port \(port), \(linked), auth \(authAge)" + let instance = await PortGuardian.shared.describe(port: port) + let instanceText: String + if let instance { + let path = instance.executablePath ?? "path unknown" + instanceText = "pid \(instance.pid) \(instance.command) @ \(path)" + } else { + instanceText = "pid unknown" + } + details = "port \(port), \(linked), auth \(authAge), \(instanceText)" } else { details = "port \(port), health probe succeeded" } diff --git a/apps/macos/Sources/Clawdis/PortGuardian.swift b/apps/macos/Sources/Clawdis/PortGuardian.swift index 75a8059f9..cdb6be0ee 100644 --- a/apps/macos/Sources/Clawdis/PortGuardian.swift +++ b/apps/macos/Sources/Clawdis/PortGuardian.swift @@ -1,5 +1,8 @@ import Foundation import OSLog +#if canImport(Darwin) +import Darwin +#endif actor PortGuardian { static let shared = PortGuardian() @@ -12,6 +15,12 @@ actor PortGuardian { let timestamp: TimeInterval } + struct Descriptor: Sendable { + let pid: Int32 + let command: String + let executablePath: String? + } + private var records: [Record] = [] private let logger = Logger(subsystem: "com.steipete.clawdis", category: "portguard") nonisolated private static let appSupportDir: URL = { @@ -63,6 +72,12 @@ actor PortGuardian { } } + func describe(port: Int) async -> Descriptor? { + guard let listener = await self.listeners(on: port).first else { return nil } + let path = Self.executablePath(for: listener.pid) + return Descriptor(pid: listener.pid, command: listener.command, executablePath: path) + } + // MARK: - Internals private struct Listener { @@ -112,6 +127,17 @@ actor PortGuardian { return listeners } + private static func executablePath(for pid: Int32) -> String? { + #if canImport(Darwin) + var buffer = [CChar](repeating: 0, count: Int(PATH_MAX)) + let length = proc_pidpath(pid, &buffer, UInt32(buffer.count)) + guard length > 0 else { return nil } + return String(cString: buffer) + #else + return nil + #endif + } + private func kill(_ pid: Int32) async -> Bool { let term = await ShellExecutor.run(command: ["kill", "-TERM", "\(pid)"], cwd: nil, env: nil, timeout: 2) if term.ok { return true }