mirror of
https://github.com/apple/container.git
synced 2026-09-11 10:15:38 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+6
-2
@@ -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(
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user