native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211// ProwlCLI/Transport/SocketConnectionProbe.swift// Shared Unix socket connection diagnostics for CLI transport and app launch.
import Foundationimport ProwlCLIShared
#if canImport(Darwin) import Darwin#elseif canImport(Glibc) import Glibc#endif
enum SocketConnectionProbe { enum Status: Equatable { case connected case appUnavailable(Failure) case permissionDenied(Failure) case transportFailed(Failure)
var isConnected: Bool { self == .connected }
func exitError() -> ExitError? { switch self { case .connected: nil case .appUnavailable(let failure): ExitError(code: CLIErrorCode.appNotRunning, message: failure.appUnavailableMessage) case .permissionDenied(let failure): ExitError(code: CLIErrorCode.socketPermissionDenied, message: failure.permissionDeniedMessage) case .transportFailed(let failure): ExitError(code: CLIErrorCode.transportFailed, message: failure.transportFailedMessage) } }
var diagnosticMessage: String { switch self { case .connected: "connected" case .appUnavailable(let failure): failure.shortDiagnostic case .permissionDenied(let failure): failure.shortDiagnostic case .transportFailed(let failure): failure.shortDiagnostic } } }
struct Failure: Equatable { enum Kind: Equatable { case socketCreation(errno: Int32) case pathTooLong(maximumLength: Int) case connect(errno: Int32) }
let socketPath: String let kind: Kind
var shortDiagnostic: String { switch kind { case .socketCreation(let errorNumber): "socket creation failed (\(Self.errnoDescription(errorNumber)))" case .pathTooLong(let maximumLength): "socket path is too long (max \(maximumLength) bytes)" case .connect(let errorNumber): "connect failed (\(Self.errnoDescription(errorNumber)))" } }
var appUnavailableMessage: String { """ Cannot connect to Prowl CLI socket at \(socketPath): app is not running or the socket is stale \ (\(diagnosticDetail)). Start or restart Prowl, then retry. """ }
var permissionDeniedMessage: String { """ Cannot connect to Prowl CLI socket at \(socketPath): permission denied (\(diagnosticDetail)). \ If this command is running in a sandboxed agent, allow this Unix socket path in the sandbox profile, \ run prowl outside the sandbox, or start both Prowl and prowl with the same PROWL_CLI_SOCKET path \ that the sandbox can access. """ }
var transportFailedMessage: String { switch kind { case .pathTooLong(let maximumLength): """ Cannot connect to Prowl CLI socket at \(socketPath): socket path is too long \ (max \(maximumLength) bytes). If PROWL_CLI_SOCKET is set, choose a shorter path. """ default: """ Cannot connect to Prowl CLI socket at \(socketPath): transport failure (\(diagnosticDetail)). \ Check PROWL_CLI_SOCKET and ensure it points to Prowl's Unix socket. """ } }
private var diagnosticDetail: String { switch kind { case .socketCreation(let errorNumber), .connect(let errorNumber): Self.errnoDescription(errorNumber) case .pathTooLong(let maximumLength): "path too long; max \(maximumLength) bytes" } }
private static func errnoDescription(_ errorNumber: Int32) -> String { let name = errnoName(errorNumber) let message = strerror(errorNumber).map { String(cString: $0) } ?? "Unknown error" return "\(name): \(message)" } }
static func check(socketPath: String) -> Status { let socketFD = socket(AF_UNIX, SOCK_STREAM, 0) guard socketFD >= 0 else { return .transportFailed(Failure(socketPath: socketPath, kind: .socketCreation(errno: errno))) } defer { close(socketFD) }
return connect(socketFD: socketFD, socketPath: socketPath) }
static func connect(socketFD: Int32, socketPath: String) -> Status { let addr: sockaddr_un do { addr = try socketAddress(for: socketPath) } catch let error as SocketAddressError { switch error { case .pathTooLong(let maximumLength): return .transportFailed(Failure(socketPath: socketPath, kind: .pathTooLong(maximumLength: maximumLength))) } } catch { return .transportFailed(Failure(socketPath: socketPath, kind: .connect(errno: EINVAL))) }
let result = withUnsafePointer(to: addr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in systemConnect(socketFD, sockPtr, socklen_t(MemoryLayout<sockaddr_un>.size)) } } guard result == 0 else { return status(for: Failure(socketPath: socketPath, kind: .connect(errno: errno))) } return .connected }
private static func status(for failure: Failure) -> Status { guard case .connect(let errorNumber) = failure.kind else { return .transportFailed(failure) }
switch errorNumber { case ENOENT, ECONNREFUSED: return .appUnavailable(failure) case EPERM, EACCES: return .permissionDenied(failure) default: return .transportFailed(failure) } }
private static func socketAddress(for socketPath: String) throws -> sockaddr_un { var addr = sockaddr_un() addr.sun_family = sa_family_t(AF_UNIX) let pathBytes = Array(socketPath.utf8) let maxLength = MemoryLayout.size(ofValue: addr.sun_path) - 1 guard pathBytes.count <= maxLength else { throw SocketAddressError.pathTooLong(maximumLength: maxLength) }
withUnsafeMutableBytes(of: &addr.sun_path) { sunPathPtr in for idx in 0..<pathBytes.count { sunPathPtr[idx] = pathBytes[idx] } sunPathPtr[pathBytes.count] = 0 } return addr }
private static func errnoName(_ errorNumber: Int32) -> String { switch errorNumber { case EACCES: "EACCES" case ECONNREFUSED: "ECONNREFUSED" case EINVAL: "EINVAL" case ENOENT: "ENOENT" case ENOTSOCK: "ENOTSOCK" case EPERM: "EPERM" default: "errno \(errorNumber)" } }
private static func systemConnect(_ socketFD: Int32, _ address: UnsafePointer<sockaddr>, _ length: socklen_t) -> Int32 { #if canImport(Darwin) Darwin.connect(socketFD, address, length) #else Glibc.connect(socketFD, address, length) #endif }}
private enum SocketAddressError: Error { case pathTooLong(maximumLength: Int)}