Files
apple--containerization/Sources/ContainerizationOS/Command.swift
T
wehub-resource-sync 680845cb1c
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

339 lines
9.9 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 CShim
import Foundation
import Synchronization
#if canImport(Darwin)
import Darwin
private let _kill = Darwin.kill
#elseif canImport(Musl)
import Musl
private let _kill = Musl.kill
#elseif canImport(Glibc)
import Glibc
private let _kill = Glibc.kill
#endif
/// Use a command to run an executable.
public struct Command: Sendable {
/// Path to the executable binary.
public var executable: String
/// Arguments provided to the binary.
public var arguments: [String]
/// Environment variables for the process.
public var environment: [String]
/// The directory where the process should execute.
public var directory: String?
/// Additional files to pass to the process.
public var extraFiles: [FileHandle]
/// The standard input.
public var stdin: FileHandle?
/// The standard output.
public var stdout: FileHandle?
/// The standard error.
public var stderr: FileHandle?
private let state: State
/// System level attributes to set on the process.
public struct Attrs: Sendable {
/// Set pgroup for the new process.
public var setPGroup: Bool
/// Make the new process group the foreground process group (requires setPGroup).
public var setForegroundPGroup: Bool
/// Inherit the real uid/gid of the parent.
public var resetIDs: Bool
/// Reset the child's signal handlers to the default.
public var setSignalDefault: Bool
/// The initial signal mask for the process.
public var signalMask: UInt32
/// Create a new session for the process.
public var setsid: Bool
/// Set the controlling terminal for the process to fd 0.
public var setctty: Bool
/// Set the process user ID.
public var uid: UInt32?
/// Set the process group ID.
public var gid: UInt32?
/// Signal to send when parent process dies (Linux only).
public var pdeathSignal: Int32?
public init(
setPGroup: Bool = false,
setForegroundPGroup: Bool = false,
resetIDs: Bool = false,
setSignalDefault: Bool = true,
signalMask: UInt32 = 0,
setsid: Bool = false,
setctty: Bool = false,
uid: UInt32? = nil,
gid: UInt32? = nil,
pdeathSignal: Int32? = nil
) {
self.setPGroup = setPGroup
self.setForegroundPGroup = setForegroundPGroup
self.resetIDs = resetIDs
self.setSignalDefault = setSignalDefault
self.signalMask = signalMask
self.setsid = setsid
self.setctty = setctty
self.uid = uid
self.gid = gid
self.pdeathSignal = pdeathSignal
}
}
private final class State: Sendable {
let pid: Atomic<pid_t> = Atomic(-1)
}
/// Attributes to set on the process.
public var attrs = Attrs()
/// System level process identifier.
public var pid: Int32 { self.state.pid.load(ordering: .acquiring) }
public init(
_ executable: String,
arguments: [String] = [],
environment: [String] = environment(),
directory: String? = nil,
extraFiles: [FileHandle] = []
) {
self.executable = executable
self.arguments = arguments
self.environment = environment
self.extraFiles = extraFiles
self.directory = directory
self.state = State()
}
public static func environment() -> [String] {
ProcessInfo.processInfo.environment
.map { "\($0)=\($1)" }
}
}
extension Command {
public enum Error: Swift.Error, CustomStringConvertible {
case processRunning
public var description: String {
switch self {
case .processRunning:
return "the process is already running"
}
}
}
}
extension Command {
@discardableResult
public func kill(_ signal: Int32) -> Int32? {
let pid = self.pid
guard pid > 0 else {
return nil
}
return _kill(pid, signal)
}
}
extension Command {
/// Start the process.
public func start() throws {
guard self.pid == -1 else {
throw Error.processRunning
}
let child = try execute()
self.state.pid.store(child, ordering: .releasing)
}
/// Wait for the process to exit and return the exit status.
@discardableResult
public func wait() throws -> Int32 {
var rus = rusage()
var ws = Int32()
let pid = self.pid
guard pid > 0 else {
return -1
}
let result = wait4(pid, &ws, 0, &rus)
guard result == pid else {
throw POSIXError(.init(rawValue: errno)!)
}
return Self.toExitStatus(ws)
}
private func execute() throws -> pid_t {
var attrs = exec_command_attrs()
exec_command_attrs_init(&attrs)
let set = try createFileset()
defer {
for nullHandle in set.nullHandles {
try? nullHandle.close()
}
}
var fds = [Int32](repeating: 0, count: set.handles.count)
for (i, handle) in set.handles.enumerated() {
fds[i] = handle.fileDescriptor
}
attrs.setsid = self.attrs.setsid ? 1 : 0
attrs.setctty = self.attrs.setctty ? 1 : 0
attrs.setpgid = self.attrs.setPGroup ? 1 : 0
attrs.setfgpgrp = self.attrs.setForegroundPGroup ? 1 : 0
var cwdPath: UnsafeMutablePointer<CChar>?
if let chdir = self.directory {
cwdPath = strdup(chdir)
}
defer {
if let cwdPath {
free(cwdPath)
}
}
if let uid = self.attrs.uid {
attrs.uid = uid
}
if let gid = self.attrs.gid {
attrs.gid = gid
}
if let pdeathSignal = self.attrs.pdeathSignal {
attrs.pdeathSignal = pdeathSignal
}
var pid: pid_t = 0
var argv = ([executable] + arguments).map { strdup($0) } + [nil]
defer {
for arg in argv where arg != nil {
free(arg)
}
}
let env = environment.map { strdup($0) } + [nil]
defer {
for e in env where e != nil {
free(e)
}
}
let result = fds.withUnsafeBufferPointer { file_handles in
exec_command(
&pid,
argv[0],
&argv,
env,
file_handles.baseAddress!, Int32(file_handles.count),
cwdPath ?? nil,
&attrs)
}
guard result == 0 else {
throw POSIXError(.init(rawValue: errno)!)
}
return pid
}
/// Create a posix_spawn file actions set of fds to pass to the new process
private func createFileset() throws -> (nullHandles: [FileHandle], handles: [FileHandle]) {
// grab dev null handles for different purposes
let nullRead = try openDevNull(flags: O_RDONLY)
let nullWrite = try openDevNull(flags: O_WRONLY)
var files = [FileHandle]()
files.append(stdin ?? nullRead)
files.append(stdout ?? nullWrite)
files.append(stderr ?? nullWrite)
files.append(contentsOf: extraFiles)
return (nullHandles: [nullRead, nullWrite], handles: files)
}
/// Returns a file handle to /dev/null with the specified flags.
private func openDevNull(flags: Int32) throws -> FileHandle {
let fd = open("/dev/null", flags, 0)
guard fd >= 0 else {
throw POSIXError(.init(rawValue: errno)!)
}
return FileHandle(fileDescriptor: fd, closeOnDealloc: false)
}
}
extension Command {
private static let signalOffset: Int32 = 128
private static let shift: Int32 = 8
private static let mask: Int32 = 0x7F
private static let stopped: Int32 = 0x7F
private static let exited: Int32 = 0x00
static func signaled(_ ws: Int32) -> Bool {
ws & mask != stopped && ws & mask != exited
}
static func exited(_ ws: Int32) -> Bool {
ws & mask == exited
}
static func exitStatus(_ ws: Int32) -> Int32 {
let r: Int32
#if os(Linux)
r = ws >> shift & 0xFF
#else
r = ws >> shift
#endif
return r
}
public static func toExitStatus(_ ws: Int32) -> Int32 {
if signaled(ws) {
// We use the offset as that is how existing container
// runtimes minic bash for the status when signaled.
return Int32(Self.signalOffset + ws & mask)
}
if exited(ws) {
return exitStatus(ws)
}
return ws
}
}
private func WIFEXITED(_ status: Int32) -> Bool {
_WSTATUS(status) == 0
}
private func _WSTATUS(_ status: Int32) -> Int32 {
status & 0x7f
}
private func WIFSIGNALED(_ status: Int32) -> Bool {
(_WSTATUS(status) != 0) && (_WSTATUS(status) != 0x7f)
}
private func WEXITSTATUS(_ status: Int32) -> Int32 {
(status >> 8) & 0xff
}
private func WTERMSIG(_ status: Int32) -> Int32 {
status & 0x7f
}