From edadf155cd05e22258ab612aaca852ca75a1db13 Mon Sep 17 00:00:00 2001 From: Raj Date: Thu, 1 Jan 2026 15:10:39 +0530 Subject: [PATCH] Fix container auto-delete on rapid stop/start (#841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #833. Currently, when stopping and immediately restarting a container, it would fail with the error: `“container expected to be in created state, got: shuttingDown”` and then be automatically deleted. The `SandboxService` process waits five seconds before exiting after shutdown. During this interval, a rapid restart could reconnect to the still-terminating process in the `shuttingDown` state, triggering a state validation error. This fix forcefully terminates the `SandboxService` process with `SIGKILL` upon container exit, instead of waiting five seconds. The bootstrap now defensively checks for and cleans up any stale services before registering new ones, preventing reconnections to processes in the `shuttingDown` state. --- .../Containers/ContainersService.swift | 55 ++++++++++--------- .../SandboxService.swift | 9 --- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/Sources/Services/ContainerAPIService/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Containers/ContainersService.swift index d12d5072..40e55d0a 100644 --- a/Sources/Services/ContainerAPIService/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Containers/ContainersService.swift @@ -90,19 +90,12 @@ public actor ContainersService { ) ) results[config.id] = state - let plugin = runtimePlugins.first { $0.name == config.runtimeHandler } - guard let plugin else { + guard runtimePlugins.first(where: { $0.name == config.runtimeHandler }) != nil else { throw ContainerizationError( .internalError, message: "failed to find runtime plugin \(config.runtimeHandler)" ) } - try Self.registerService( - plugin: plugin, - loader: loader, - configuration: config, - path: dir - ) } catch { try? FileManager.default.removeItem(at: dir) log.warning("failed to load container bundle at \(dir.path)") @@ -229,10 +222,7 @@ public actor ContainersService { ) } - let runtimePlugin = self.runtimePlugins.filter { - $0.name == configuration.runtimeHandler - }.first - guard let runtimePlugin else { + guard self.runtimePlugins.first(where: { $0.name == configuration.runtimeHandler }) != nil else { throw ContainerizationError( .notFound, message: "unable to locate runtime plugin \(configuration.runtimeHandler)" @@ -255,13 +245,6 @@ public actor ContainersService { try bundle.setContainerRootFs(cloning: imageFs) try bundle.write(filename: "options.json", value: options) - try Self.registerService( - plugin: runtimePlugin, - loader: self.pluginLoader, - configuration: configuration, - path: path - ) - let snapshot = ContainerSnapshot( configuration: configuration, status: .stopped, @@ -293,6 +276,16 @@ public actor ContainersService { return } + let path = self.containerRoot.appendingPathComponent(id) + let bundle = ContainerClient.Bundle(path: path) + let config = try bundle.configuration + try Self.registerService( + plugin: self.runtimePlugins.first { $0.name == config.runtimeHandler }!, + loader: self.pluginLoader, + configuration: config, + path: path + ) + let runtime = state.snapshot.configuration.runtimeHandler let sandboxClient = try await SandboxClient.create( id: id, @@ -536,15 +529,23 @@ public actor ContainersService { await self.exitMonitor.stopTracking(id: id) - // Try and shutdown the runtime helper. - do { - self.log.info("Shutting down sandbox service for \(id)") + // Shutdown and deregister the sandbox service + self.log.info("Shutting down sandbox service for \(id)") - let client = try state.getClient() - try await client.shutdown() - } catch { - self.log.error("failed to shutdown sandbox service for \(id): \(error)") - } + let path = self.containerRoot.appendingPathComponent(id) + let bundle = ContainerClient.Bundle(path: path) + let config = try bundle.configuration + let label = Self.fullLaunchdServiceLabel( + runtimeName: config.runtimeHandler, + instanceId: id + ) + + let client = try state.getClient() + try await client.shutdown() + + // Deregister the service, launchd will terminate the process + try ServiceManager.deregister(fullServiceLabel: label) + self.log.info("Deregistered sandbox service for \(id)") state.snapshot.status = .stopped state.snapshot.networks = [] diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 9a82f3f3..f77e37cb 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -310,15 +310,6 @@ public actor SandboxService { case .created, .stopped(_), .stopping: await self.setState(.shuttingDown) - Task { - do { - try await Task.sleep(for: .seconds(5)) - } catch { - self.log.error("failed to sleep before shutting down SandboxService: \(error)") - } - self.log.info("Shutting down SandboxService") - exit(0) - } default: throw ContainerizationError( .invalidState,