diff --git a/Sources/Services/ContainerAPIService/Client/ClientProcess.swift b/Sources/Services/ContainerAPIService/Client/ClientProcess.swift index 5a5f8543e..5e360dcf3 100644 --- a/Sources/Services/ContainerAPIService/Client/ClientProcess.swift +++ b/Sources/Services/ContainerAPIService/Client/ClientProcess.swift @@ -36,7 +36,7 @@ public protocol ClientProcess: Sendable { func resize(_ size: Terminal.Size) async throws /// Send a signal to the process `id`. /// Kill does not wait for the process to exit, it only delivers the signal. - func kill(_ signal: Int32) async throws + func kill(_ signal: String) async throws /// Wait for the process `id` to complete and return its exit code. /// This method blocks until the process exits and the code is obtained. func wait() async throws -> Int32 @@ -76,11 +76,11 @@ struct ClientProcessImpl: ClientProcess, Sendable { } /// Send a signal to the process. - public func kill(_ signal: Int32) async throws { + public func kill(_ signal: String) async throws { let request = XPCMessage(route: .containerKill) request.set(key: .id, value: containerId) request.set(key: .processIdentifier, value: id) - request.set(key: .signal, value: Int64(signal)) + request.set(key: .signal, value: signal) try await xpcClient.send(request) } diff --git a/Sources/Services/ContainerAPIService/Client/ProcessIO.swift b/Sources/Services/ContainerAPIService/Client/ProcessIO.swift index 2af8306ab..4994461f0 100644 --- a/Sources/Services/ContainerAPIService/Client/ProcessIO.swift +++ b/Sources/Services/ContainerAPIService/Client/ProcessIO.swift @@ -30,9 +30,23 @@ public struct ProcessIO: Sendable { SIGINT, SIGUSR1, SIGUSR2, - SIGWINCH, ] + static func signalName(_ signal: Int32) -> String { + switch signal { + case SIGTERM: + "SIGTERM" + case SIGINT: + "SIGINT" + case SIGUSR1: + "SIGUSR1" + case SIGUSR2: + "SIGUSR2" + default: + "\(signal)" + } + } + public struct IoTracker: Sendable { let stream: AsyncStream let cont: AsyncStream.Continuation @@ -198,7 +212,7 @@ public struct ProcessIO: Sendable { _ = group.addTaskUnlessCancelled { for await sig in signals.signals { do { - try await process.kill(sig) + try await process.kill(Self.signalName(sig)) } catch { log.error( "failed to send signal", diff --git a/Tests/ContainerAPIClientTests/ProcessIOTests.swift b/Tests/ContainerAPIClientTests/ProcessIOTests.swift new file mode 100644 index 000000000..c18f6568f --- /dev/null +++ b/Tests/ContainerAPIClientTests/ProcessIOTests.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Darwin +import Testing + +@testable import ContainerAPIClient + +struct ProcessIOTests { + @Test("Forwarded process signals are encoded as names") + func forwardedSignalNames() { + #expect(ProcessIO.signalName(SIGTERM) == "SIGTERM") + #expect(ProcessIO.signalName(SIGINT) == "SIGINT") + #expect(ProcessIO.signalName(SIGUSR1) == "SIGUSR1") + #expect(ProcessIO.signalName(SIGUSR2) == "SIGUSR2") + } + + @Test("Terminal resize signal is handled by the resize path") + func terminalResizeSignalIsNotForwardedAsKill() { + #expect(!ProcessIO.signalSet.contains(SIGWINCH)) + } +}