LinuxContainer/LinuxProcess: Have wait return ExitStatus (#300)

Have wait return a struct containing the exit code and the time at which
the process actually exited. This is useful so higher level tools (like
container) don't need to craft a date themselves, and it will be more
accurate as it's taken in the guest directly after the process exits.
This commit is contained in:
Danny Canter
2025-09-29 14:09:37 -04:00
committed by GitHub
parent 511dd59e19
commit 82f2d887dd
14 changed files with 100 additions and 39 deletions
+26
View File
@@ -0,0 +1,26 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// 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 Foundation
/// ExitStatus contains the exit code for a given container process,
/// as well as the timestamp at which it exited.
public struct ExitStatus: Sendable {
/// The exit code for the process.
public var exitCode: Int32
/// The timestamp when the process exited.
public var exitedAt: Date
}
@@ -742,7 +742,7 @@ extension LinuxContainer {
/// Wait for the container to exit. Returns the exit code.
@discardableResult
public func wait(timeoutInSeconds: Int64? = nil) async throws -> Int32 {
public func wait(timeoutInSeconds: Int64? = nil) async throws -> ExitStatus {
let state = try self.state.withLock { try $0.startedState("wait") }
return try await state.process.wait(timeoutInSeconds: timeoutInSeconds)
}
+3 -3
View File
@@ -343,15 +343,15 @@ extension LinuxProcess {
/// Wait on the process to exit with an optional timeout. Returns the exit code of the process.
@discardableResult
public func wait(timeoutInSeconds: Int64? = nil) async throws -> Int32 {
public func wait(timeoutInSeconds: Int64? = nil) async throws -> ExitStatus {
do {
let code = try await self.agent.waitProcess(
let exitStatus = try await self.agent.waitProcess(
id: self.id,
containerID: self.owningContainer,
timeoutInSeconds: timeoutInSeconds
)
await self.waitIoComplete()
return code
return exitStatus
} catch {
if error is ContainerizationError {
throw error
@@ -476,9 +476,20 @@ public struct Com_Apple_Containerization_Sandbox_V3_WaitProcessResponse: Sendabl
public var exitCode: Int32 = 0
public var exitedAt: SwiftProtobuf.Google_Protobuf_Timestamp {
get {return _exitedAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()}
set {_exitedAt = newValue}
}
/// Returns true if `exitedAt` has been explicitly set.
public var hasExitedAt: Bool {return self._exitedAt != nil}
/// Clears the value of `exitedAt`. Subsequent reads from it will return its default value.
public mutating func clearExitedAt() {self._exitedAt = nil}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
fileprivate var _exitedAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil
}
public struct Com_Apple_Containerization_Sandbox_V3_ResizeProcessRequest: Sendable {
@@ -1765,6 +1776,7 @@ extension Com_Apple_Containerization_Sandbox_V3_WaitProcessResponse: SwiftProtob
public static let protoMessageName: String = _protobuf_package + ".WaitProcessResponse"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "exitCode"),
2: .standard(proto: "exited_at"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -1774,20 +1786,29 @@ extension Com_Apple_Containerization_Sandbox_V3_WaitProcessResponse: SwiftProtob
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularInt32Field(value: &self.exitCode) }()
case 2: try { try decoder.decodeSingularMessageField(value: &self._exitedAt) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if self.exitCode != 0 {
try visitor.visitSingularInt32Field(value: self.exitCode, fieldNumber: 1)
}
try { if let v = self._exitedAt {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} }()
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_WaitProcessResponse, rhs: Com_Apple_Containerization_Sandbox_V3_WaitProcessResponse) -> Bool {
if lhs.exitCode != rhs.exitCode {return false}
if lhs._exitedAt != rhs._exitedAt {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@@ -2,6 +2,8 @@ syntax = "proto3";
package com.apple.containerization.sandbox.v3;
import "google/protobuf/timestamp.proto";
// Context for interacting with a container's runtime environment.
service SandboxContext {
// Mount a filesystem.
@@ -155,6 +157,7 @@ message WaitProcessRequest {
message WaitProcessResponse {
int32 exitCode = 1;
google.protobuf.Timestamp exited_at = 2;
}
message ResizeProcessRequest {
@@ -57,7 +57,7 @@ public protocol VirtualMachineAgent: Sendable {
func startProcess(id: String, containerID: String?) async throws -> Int32
func signalProcess(id: String, containerID: String?, signal: Int32) async throws
func resizeProcess(id: String, containerID: String?, columns: UInt32, rows: UInt32) async throws
func waitProcess(id: String, containerID: String?, timeoutInSeconds: Int64?) async throws -> Int32
func waitProcess(id: String, containerID: String?, timeoutInSeconds: Int64?) async throws -> ExitStatus
func deleteProcess(id: String, containerID: String?) async throws
func closeProcessStdin(id: String, containerID: String?) async throws
@@ -183,7 +183,11 @@ extension Vminitd: VirtualMachineAgent {
_ = try await client.resizeProcess(request)
}
public func waitProcess(id: String, containerID: String?, timeoutInSeconds: Int64? = nil) async throws -> Int32 {
public func waitProcess(
id: String,
containerID: String?,
timeoutInSeconds: Int64? = nil
) async throws -> ExitStatus {
let request = Com_Apple_Containerization_Sandbox_V3_WaitProcessRequest.with {
$0.id = id
if let containerID {
@@ -198,7 +202,7 @@ extension Vminitd: VirtualMachineAgent {
}
do {
let resp = try await client.waitProcess(request, callOptions: callOpts)
return resp.exitCode
return ExitStatus(exitCode: resp.exitCode, exitedAt: resp.exitedAt.date)
} catch {
if let err = error as? GRPCError.RPCTimedOut {
throw ContainerizationError(
+15 -15
View File
@@ -37,7 +37,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
}
@@ -56,7 +56,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 1 else {
guard status.exitCode == 1 else {
throw IntegrationError.assert(msg: "process status \(status) != 1")
}
}
@@ -105,7 +105,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 1")
}
@@ -140,7 +140,7 @@ extension IntegrationSuite {
group.addTask {
try await exec.start()
let status = try await exec.wait()
if status != 0 {
if status.exitCode != 0 {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
try await exec.delete()
@@ -185,7 +185,7 @@ extension IntegrationSuite {
try await exec.start()
let status = try await exec.wait()
if status != 0 {
if status.exitCode != 0 {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -203,7 +203,7 @@ extension IntegrationSuite {
try await exec.start()
let status = try await exec.wait()
if status != 0 {
if status.exitCode != 0 {
throw IntegrationError.assert(msg: "process \(idx) status \(status) != 0")
}
@@ -247,7 +247,7 @@ extension IntegrationSuite {
var status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -271,7 +271,7 @@ extension IntegrationSuite {
status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -295,7 +295,7 @@ extension IntegrationSuite {
status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -340,7 +340,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -374,7 +374,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -409,7 +409,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -439,7 +439,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
let expected = "foo-bar"
@@ -468,7 +468,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -496,7 +496,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
let expected = "Hello from test"
+8 -8
View File
@@ -40,7 +40,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -96,7 +96,7 @@ extension IntegrationSuite {
let status = try await t.value
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -160,7 +160,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
}
@@ -195,7 +195,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -234,7 +234,7 @@ extension IntegrationSuite {
// Wait for completion
let status = try await container.wait()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -277,7 +277,7 @@ extension IntegrationSuite {
// Wait for completion
var status = try await container.wait()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
try await container.stop()
@@ -288,7 +288,7 @@ extension IntegrationSuite {
// Wait for completion.. again.
status = try await container.wait()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -332,7 +332,7 @@ extension IntegrationSuite {
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
@@ -124,7 +124,7 @@ extension ManagedContainer {
return try await ProcessSupervisor.default.start(process: proc)
}
func wait(execID: String) async throws -> Int32 {
func wait(execID: String) async throws -> ManagedProcess.ExitStatus {
let proc = try self.getExecOrInit(execID: execID)
return await proc.wait()
}
+11 -5
View File
@@ -36,14 +36,19 @@ final class ManagedProcess: Sendable {
private let bundle: ContainerizationOCI.Bundle
private let cgroupManager: Cgroup2Manager?
struct ExitStatus {
var exitStatus: Int32
var exitedAt: Date
}
private struct State {
init(io: IO) {
self.io = io
}
let io: IO
var waiters: [CheckedContinuation<Int32, Never>] = []
var exitStatus: Int32? = nil
var waiters: [CheckedContinuation<ExitStatus, Never>] = []
var exitStatus: ExitStatus? = nil
var pid: Int32 = 0
}
@@ -256,7 +261,8 @@ extension ManagedProcess {
"status": "\(status)"
])
$0.exitStatus = status
let exitStatus = ExitStatus(exitStatus: status, exitedAt: Date.now)
$0.exitStatus = exitStatus
do {
try $0.io.close()
@@ -265,7 +271,7 @@ extension ManagedProcess {
}
for waiter in $0.waiters {
waiter.resume(returning: status)
waiter.resume(returning: exitStatus)
}
self.log.debug("\($0.waiters.count) managed process waiters signaled")
@@ -274,7 +280,7 @@ extension ManagedProcess {
}
/// Wait on the process to exit
func wait() async -> Int32 {
func wait() async -> ExitStatus {
await withCheckedContinuation { cont in
self.state.withLock {
if let status = $0.exitStatus {
+4 -3
View File
@@ -24,6 +24,7 @@ import GRPC
import Logging
import NIOCore
import NIOPosix
import SwiftProtobuf
import _NIOFileSystem
private let _setenv = Foundation.setenv
@@ -665,11 +666,11 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
do {
let ctr = try await self.state.get(container: request.containerID)
let exitCode = try await ctr.wait(execID: request.id)
let exitStatus = try await ctr.wait(execID: request.id)
return .with {
$0.exitCode = exitCode
$0.exitCode = exitStatus.exitStatus
$0.exitedAt = Google_Protobuf_Timestamp(date: exitStatus.exitedAt)
}
} catch {
log.error(