From 90dfc0de3ec1877dd47a84ba72db26293727bc2b Mon Sep 17 00:00:00 2001 From: swiftveteran Date: Sat, 27 Dec 2025 10:28:34 -0600 Subject: [PATCH] fix(xpc): prevent startup hangs with proper timeout handling (#561) - Add timeout to XPCClient.send() that cancels connection on expiry - Use .timeout error code with helpful message suggesting system start - Skip blocking default network creation during apiserver startup - Add 5-second timeouts to all NetworkClient XPC calls - Fix NetworksService init to continue on network failure, not return --- Sources/ContainerXPC/XPCClient.swift | 9 ++++++--- Sources/ContainerXPC/XPCServer.swift | 2 +- Sources/Helpers/APIServer/APIServer+Start.swift | 5 +++-- .../Networks/NetworksService.swift | 2 +- .../ContainerNetworkService/NetworkClient.swift | 13 ++++++++----- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Sources/ContainerXPC/XPCClient.swift b/Sources/ContainerXPC/XPCClient.swift index 72affece..4b20ca3a 100644 --- a/Sources/ContainerXPC/XPCClient.swift +++ b/Sources/ContainerXPC/XPCClient.swift @@ -65,14 +65,17 @@ extension XPCClient { /// Send the provided message to the service. @discardableResult public func send(_ message: XPCMessage, responseTimeout: Duration? = nil) async throws -> XPCMessage { - try await withThrowingTaskGroup(of: XPCMessage.self, returning: XPCMessage.self) { group in + // Use a more robust timeout that cancels the XPC connection if it hangs + return try await withThrowingTaskGroup(of: XPCMessage.self, returning: XPCMessage.self) { group in if let responseTimeout { group.addTask { try await Task.sleep(for: responseTimeout) + // Cancel the connection to unblock the XPC call + xpc_connection_cancel(self.connection) let route = message.string(key: XPCMessage.routeKey) ?? "nil" throw ContainerizationError( - .internalError, - message: "XPC timeout for request to \(self.service)/\(route)" + .timeout, + message: "XPC timeout after \(Int(responseTimeout.components.seconds))s for \(self.service)/\(route). Is the apiserver running? Try: container system start" ) } } diff --git a/Sources/ContainerXPC/XPCServer.swift b/Sources/ContainerXPC/XPCServer.swift index 3a21a307..96156e36 100644 --- a/Sources/ContainerXPC/XPCServer.swift +++ b/Sources/ContainerXPC/XPCServer.swift @@ -52,7 +52,7 @@ public struct XPCServer: Sendable { public func listen() async throws { let connections = AsyncStream { cont in - lock.withLock { + self.lock.withLock { xpc_connection_set_event_handler(self.connection) { object in switch xpc_get_type(object) { case XPC_TYPE_CONNECTION: diff --git a/Sources/Helpers/APIServer/APIServer+Start.swift b/Sources/Helpers/APIServer/APIServer+Start.swift index 8bf36335..9c102e71 100644 --- a/Sources/Helpers/APIServer/APIServer+Start.swift +++ b/Sources/Helpers/APIServer/APIServer+Start.swift @@ -240,12 +240,13 @@ extension APIServer { log: log ) + // Check for default network - don't block on creation during startup + // to avoid blocking if vmnet plugin fails. Users can create networks on-demand. let defaultNetwork = try await service.list() .filter { $0.id == ClientNetwork.defaultNetworkName } .first if defaultNetwork == nil { - let config = try NetworkConfiguration(id: ClientNetwork.defaultNetworkName, mode: .nat) - _ = try await service.create(configuration: config) + log.info("default network not present, can be created with 'container network create'") } let harness = NetworksHarness(service: service, log: log) diff --git a/Sources/Services/ContainerAPIService/Networks/NetworksService.swift b/Sources/Services/ContainerAPIService/Networks/NetworksService.swift index 78b8528e..3221dfb0 100644 --- a/Sources/Services/ContainerAPIService/Networks/NetworksService.swift +++ b/Sources/Services/ContainerAPIService/Networks/NetworksService.swift @@ -95,7 +95,7 @@ public actor NetworksService { "id": "\(configuration.id)", "state": "\(networkState.state)", ]) - return + continue } } } diff --git a/Sources/Services/ContainerNetworkService/NetworkClient.swift b/Sources/Services/ContainerNetworkService/NetworkClient.swift index e579f3e0..401b753d 100644 --- a/Sources/Services/ContainerNetworkService/NetworkClient.swift +++ b/Sources/Services/ContainerNetworkService/NetworkClient.swift @@ -37,11 +37,14 @@ public struct NetworkClient: Sendable { // Runtime Methods extension NetworkClient { + /// Default timeout for network XPC calls. + private static let xpcTimeout: Duration = .seconds(5) + public func state() async throws -> NetworkState { let request = XPCMessage(route: NetworkRoutes.state.rawValue) let client = createClient() - let response = try await client.send(request) + let response = try await client.send(request, responseTimeout: Self.xpcTimeout) let state = try response.state() return state } @@ -55,7 +58,7 @@ extension NetworkClient { let client = createClient() - let response = try await client.send(request) + let response = try await client.send(request, responseTimeout: Self.xpcTimeout) let attachment = try response.attachment() let additionalData = response.additionalData() return (attachment, additionalData) @@ -66,7 +69,7 @@ extension NetworkClient { request.set(key: NetworkKeys.hostname.rawValue, value: hostname) let client = createClient() - try await client.send(request) + try await client.send(request, responseTimeout: Self.xpcTimeout) } public func lookup(hostname: String) async throws -> Attachment? { @@ -75,7 +78,7 @@ extension NetworkClient { let client = createClient() - let response = try await client.send(request) + let response = try await client.send(request, responseTimeout: Self.xpcTimeout) return try response.dataNoCopy(key: NetworkKeys.attachment.rawValue).map { try JSONDecoder().decode(Attachment.self, from: $0) } @@ -86,7 +89,7 @@ extension NetworkClient { let client = createClient() - let response = try await client.send(request) + let response = try await client.send(request, responseTimeout: Self.xpcTimeout) return try response.allocatorDisabled() }