339 lines
9.9 KiB
Swift
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
|
|
}
|