Files
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

1823 lines
65 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 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.
//===----------------------------------------------------------------------===//
#if os(Linux)
import Cgroup
import Containerization
import ContainerizationArchive
import ContainerizationError
import ContainerizationExtras
import ContainerizationNetlink
import ContainerizationOCI
import ContainerizationOS
import Foundation
import GRPCCore
import GRPCProtobuf
import Logging
import NIOCore
import NIOPosix
import SwiftProtobuf
import SystemPackage
private let _setenv = Foundation.setenv
#if canImport(Musl)
import Musl
private let _mount = Musl.mount
private let _umount = Musl.umount2
private let _kill = Musl.kill
private let _sync = Musl.sync
typealias _stat_struct = Musl.stat
private let _stat: @Sendable (UnsafePointer<CChar>, UnsafeMutablePointer<_stat_struct>) -> Int32 = stat
#elseif canImport(Glibc)
import Glibc
private let _mount = Glibc.mount
private let _umount = Glibc.umount2
private let _kill = Glibc.kill
private let _sync = Glibc.sync
typealias _stat_struct = Glibc.stat
private let _stat: @Sendable (UnsafePointer<CChar>, UnsafeMutablePointer<_stat_struct>) -> Int32 = stat
#endif
extension ContainerizationError {
func toRPCError(operation: String) -> RPCError {
let message = "\(operation): \(self)"
let code: RPCError.Code = {
switch self.code {
case .invalidArgument:
return .invalidArgument
case .notFound:
return .notFound
case .exists:
return .alreadyExists
case .cancelled:
return .cancelled
case .unsupported:
return .unimplemented
case .unknown:
return .unknown
case .internalError:
return .internalError
case .interrupted:
return .unavailable
case .invalidState:
return .failedPrecondition
case .timeout:
return .deadlineExceeded
default:
return .internalError
}
}()
return RPCError(code: code, message: message, cause: self)
}
}
extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServiceProtocol {
public func setTime(
request: Com_Apple_Containerization_Sandbox_V3_SetTimeRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_SetTimeResponse {
log.trace(
"setTime",
metadata: [
"sec": "\(request.sec)",
"usec": "\(request.usec)",
])
var tv = timeval(tv_sec: time_t(request.sec), tv_usec: suseconds_t(request.usec))
guard settimeofday(&tv, nil) == 0 else {
let error = swiftErrno("settimeofday")
log.error(
"setTime",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "failed to settimeofday", cause: error)
}
return .init()
}
public func setupEmulator(
request: Com_Apple_Containerization_Sandbox_V3_SetupEmulatorRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_SetupEmulatorResponse {
log.debug(
"setupEmulator",
metadata: [
"request": "\(request)"
])
if !Binfmt.mounted() {
throw RPCError(
code: .internalError,
message: "\(Binfmt.path) is not mounted"
)
}
do {
let bfmt = Binfmt.Entry(
name: request.name,
type: request.type,
offset: request.offset,
magic: request.magic,
mask: request.mask,
flags: request.flags
)
try bfmt.register(binaryPath: request.binaryPath)
} catch {
log.error(
"setupEmulator",
metadata: [
"error": "\(error)"
])
throw RPCError(
code: .internalError,
message: "setupEmulator: failed to register binfmt_misc entry",
cause: error
)
}
return .init()
}
public func sysctl(
request: Com_Apple_Containerization_Sandbox_V3_SysctlRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_SysctlResponse {
log.debug(
"sysctl",
metadata: [
"settings": "\(request.settings)"
])
do {
let sysctlPath = URL(fileURLWithPath: "/proc/sys/")
for (k, v) in request.settings {
guard let data = v.data(using: .ascii) else {
throw RPCError(code: .internalError, message: "failed to convert \(v) to data buffer for sysctl write")
}
let setting =
sysctlPath
.appendingPathComponent(k.replacingOccurrences(of: ".", with: "/"))
let fh = try FileHandle(forWritingTo: setting)
defer { try? fh.close() }
try fh.write(contentsOf: data)
}
} catch {
log.error(
"sysctl",
metadata: [
"error": "\(error)"
])
throw RPCError(
code: .internalError,
message: "sysctl: failed to set sysctl",
cause: error
)
}
return .init()
}
public func proxyVsock(
request: Com_Apple_Containerization_Sandbox_V3_ProxyVsockRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ProxyVsockResponse {
log.debug(
"proxyVsock",
metadata: [
"id": "\(request.id)",
"port": "\(request.vsockPort)",
"guestPath": "\(request.guestPath)",
"action": "\(request.action)",
])
let proxy = VsockProxy(
id: request.id,
action: request.action == .into ? .dial : .listen,
port: request.vsockPort,
path: URL(fileURLWithPath: request.guestPath),
udsPerms: request.guestSocketPermissions,
log: log
)
do {
try await proxy.start()
try await state.add(proxy: proxy)
} catch {
try? await proxy.close()
log.error(
"proxyVsock",
metadata: [
"error": "\(error)"
])
throw RPCError(
code: .internalError,
message: "proxyVsock: failed to setup vsock proxy",
cause: error
)
}
log.info(
"proxyVsock started",
metadata: [
"id": "\(request.id)",
"port": "\(request.vsockPort)",
"guestPath": "\(request.guestPath)",
])
return .init()
}
public func stopVsockProxy(
request: Com_Apple_Containerization_Sandbox_V3_StopVsockProxyRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_StopVsockProxyResponse {
log.debug(
"stopVsockProxy",
metadata: [
"id": "\(request.id)"
])
do {
let proxy = try await state.remove(proxy: request.id)
try await proxy.close()
} catch {
log.error(
"stopVsockProxy",
metadata: [
"error": "\(error)"
])
throw RPCError(
code: .internalError,
message: "stopVsockProxy: failed to stop vsock proxy",
cause: error
)
}
log.info(
"stopVsockProxy completed",
metadata: [
"id": "\(request.id)"
])
return .init()
}
public func mkdir(request: Com_Apple_Containerization_Sandbox_V3_MkdirRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_MkdirResponse
{
log.debug(
"mkdir",
metadata: [
"path": "\(request.path)",
"all": "\(request.all)",
])
do {
try FileManager.default.createDirectory(
atPath: request.path,
withIntermediateDirectories: request.all
)
} catch {
log.error(
"mkdir",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "mkdir", cause: error)
}
return .init()
}
public func writeFile(request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_WriteFileResponse
{
log.debug(
"writeFile",
metadata: [
"path": "\(request.path)",
"mode": "\(request.mode)",
"dataSize": "\(request.data.count)",
])
do {
if request.flags.createParentDirs {
let fileURL = URL(fileURLWithPath: request.path)
let parentDir = fileURL.deletingLastPathComponent()
try FileManager.default.createDirectory(
at: parentDir,
withIntermediateDirectories: true
)
}
var flags = O_WRONLY
if request.flags.createIfMissing {
flags |= O_CREAT
}
if request.flags.append {
flags |= O_APPEND
}
let mode = request.mode > 0 ? mode_t(request.mode) : mode_t(0644)
let fd = open(request.path, flags, mode)
guard fd != -1 else {
let error = swiftErrno("open")
throw RPCError(
code: .internalError,
message: "writeFile: failed to open file",
cause: error
)
}
let fh = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
try fh.write(contentsOf: request.data)
} catch {
log.error(
"writeFile",
metadata: [
"error": "\(error)"
])
if error is RPCError {
throw error
}
throw RPCError(
code: .internalError,
message: "writeFile",
cause: error
)
}
return .init()
}
public func stat(
request: Com_Apple_Containerization_Sandbox_V3_StatRequest,
context: GRPCCore.ServerContext,
) async throws -> Com_Apple_Containerization_Sandbox_V3_StatResponse {
log.debug(
"stat",
metadata: [
"path": "\(request.path)"
]
)
#if os(Linux)
var s = _stat_struct()
let result = _stat(request.path, &s)
if result == -1 {
let error = swiftErrno("stat")
if error.code == .ENOENT {
throw RPCError(
code: .notFound,
message: "stat: path not found '\(request.path)'",
cause: error
)
}
return .with { $0.error = "\(error)" }
}
return .with {
$0.stat = .with {
$0.dev = UInt64(s.st_dev)
$0.ino = UInt64(s.st_ino)
$0.mode = s.st_mode
$0.nlink = UInt64(s.st_nlink)
$0.uid = s.st_uid
$0.gid = s.st_gid
$0.rdev = UInt64(s.st_rdev)
$0.size = Int64(s.st_size)
$0.blksize = Int64(s.st_blksize)
$0.blocks = Int64(s.st_blocks)
$0.atime = .with {
$0.seconds = Int64(s.st_atim.tv_sec)
$0.nanos = Int32(s.st_atim.tv_nsec)
}
$0.mtime = .with {
$0.seconds = Int64(s.st_mtim.tv_sec)
$0.nanos = Int32(s.st_mtim.tv_nsec)
}
$0.ctime = .with {
$0.seconds = Int64(s.st_ctim.tv_sec)
$0.nanos = Int32(s.st_ctim.tv_nsec)
}
}
}
#else
fatalError("stat not supported on platform")
#endif
}
// Chunk size for streaming file transfers (1MB).
private static let copyChunkSize = 1024 * 1024
public func copy(
request: Com_Apple_Containerization_Sandbox_V3_CopyRequest,
response: GRPCCore.RPCWriter<Com_Apple_Containerization_Sandbox_V3_CopyResponse>,
context: GRPCCore.ServerContext
) async throws {
let path = request.path
let vsockPort = request.vsockPort
log.debug(
"copy",
metadata: [
"direction": "\(request.direction)",
"path": "\(path)",
"vsockPort": "\(vsockPort)",
"isArchive": "\(request.isArchive)",
"mode": "\(request.mode)",
"createParents": "\(request.createParents)",
])
do {
switch request.direction {
case .copyIn:
try await handleCopyIn(request: request, response: response)
case .copyOut:
try await handleCopyOut(request: request, response: response)
case .UNRECOGNIZED(let value):
throw RPCError(code: .invalidArgument, message: "copy: unrecognized direction \(value)")
}
} catch {
log.error(
"copy failed",
metadata: [
"direction": "\(request.direction)",
"path": "\(path)",
"error": "\(error)",
])
if error is RPCError {
throw error
}
throw RPCError(code: .internalError, message: "copy failed", cause: error)
}
}
/// Handle a COPY_IN request: connect to host vsock port, read data, write to guest filesystem.
private func handleCopyIn(
request: Com_Apple_Containerization_Sandbox_V3_CopyRequest,
response: GRPCCore.RPCWriter<Com_Apple_Containerization_Sandbox_V3_CopyResponse>
) async throws {
let path = request.path
let isArchive = request.isArchive
if request.createParents {
let parentDir = URL(fileURLWithPath: path).deletingLastPathComponent()
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true)
}
// Connect to the host's vsock port for data transfer.
let vsockType = VsockType(port: request.vsockPort, cid: VsockType.hostCID)
let sock = try Socket(type: vsockType, closeOnDeinit: false)
try sock.connect()
let sockFd = sock.fileDescriptor
// Dispatch blocking I/O onto the thread pool.
let rejected: [String] = try await blockingPool.runIfActive { [self] in
defer { try? sock.close() }
guard isArchive else {
let mode = request.mode > 0 ? mode_t(request.mode) : mode_t(0o644)
let fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
guard fd != -1 else {
throw RPCError(
code: .internalError,
message: "copy: failed to open file '\(path)': \(swiftErrno("open"))"
)
}
defer { close(fd) }
var buf = [UInt8](repeating: 0, count: Self.copyChunkSize)
while true {
let n = read(sockFd, &buf, buf.count)
if n == 0 { break }
guard n > 0 else {
throw RPCError(
code: .internalError,
message: "copy: vsock read error: \(swiftErrno("read"))"
)
}
var written = 0
while written < n {
let w = buf.withUnsafeBytes { ptr in
write(fd, ptr.baseAddress! + written, n - written)
}
guard w > 0 else {
throw RPCError(
code: .internalError,
message: "copy: write error: \(swiftErrno("write"))"
)
}
written += w
}
}
return []
}
let destURL = URL(fileURLWithPath: path)
try FileManager.default.createDirectory(at: destURL, withIntermediateDirectories: true)
let fileHandle = FileHandle(fileDescriptor: sockFd, closeOnDealloc: false)
let reader = try ArchiveReader(format: .pax, filter: .gzip, fileHandle: fileHandle)
return try reader.extractContents(to: destURL)
}
if !rejected.isEmpty {
log.info("copy: archive extracted", metadata: ["path": "\(path)", "rejectedCount": "\(rejected.count)"])
for rejectedPath in rejected {
log.error("copy: rejected archive path", metadata: ["path": "\(rejectedPath)"])
}
}
log.debug("copy: copyIn complete", metadata: ["path": "\(path)", "isArchive": "\(isArchive)"])
// Send completion response.
try await response.write(.with { $0.status = .complete })
}
/// Handle a COPY_OUT request: stat path, send metadata, connect to host vsock port, write data.
private func handleCopyOut(
request: Com_Apple_Containerization_Sandbox_V3_CopyRequest,
response: GRPCCore.RPCWriter<Com_Apple_Containerization_Sandbox_V3_CopyResponse>
) async throws {
let path = request.path
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else {
throw RPCError(code: .notFound, message: "copy: path not found '\(path)'")
}
let isArchive = isDirectory.boolValue
// Determine total size for single files.
var totalSize: UInt64 = 0
if !isArchive {
let attrs = try FileManager.default.attributesOfItem(atPath: path)
if let size = attrs[.size] as? UInt64 {
totalSize = size
}
}
// Send metadata response BEFORE connecting to vsock, so host knows what to expect.
try await response.write(
.with {
$0.status = .metadata
$0.isArchive = isArchive
$0.totalSize = totalSize
})
// Connect to the host's vsock port and dispatch blocking I/O onto the thread pool.
let vsockType = VsockType(port: request.vsockPort, cid: VsockType.hostCID)
let sock = try Socket(type: vsockType, closeOnDeinit: false)
try sock.connect()
try await blockingPool.runIfActive { [self] in
defer { try? sock.close() }
if isArchive {
let fileURL = URL(fileURLWithPath: path)
let writer = try ArchiveWriter(configuration: .init(format: .pax, filter: .gzip))
try writer.open(fileDescriptor: sock.fileDescriptor)
try writer.archiveDirectory(fileURL)
try writer.finishEncoding()
} else {
let srcFd = open(path, O_RDONLY)
guard srcFd != -1 else {
throw RPCError(
code: .internalError,
message: "copy: failed to open '\(path)': \(swiftErrno("open"))"
)
}
defer { close(srcFd) }
var buf = [UInt8](repeating: 0, count: Self.copyChunkSize)
while true {
let n = read(srcFd, &buf, buf.count)
if n == 0 { break }
guard n > 0 else {
throw RPCError(
code: .internalError,
message: "copy: read error: \(swiftErrno("read"))"
)
}
var written = 0
while written < n {
let w = buf.withUnsafeBytes { ptr in
write(sock.fileDescriptor, ptr.baseAddress! + written, n - written)
}
guard w > 0 else {
throw RPCError(
code: .internalError,
message: "copy: vsock write error: \(swiftErrno("write"))"
)
}
written += w
}
}
}
}
log.debug(
"copy: copyOut complete",
metadata: [
"path": "\(path)",
"isArchive": "\(isArchive)",
])
// Send completion response after vsock data transfer is done.
try await response.write(.with { $0.status = .complete })
}
public func mount(request: Com_Apple_Containerization_Sandbox_V3_MountRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_MountResponse
{
log.debug(
"mount",
metadata: [
"type": "\(request.type)",
"source": "\(request.source)",
"destination": "\(request.destination)",
])
do {
let mnt = ContainerizationOS.Mount(
type: request.type,
source: request.source,
target: request.destination,
options: request.options
)
#if os(Linux)
try mnt.mount(createWithPerms: 0o755)
return .init()
#else
fatalError("mount not supported on platform")
#endif
} catch {
log.error(
"mount",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "mount", cause: error)
}
}
public func filesystemOperation(request: Com_Apple_Containerization_Sandbox_V3_FilesystemOperationRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_FilesystemOperationResponse
{
let path = FilePath(request.path)
log.debug(
"filesystemOperation",
metadata: [
"operation": "\(String(describing: request.operation))",
"path": "\(path)",
])
if !path.isAbsolute {
throw RPCError(code: .invalidArgument, message: "path must be absolute")
}
var finfo = _stat_struct()
let rc = _stat(path.string, &finfo)
if rc != 0 {
let error = swiftErrno("stat")
throw RPCError(code: .notFound, message: "failed to stat path", cause: error)
}
let fd = open(path.string, O_RDONLY | O_NOFOLLOW)
if fd < 0 {
if errno == ELOOP {
throw RPCError(code: .internalError, message: "path cannot be a symlink")
}
let error = swiftErrno("open")
throw RPCError(code: .internalError, message: "failed to open path", cause: error)
}
defer { close(fd) }
do {
switch request.operation {
case .freeze:
try freezeFilesystem(fd: fd)
case .thaw:
try thawFilesystem(fd: fd)
case .trim(let params):
switch params.schedule {
case .oneShot:
try trimFilesystem(fd: fd)
case .none:
throw RPCError(code: .invalidArgument, message: "trim schedule must be specified")
}
case .none:
throw RPCError(code: .invalidArgument, message: "invalid operation")
}
} catch {
log.error(
"filesystemOperation",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "filesystemOperation", cause: error)
}
return .init()
}
private func freezeFilesystem(fd: Int32) throws {
let FIFREEZE: UInt = 0xC004_5877
let rc: CInt = ioctl(fd, FIFREEZE, 0)
if rc != 0 {
let error = swiftErrno("ioctl(FIFREEZE)")
throw RPCError(code: .internalError, message: "freeze failed", cause: error)
}
}
private func thawFilesystem(fd: Int32) throws {
let FITHAW: UInt = 0xC004_5878
let rc: CInt = ioctl(fd, FITHAW, 0)
if rc != 0 {
let error = swiftErrno("ioctl(FITHAW)")
throw RPCError(code: .internalError, message: "thaw failed", cause: error)
}
}
private struct fitrim_range {
var start: UInt64
var len: UInt64
var min_len: UInt64
}
private func trimFilesystem(fd: Int32) throws {
let FITRIM: UInt = 0xC018_5879
var trange = fitrim_range(start: 0, len: UInt64.max, min_len: 0)
let rc: CInt = ioctl(fd, FITRIM, &trange)
if rc != 0 {
let error = swiftErrno("ioctl(FITRIM)")
throw RPCError(code: .internalError, message: "trim failed", cause: error)
}
}
public func umount(request: Com_Apple_Containerization_Sandbox_V3_UmountRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_UmountResponse
{
log.debug(
"umount",
metadata: [
"path": "\(request.path)",
"flags": "\(request.flags)",
])
#if os(Linux)
// Best effort EBUSY handle.
for _ in 0...50 {
let result = _umount(request.path, request.flags)
if result == -1 {
if errno == EBUSY {
try await Task.sleep(for: .milliseconds(10))
continue
}
let error = swiftErrno("umount")
log.error(
"umount",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .invalidArgument, message: "umount", cause: error)
}
break
}
return .init()
#else
fatalError("umount not supported on platform")
#endif
}
public func setenv(request: Com_Apple_Containerization_Sandbox_V3_SetenvRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_SetenvResponse
{
log.debug(
"setenv",
metadata: [
"key": "\(request.key)",
"value": "\(request.value)",
])
guard _setenv(request.key, request.value, 1) == 0 else {
let error = swiftErrno("setenv")
log.error(
"setEnv",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .invalidArgument, message: "setenv", cause: error)
}
return .init()
}
public func getenv(request: Com_Apple_Containerization_Sandbox_V3_GetenvRequest, context: GRPCCore.ServerContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_GetenvResponse
{
log.debug(
"getenv",
metadata: [
"key": "\(request.key)"
])
let env = ProcessInfo.processInfo.environment[request.key]
return .with {
if let env {
$0.value = env
}
}
}
public func createProcess(
request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse {
log.debug(
"createProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"stdin": "Port: \(request.stdin)",
"stdout": "Port: \(request.stdout)",
"stderr": "Port: \(request.stderr)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
var ociSpec = try JSONDecoder().decode(
ContainerizationOCI.Spec.self,
from: request.configuration
)
try ociAlterations(id: request.id, ociSpec: &ociSpec)
guard let process = ociSpec.process else {
throw ContainerizationError(
.invalidArgument,
message: "oci runtime spec missing process configuration"
)
}
let stdioPorts = HostStdio(
stdin: request.hasStdin ? request.stdin : nil,
stdout: request.hasStdout ? request.stdout : nil,
stderr: request.hasStderr ? request.stderr : nil,
terminal: process.terminal
)
// This is an exec.
if let container = await self.state.containers[request.containerID] {
try await container.createExec(
id: request.id,
stdio: stdioPorts,
process: process
)
} else {
// We need to make our new fangled container.
// The process ID must match the container ID for this.
guard request.id == request.containerID else {
throw ContainerizationError(
.invalidArgument,
message: "init process id must match container id"
)
}
// Write the etc/hostname file in the container rootfs since some init-systems
// depend on it.
let hostname = ociSpec.hostname
if let root = ociSpec.root, !hostname.isEmpty {
let etc = URL(fileURLWithPath: root.path).appendingPathComponent("etc")
try FileManager.default.createDirectory(atPath: etc.path, withIntermediateDirectories: true)
let hostnamePath = etc.appendingPathComponent("hostname")
try hostname.write(toFile: hostnamePath.path, atomically: true, encoding: .utf8)
}
let ctr = try await ManagedContainer(
id: request.id,
stdio: stdioPorts,
spec: ociSpec,
ociRuntimePath: request.hasOciRuntimePath ? request.ociRuntimePath : nil,
log: self.log
)
try await self.state.add(container: ctr)
}
return .init()
} catch let err as ContainerizationError {
log.error(
"createProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "createProcess: failed to create process")
} catch {
log.error(
"createProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
if error is RPCError {
throw error
}
throw RPCError(code: .internalError, message: "createProcess: \(error)", cause: error)
}
}
public func killProcess(
request: Com_Apple_Containerization_Sandbox_V3_KillProcessRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_KillProcessResponse {
log.debug(
"killProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"signal": "\(request.signal)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
let ctr = try await self.state.get(container: request.containerID)
try await ctr.kill(execID: request.id, request.signal)
return .init()
} catch let err as ContainerizationError {
log.error(
"killProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "killProcess: failed to kill process")
} catch {
log.error(
"killProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
throw RPCError(code: .internalError, message: "killProcess: failed to kill process: \(error)")
}
}
public func deleteProcess(
request: Com_Apple_Containerization_Sandbox_V3_DeleteProcessRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_DeleteProcessResponse {
log.debug(
"deleteProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
let ctr = try await self.state.get(container: request.containerID)
// Are we trying to delete the container itself?
if request.id == request.containerID {
try await ctr.delete()
try await state.remove(container: request.id)
} else {
// Or just a single exec.
try await ctr.deleteExec(id: request.id)
}
return .init()
} catch let err as ContainerizationError {
log.error(
"deleteProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "deleteProcess: failed to delete process")
} catch {
log.error(
"deleteProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
throw RPCError(
code: .internalError,
message: "deleteProcess: \(error)"
)
}
}
public func startProcess(
request: Com_Apple_Containerization_Sandbox_V3_StartProcessRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_StartProcessResponse {
log.debug(
"startProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
let ctr = try await self.state.get(container: request.containerID)
let pid = try await ctr.start(execID: request.id)
return .with {
$0.pid = pid
}
} catch let err as ContainerizationError {
log.error(
"startProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "startProcess: failed to start process")
} catch {
log.error(
"startProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
throw RPCError(
code: .internalError,
message: "startProcess: failed to start process",
cause: error
)
}
}
public func resizeProcess(
request: Com_Apple_Containerization_Sandbox_V3_ResizeProcessRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ResizeProcessResponse {
log.debug(
"resizeProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
let ctr = try await self.state.get(container: request.containerID)
let size = Terminal.Size(
width: UInt16(request.columns),
height: UInt16(request.rows)
)
try await ctr.resize(execID: request.id, size: size)
} catch let err as ContainerizationError {
log.error(
"resizeProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "resizeProcess: failed to resize process")
} catch {
log.error(
"resizeProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
throw RPCError(
code: .internalError,
message: "resizeProcess: failed to resize process",
cause: error
)
}
return .init()
}
public func waitProcess(
request: Com_Apple_Containerization_Sandbox_V3_WaitProcessRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_WaitProcessResponse {
log.debug(
"waitProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
let ctr = try await self.state.get(container: request.containerID)
let exitStatus = try await ctr.wait(execID: request.id)
return .with {
$0.exitCode = exitStatus.exitCode
$0.exitedAt = Google_Protobuf_Timestamp(date: exitStatus.exitedAt)
}
} catch let err as ContainerizationError {
log.error(
"waitProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "waitProcess: failed to wait on process")
} catch {
log.error(
"waitProcess",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
throw RPCError(
code: .internalError,
message: "waitProcess: failed to wait on process",
cause: error
)
}
}
public func closeProcessStdin(
request: Com_Apple_Containerization_Sandbox_V3_CloseProcessStdinRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_CloseProcessStdinResponse {
log.debug(
"closeProcessStdin",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
])
do {
if !request.hasContainerID {
throw ContainerizationError(
.invalidArgument,
message: "processes in the root of the vm not implemented"
)
}
let ctr = try await self.state.get(container: request.containerID)
try await ctr.closeStdin(execID: request.id)
return .init()
} catch let err as ContainerizationError {
log.error(
"closeProcessStdin",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(err)",
])
throw err.toRPCError(operation: "closeProcessStdin: failed to close process stdin")
} catch {
log.error(
"closeProcessStdin",
metadata: [
"id": "\(request.id)",
"containerID": "\(request.containerID)",
"error": "\(error)",
])
throw RPCError(
code: .internalError,
message: "closeProcessStdin: failed to close process stdin",
cause: error
)
}
}
public func ipLinkSet(
request: Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_IpLinkSetResponse {
log.debug(
"ipLinkSet",
metadata: [
"interface": "\(request.interface)",
"up": "\(request.up)",
])
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
let mtuValue: UInt32? = request.hasMtu ? request.mtu : nil
try session.linkSet(interface: request.interface, up: request.up, mtu: mtuValue)
} catch {
log.error(
"ipLinkSet",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "ipLinkSet", cause: error)
}
return .init()
}
public func ipAddrAdd(
request: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_IpAddrAddResponse {
log.debug(
"ipAddrAdd",
metadata: [
"interface": "\(request.interface)",
"ipv4Address": "\(request.ipv4Address)",
"ipv6Address": "\(request.hasIpv6Address ? request.ipv6Address : "<none>")",
])
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
let ipv4Address = try CIDRv4(request.ipv4Address)
try session.addressAdd(interface: request.interface, ipv4Address: ipv4Address)
if request.hasIpv6Address {
// Suppress SLAAC on this interface before adding the static
// address: the host would provide a static IPv6 config, this
// auto-derived IPv6 config would compete with the static one.
let confPath = URL(fileURLWithPath: "/proc/sys/net/ipv6/conf/\(request.interface)")
for key in ["accept_ra", "autoconf"] {
let setting = confPath.appendingPathComponent(key)
do {
let fh = try FileHandle(forWritingTo: setting)
defer { try? fh.close() }
try fh.write(contentsOf: Data("0".utf8))
} catch {
log.warning(
"ipAddrAdd: failed to disable IPv6 auto-configuration",
metadata: [
"path": "\(setting.path)",
"error": "\(error)",
])
}
}
let ipv6Address = try CIDRv6(request.ipv6Address)
try session.addressAdd(interface: request.interface, ipv6Address: ipv6Address)
}
} catch {
log.error(
"ipAddrAdd",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "ipAddrAdd", cause: error)
}
return .init()
}
public func ipRouteAddLink(
request: Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest, context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkResponse {
log.debug(
"ipRouteAddLink",
metadata: [
"interface": "\(request.interface)",
"dstIpv4Addr": "\(request.dstIpv4Addr)",
"srcIpv4Addr": "\(request.srcIpv4Addr)",
"dstIpv6Addr": "\(request.hasDstIpv6Addr ? request.dstIpv6Addr : "<none>")",
"srcIpv6Addr": "\(request.hasSrcIpv6Addr ? request.srcIpv6Addr : "<none>")",
])
guard !request.dstIpv4Addr.isEmpty || request.hasDstIpv6Addr else {
throw RPCError(
code: .invalidArgument,
message: "ipRouteAddLink requires at least one of dstIpv4Addr or dstIpv6Addr"
)
}
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
if !request.dstIpv4Addr.isEmpty {
let dstIpv4Addr = try CIDRv4(request.dstIpv4Addr)
let srcIpv4Addr = request.srcIpv4Addr.isEmpty ? nil : try IPv4Address(request.srcIpv4Addr)
try session.routeAdd(
interface: request.interface,
dstIpv4Addr: dstIpv4Addr,
srcIpv4Addr: srcIpv4Addr
)
}
if request.hasDstIpv6Addr {
let dstIpv6Addr = try CIDRv6(request.dstIpv6Addr)
let srcIpv6Addr = request.hasSrcIpv6Addr ? try IPv6Address(request.srcIpv6Addr) : nil
try session.routeAdd(
interface: request.interface,
dstIpv6Addr: dstIpv6Addr,
srcIpv6Addr: srcIpv6Addr
)
}
} catch {
log.error(
"ipRouteAddLink",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "ipRouteAddLink", cause: error)
}
return .init()
}
public func ipRouteAddDefault(
request: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultResponse {
log.debug(
"ipRouteAddDefault",
metadata: [
"interface": "\(request.interface)",
"ipv4Gateway": "\(request.ipv4Gateway)",
"ipv6Gateway": "\(request.hasIpv6Gateway ? request.ipv6Gateway : "<none>")",
])
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
if !request.ipv4Gateway.isEmpty {
let ipv4Gateway = try IPv4Address(request.ipv4Gateway)
try session.routeAddDefault(interface: request.interface, ipv4Gateway: ipv4Gateway)
} else if !request.hasIpv6Gateway {
// No v4 gateway and no v6 either: install a v4 default route
// with no gateway (preserves pre-IPv6 behavior).
try session.routeAddDefault(interface: request.interface, ipv4Gateway: nil)
}
if request.hasIpv6Gateway {
let ipv6Gateway = try IPv6Address(request.ipv6Gateway)
try session.routeAddDefault(interface: request.interface, ipv6Gateway: ipv6Gateway)
}
} catch {
log.error(
"ipRouteAddDefault",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "ipRouteAddDefault", cause: error)
}
return .init()
}
public func configureDns(
request: Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse {
let domain = request.hasDomain ? request.domain : nil
log.debug(
"configureDns",
metadata: [
"location": "\(request.location)",
"nameservers": "\(request.nameservers)",
"domain": "\(domain ?? "")",
"searchDomains": "\(request.searchDomains)",
"options": "\(request.options)",
])
do {
let etc = URL(fileURLWithPath: request.location).appendingPathComponent("etc")
try FileManager.default.createDirectory(atPath: etc.path, withIntermediateDirectories: true)
let resolvConf = etc.appendingPathComponent("resolv.conf")
let config = DNS(
nameservers: request.nameservers,
domain: domain,
searchDomains: request.searchDomains,
options: request.options
)
let text = config.resolvConf
log.debug("writing to path \(resolvConf.path) \(text)")
try text.write(toFile: resolvConf.path, atomically: true, encoding: .utf8)
log.debug("wrote resolver configuration", metadata: ["path": "\(resolvConf.path)"])
} catch {
log.error(
"configureDns",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "configureDns", cause: error)
}
return .init()
}
public func configureHosts(
request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse {
log.debug(
"configureHosts",
metadata: [
"location": "\(request.location)"
])
do {
let etc = URL(fileURLWithPath: request.location).appendingPathComponent("etc")
try FileManager.default.createDirectory(atPath: etc.path, withIntermediateDirectories: true)
let hostsPath = etc.appendingPathComponent("hosts")
let config = request.toCZHosts()
let text = config.hostsFile
try text.write(toFile: hostsPath.path, atomically: true, encoding: .utf8)
log.debug("wrote /etc/hosts configuration", metadata: ["path": "\(hostsPath.path)"])
} catch {
log.error(
"configureHosts",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "configureHosts", cause: error)
}
return .init()
}
public func containerStatistics(
request: Com_Apple_Containerization_Sandbox_V3_ContainerStatisticsRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ContainerStatisticsResponse {
log.debug(
"containerStatistics",
metadata: [
"container_ids": "\(request.containerIds)",
"categories": "\(request.categories)",
])
do {
// Parse requested categories (empty = all)
let categories = Set(request.categories)
let wantAll = categories.isEmpty
let wantProcess = wantAll || categories.contains(.process)
let wantMemory = wantAll || categories.contains(.memory)
let wantCPU = wantAll || categories.contains(.cpu)
let wantBlockIO = wantAll || categories.contains(.blockIo)
let wantNetwork = wantAll || categories.contains(.network)
let wantMemoryEvents = wantAll || categories.contains(.memoryEvents)
// Get all network interfaces (skip loopback) only if needed
let interfaces = wantNetwork ? try getNetworkInterfaces() : []
// Get containers to query
let containerIDs: [String]
if request.containerIds.isEmpty {
containerIDs = await Array(state.containers.keys)
} else {
containerIDs = request.containerIds
}
var containerStats: [Com_Apple_Containerization_Sandbox_V3_ContainerStats] = []
for containerID in containerIDs {
let container = try await state.get(container: containerID)
// Only read the cgroup stat groups that were requested.
var cgCategories: Cgroup2StatsCategory = []
if wantProcess { cgCategories.insert(.pids) }
if wantMemory { cgCategories.insert(.memory) }
if wantCPU { cgCategories.insert(.cpu) }
if wantBlockIO { cgCategories.insert(.io) }
let cgStats: Cgroup2Stats? = cgCategories.isEmpty ? nil : try await container.stats(cgCategories)
// Get network stats only if requested
var networkStats: [Com_Apple_Containerization_Sandbox_V3_NetworkStats] = []
if wantNetwork {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
for interface in interfaces {
let responses = try session.linkGet(interface: interface, includeStats: true)
if responses.count == 1, let stats = try responses[0].getStatistics() {
networkStats.append(
.with {
$0.interface = interface
$0.receivedPackets = stats.rxPackets
$0.transmittedPackets = stats.txPackets
$0.receivedBytes = stats.rxBytes
$0.transmittedBytes = stats.txBytes
$0.receivedErrors = stats.rxErrors
$0.transmittedErrors = stats.txErrors
})
}
}
}
// Get memory events only if requested
var memoryEvents: MemoryEvents?
if wantMemoryEvents {
memoryEvents = try await container.getMemoryEvents()
}
containerStats.append(
mapStatsToProto(
containerID: containerID,
cgStats: cgStats,
networkStats: networkStats,
memoryEvents: memoryEvents,
wantProcess: wantProcess,
wantMemory: wantMemory,
wantCPU: wantCPU,
wantBlockIO: wantBlockIO,
wantNetwork: wantNetwork,
wantMemoryEvents: wantMemoryEvents
)
)
}
return .with {
$0.containers = containerStats
}
} catch {
log.error(
"containerStatistics",
metadata: [
"error": "\(error)"
])
throw RPCError(code: .internalError, message: "containerStatistics", cause: error)
}
}
private func swiftErrno(_ msg: Logger.Message) -> POSIXError {
let error = POSIXError(.init(rawValue: errno)!)
log.error(
msg,
metadata: [
"error": "\(error)"
])
return error
}
// NOTE: This is just crummy. It works because today the assumption is
// every NIC in the root net namespace is for the container(s), but if we
// ever supported individual containers having their own NICs/IPs then this
// logic needs to change. We only create ethernet devices today too, so that's
// what this filters for as well.
private func getNetworkInterfaces() throws -> [String] {
let netPath = URL(filePath: "/sys/class/net")
let interfaces = try FileManager.default.contentsOfDirectory(
at: netPath,
includingPropertiesForKeys: nil
)
return
interfaces
.map { $0.lastPathComponent }
.filter { $0.hasPrefix("eth") }
}
private func mapStatsToProto(
containerID: String,
cgStats: Cgroup2Stats?,
networkStats: [Com_Apple_Containerization_Sandbox_V3_NetworkStats],
memoryEvents: MemoryEvents?,
wantProcess: Bool,
wantMemory: Bool,
wantCPU: Bool,
wantBlockIO: Bool,
wantNetwork: Bool,
wantMemoryEvents: Bool
) -> Com_Apple_Containerization_Sandbox_V3_ContainerStats {
.with {
$0.containerID = containerID
if wantProcess, let pids = cgStats?.pids {
$0.process = .with {
$0.current = pids.current
$0.limit = pids.max ?? 0
}
}
if wantMemory, let memory = cgStats?.memory {
$0.memory = .with {
$0.usageBytes = memory.usage
$0.limitBytes = memory.usageLimit ?? 0
$0.swapUsageBytes = memory.swapUsage ?? 0
$0.swapLimitBytes = memory.swapLimit ?? 0
$0.cacheBytes = memory.file
$0.kernelStackBytes = memory.kernelStack
$0.slabBytes = memory.slab
$0.pageFaults = memory.pgfault
$0.majorPageFaults = memory.pgmajfault
$0.inactiveFile = memory.inactiveFile
$0.anon = memory.anon
$0.workingsetRefaultAnon = memory.workingsetRefaultAnon
$0.workingsetRefaultFile = memory.workingsetRefaultFile
$0.pgstealKswapd = memory.pgstealKswapd
$0.pgstealDirect = memory.pgstealDirect
$0.pgstealKhugepaged = memory.pgstealKhugepaged
}
}
if wantCPU, let cpu = cgStats?.cpu {
$0.cpu = .with {
$0.usageUsec = cpu.usageUsec
$0.userUsec = cpu.userUsec
$0.systemUsec = cpu.systemUsec
$0.throttlingPeriods = cpu.nrPeriods
$0.throttledPeriods = cpu.nrThrottled
$0.throttledTimeUsec = cpu.throttledUsec
}
}
if wantBlockIO, let io = cgStats?.io {
$0.blockIo = .with {
$0.devices = io.entries.map { entry in
.with {
$0.major = entry.major
$0.minor = entry.minor
$0.readBytes = entry.rbytes
$0.writeBytes = entry.wbytes
$0.readOperations = entry.rios
$0.writeOperations = entry.wios
}
}
}
}
if wantNetwork {
$0.networks = networkStats
}
if wantMemoryEvents, let events = memoryEvents {
$0.memoryEvents = .with {
$0.low = events.low
$0.high = events.high
$0.max = events.max
$0.oom = events.oom
$0.oomKill = events.oomKill
}
}
}
}
public func sync(
request: Com_Apple_Containerization_Sandbox_V3_SyncRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_SyncResponse {
log.debug("sync")
_sync()
return .init()
}
public func kill(
request: Com_Apple_Containerization_Sandbox_V3_KillRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_KillResponse {
log.debug(
"kill",
metadata: [
"pid": "\(request.pid)",
"signal": "\(request.signal)",
])
let r = _kill(request.pid, request.signal)
return .with {
$0.result = r
}
}
}
extension Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest {
func toCZHosts() -> Hosts {
let entries = self.entries.map {
Hosts.Entry(
ipAddress: $0.ipAddress,
hostnames: $0.hostnames,
comment: $0.hasComment ? $0.comment : nil
)
}
return Hosts(
entries: entries,
comment: self.hasComment ? self.comment : nil
)
}
}
extension Initd {
func ociAlterations(id: String, ociSpec: inout ContainerizationOCI.Spec) throws {
guard var process = ociSpec.process else {
throw ContainerizationError(
.invalidArgument,
message: "runtime spec without process field present"
)
}
guard let root = ociSpec.root else {
throw ContainerizationError(
.invalidArgument,
message: "runtime spec without root field present"
)
}
if ociSpec.linux!.cgroupsPath.isEmpty {
ociSpec.linux!.cgroupsPath = "/container/\(id)"
}
if process.cwd.isEmpty {
process.cwd = "/"
}
// NOTE: The OCI runtime specs Username field is truthfully Windows exclusive, but we use this as a way
// to pass through the exact string representation of a username (or username:group, uid:group etc.) a client
// may have given us.
let username = process.user.username.isEmpty ? "\(process.user.uid):\(process.user.gid)" : process.user.username
let parsedUser = try User.getExecUser(
userString: username,
passwdPath: URL(filePath: root.path).appending(path: "etc/passwd"),
groupPath: URL(filePath: root.path).appending(path: "etc/group")
)
process.user.uid = parsedUser.uid
process.user.gid = parsedUser.gid
process.user.additionalGids.append(contentsOf: parsedUser.sgids)
process.user.additionalGids.append(process.user.gid)
var seenSuppGids = Set<UInt32>()
process.user.additionalGids = process.user.additionalGids.filter {
seenSuppGids.insert($0).inserted
}
if !process.env.contains(where: { $0.hasPrefix("PATH=") }) {
process.env.append("PATH=\(LinuxProcessConfiguration.defaultPath)")
}
if !process.env.contains(where: { $0.hasPrefix("HOME=") }) {
process.env.append("HOME=\(parsedUser.home)")
}
// Defensive programming a tad, but ensure we have TERM set if
// the client requested a pty.
if process.terminal {
let termEnv = "TERM="
if !process.env.contains(where: { $0.hasPrefix(termEnv) }) {
process.env.append("TERM=xterm")
}
}
ociSpec.process = process
}
}
#endif