Reorganize runtime targets for scalable plugin support (#1577)

## Type of Change
- [x] Bug fix
- [ ] New feature  
- [ ] Breaking change
- [ ] Documentation update

## Motivation and Context
Closes #1569

Separates generic runtime infrastructure from Linux-specific
implementation into distinct SPM targets:

- ContainerRuntimeClient — generic runtime plugin client API
- ContainerRuntimeLinuxClient — Linux-specific types
- ContainerRuntimeLinuxServer — Linux runtime service + interface
strategy implementations

Renames types to reflect the runtime/sandbox distinction:
- SandboxClient → RuntimeClient
- SandboxService → RuntimeService
- SandboxRoutes → RuntimeRoutes
- SandboxKeys → RuntimeKeys

Breaking for external consumers:
- ContainerSandboxService removed, replaced by ContainerRuntimeClient +
ContainerRuntimeLinuxServer
- Public types renamed

## Testing
- [x] Tested locally
- [ ] Added/updated tests
- [ ] Added/updated docs
This commit is contained in:
AJ Emory
2026-05-20 12:57:10 -07:00
committed by GitHub
parent 4f097eb891
commit afeceb05d4
20 changed files with 217 additions and 196 deletions
+3 -2
View File
@@ -5,8 +5,9 @@ builder:
documentation_targets:
- ContainerAPIService
- ContainerAPIClient
- ContainerSandboxService
- ContainerSandboxServiceClient
- ContainerRuntimeClient
- ContainerRuntimeLinuxClient
- ContainerRuntimeLinuxServer
- ContainerNetworkService
- ContainerNetworkServiceClient
- ContainerImagesService
+21 -16
View File
@@ -35,11 +35,13 @@ let package = Package(
.library(name: "ContainerAPIClient", targets: ["ContainerAPIClient"]),
.library(name: "ContainerImagesService", targets: ["ContainerImagesService", "ContainerImagesServiceClient"]),
.library(name: "ContainerNetworkService", targets: ["ContainerNetworkService", "ContainerNetworkServiceClient"]),
.library(name: "ContainerSandboxService", targets: ["ContainerSandboxService", "ContainerSandboxServiceClient"]),
.library(name: "ContainerResource", targets: ["ContainerResource"]),
.library(name: "ContainerLog", targets: ["ContainerLog"]),
.library(name: "ContainerPersistence", targets: ["ContainerPersistence"]),
.library(name: "ContainerPlugin", targets: ["ContainerPlugin"]),
.library(name: "ContainerRuntimeClient", targets: ["ContainerRuntimeClient"]),
.library(name: "ContainerRuntimeLinuxClient", targets: ["ContainerRuntimeLinuxClient"]),
.library(name: "ContainerRuntimeLinuxServer", targets: ["ContainerRuntimeLinuxServer"]),
.library(name: "ContainerVersion", targets: ["ContainerVersion"]),
.library(name: "ContainerXPC", targets: ["ContainerXPC"]),
.library(name: "ContainerOS", targets: ["ContainerOS"]),
@@ -108,7 +110,8 @@ let package = Package(
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
"ContainerRuntimeLinuxTypes",
"ContainerRuntimeClient",
"ContainerRuntimeLinuxClient",
"ContainerVersion",
"ContainerXPC",
"TerminalProgress",
@@ -188,7 +191,7 @@ let package = Package(
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
"ContainerSandboxServiceClient",
"ContainerRuntimeClient",
"ContainerVersion",
"ContainerXPC",
"TerminalProgress",
@@ -200,8 +203,8 @@ let package = Package(
dependencies: [
.product(name: "Containerization", package: "containerization"),
"ContainerResource",
"ContainerRuntimeLinuxTypes",
"ContainerSandboxServiceClient",
"ContainerRuntimeLinuxClient",
"ContainerRuntimeClient",
]
),
.target(
@@ -336,9 +339,9 @@ let package = Package(
path: "Sources/Services/ContainerNetworkService/Client"
),
.target(
name: "ContainerRuntimeLinuxTypes",
name: "ContainerRuntimeLinuxClient",
dependencies: [],
path: "Sources/Plugins/RuntimeLinux/Types"
path: "Sources/Services/RuntimeLinux/Client"
),
.executableTarget(
name: "container-runtime-linux",
@@ -349,41 +352,43 @@ let package = Package(
"ContainerLog",
"ContainerPlugin",
"ContainerResource",
"ContainerRuntimeLinuxTypes",
"ContainerSandboxService",
"ContainerSandboxServiceClient",
"ContainerRuntimeClient",
"ContainerRuntimeLinuxClient",
"ContainerRuntimeLinuxServer",
"ContainerVersion",
"ContainerXPC",
],
path: "Sources/Plugins/RuntimeLinux",
exclude: ["config.toml", "Types"]
exclude: ["config.toml"]
),
.target(
name: "ContainerSandboxService",
name: "ContainerRuntimeLinuxServer",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"ContainerAPIClient",
"ContainerNetworkServiceClient",
"ContainerOS",
"ContainerPersistence",
"ContainerResource",
"ContainerSandboxServiceClient",
"ContainerRuntimeClient",
"ContainerRuntimeLinuxClient",
"ContainerXPC",
"SocketForwarder",
],
path: "Sources/Services/ContainerSandboxService/Server"
path: "Sources/Services/RuntimeLinux/Server"
),
.target(
name: "ContainerSandboxServiceClient",
name: "ContainerRuntimeClient",
dependencies: [
"ContainerAPIClient",
"ContainerResource",
"ContainerXPC",
],
path: "Sources/Services/ContainerSandboxService/Client"
path: "Sources/Services/Runtime/RuntimeClient"
),
.target(
name: "ContainerResource",
@@ -18,8 +18,8 @@ import ArgumentParser
import ContainerLog
import ContainerPlugin
import ContainerResource
import ContainerSandboxService
import ContainerSandboxServiceClient
import ContainerRuntimeClient
import ContainerRuntimeLinuxServer
import ContainerXPC
import Foundation
import Logging
@@ -74,7 +74,7 @@ extension RuntimeLinuxHelper {
log.info("configuring XPC server")
nonisolated(unsafe) let anonymousConnection = xpc_connection_create(nil, nil)
let server = SandboxService(
let server = RuntimeService(
root: .init(fileURLWithPath: root),
interfaceStrategies: interfaceStrategies,
eventLoopGroup: eventLoopGroup,
@@ -85,7 +85,7 @@ extension RuntimeLinuxHelper {
let endpointServer = XPCServer(
identifier: machServiceLabel,
routes: [
SandboxRoutes.createEndpoint.rawValue: XPCServer.route(server.createEndpoint)
RuntimeRoutes.createEndpoint.rawValue: XPCServer.route(server.createEndpoint)
],
log: log
)
@@ -93,19 +93,19 @@ extension RuntimeLinuxHelper {
let mainServer = XPCServer(
connection: anonymousConnection,
routes: [
SandboxRoutes.bootstrap.rawValue: XPCServer.route(server.bootstrap),
SandboxRoutes.createProcess.rawValue: XPCServer.route(server.createProcess),
SandboxRoutes.state.rawValue: XPCServer.route(server.state),
SandboxRoutes.stop.rawValue: XPCServer.route(server.stop),
SandboxRoutes.kill.rawValue: XPCServer.route(server.kill),
SandboxRoutes.resize.rawValue: XPCServer.route(server.resize),
SandboxRoutes.wait.rawValue: XPCServer.route(server.wait),
SandboxRoutes.start.rawValue: XPCServer.route(server.startProcess),
SandboxRoutes.dial.rawValue: XPCServer.route(server.dial),
SandboxRoutes.shutdown.rawValue: XPCServer.route(server.shutdown),
SandboxRoutes.statistics.rawValue: XPCServer.route(server.statistics),
SandboxRoutes.copyIn.rawValue: XPCServer.route(server.copyIn),
SandboxRoutes.copyOut.rawValue: XPCServer.route(server.copyOut),
RuntimeRoutes.bootstrap.rawValue: XPCServer.route(server.bootstrap),
RuntimeRoutes.createProcess.rawValue: XPCServer.route(server.createProcess),
RuntimeRoutes.state.rawValue: XPCServer.route(server.state),
RuntimeRoutes.stop.rawValue: XPCServer.route(server.stop),
RuntimeRoutes.kill.rawValue: XPCServer.route(server.kill),
RuntimeRoutes.resize.rawValue: XPCServer.route(server.resize),
RuntimeRoutes.wait.rawValue: XPCServer.route(server.wait),
RuntimeRoutes.start.rawValue: XPCServer.route(server.startProcess),
RuntimeRoutes.dial.rawValue: XPCServer.route(server.dial),
RuntimeRoutes.shutdown.rawValue: XPCServer.route(server.shutdown),
RuntimeRoutes.statistics.rawValue: XPCServer.route(server.statistics),
RuntimeRoutes.copyIn.rawValue: XPCServer.route(server.copyIn),
RuntimeRoutes.copyOut.rawValue: XPCServer.route(server.copyOut),
],
log: log
)
@@ -19,7 +19,7 @@ import ContainerAPIClient
import ContainerPersistence
import ContainerPlugin
import ContainerResource
import ContainerSandboxServiceClient
import ContainerRuntimeClient
import ContainerXPC
import Containerization
import ContainerizationEXT4
@@ -34,11 +34,11 @@ import SystemPackage
public actor ContainersService {
struct ContainerState {
var snapshot: ContainerSnapshot
var client: SandboxClient? = nil
var client: RuntimeClient? = nil
func getClient() throws -> SandboxClient {
func getClient() throws -> RuntimeClient {
guard let client else {
var message = "no sandbox client exists"
var message = "no runtime client exists"
if snapshot.status == .stopped {
message += ": container is stopped"
}
@@ -335,7 +335,7 @@ public actor ContainersService {
// to boot. We can go lower, but this is a somewhat safe threshold. Containerization
// also gives a little bit extra than the user asked for to account for guest agent overhead.
//
// NOTE: We could potentially leave this validation to the sandbox service(s), as
// NOTE: We could potentially leave this validation to the runtime service(s), as
// it's possible there could be an implementation that can get away with a lower
// amount and be perfectly safe.
let minimumMemory: UInt64 = 200.mib()
@@ -451,18 +451,18 @@ public actor ContainersService {
)
let runtime = state.snapshot.configuration.runtimeHandler
let sandboxClient = try await SandboxClient.create(
let runtimeClient = try await RuntimeClient.create(
id: id,
runtime: runtime
)
try await sandboxClient.bootstrap(stdio: stdio, networkBootstrapInfos: networkBootstrapInfos, dynamicEnv: dynamicEnv)
try await runtimeClient.bootstrap(stdio: stdio, networkBootstrapInfos: networkBootstrapInfos, dynamicEnv: dynamicEnv)
try await self.exitMonitor.registerProcess(
id: id,
onExit: self.handleContainerExit
)
state.client = sandboxClient
state.client = runtimeClient
await self.setContainerState(id, state, context: context)
} catch {
let label = Self.fullLaunchdServiceLabel(
@@ -629,7 +629,7 @@ public actor ContainersService {
let state = try self._getContainerState(id: id)
// Stop should be idempotent.
let client: SandboxClient
let client: RuntimeClient
do {
client = try state.getClient()
} catch {
@@ -941,8 +941,8 @@ public actor ContainersService {
await self.exitMonitor.stopTracking(id: id)
// Shutdown and deregister the sandbox service
self.log.info("shutting down sandbox service", metadata: ["id": "\(id)"])
// Shutdown and deregister the runtime service
self.log.info("shutting down runtime service", metadata: ["id": "\(id)"])
let path = self.containerRoot.appendingPathComponent(id)
let bundle = ContainerResource.Bundle(path: path)
@@ -952,7 +952,7 @@ public actor ContainersService {
instanceId: id
)
// Try to shutdown the client gracefully, but if the sandbox service
// Try to shutdown the client gracefully, but if the runtime service
// is already dead (e.g., killed externally), we should still continue
// with state cleanup.
if let client = state.client {
@@ -960,7 +960,7 @@ public actor ContainersService {
try await client.shutdown()
} catch {
self.log.error(
"failed to shutdown sandbox service",
"failed to shutdown runtime service",
metadata: [
"id": "\(id)",
"error": "\(error)",
@@ -973,10 +973,10 @@ public actor ContainersService {
// the process was killed externally.
do {
try ServiceManager.deregister(fullServiceLabel: label)
self.log.info("deregistered sandbox service", metadata: ["id": "\(id)"])
self.log.info("deregistered runtime service", metadata: ["id": "\(id)"])
} catch {
self.log.error(
"failed to deregister sandbox service",
"failed to deregister runtime service",
metadata: [
"id": "\(id)",
"error": "\(error)",
@@ -1,48 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
public enum SandboxRoutes: String {
/// Create an xpc endpoint to the sandbox instance.
case createEndpoint = "com.apple.container.sandbox/createEndpoint"
/// Bootstrap the sandbox instance and create the init process.
case bootstrap = "com.apple.container.sandbox/bootstrap"
/// Create a process in the sandbox.
case createProcess = "com.apple.container.sandbox/createProcess"
/// Start a process in the sandbox.
case start = "com.apple.container.sandbox/start"
/// Stop the sandbox.
case stop = "com.apple.container.sandbox/stop"
/// Return the current state of the sandbox.
case state = "com.apple.container.sandbox/state"
/// Kill a process in the sandbox.
case kill = "com.apple.container.sandbox/kill"
/// Resize the pty of a process in the sandbox.
case resize = "com.apple.container.sandbox/resize"
/// Wait on a process in the sandbox.
case wait = "com.apple.container.sandbox/wait"
/// Execute a new process in the sandbox.
case exec = "com.apple.container.sandbox/exec"
/// Dial a vsock port in the sandbox.
case dial = "com.apple.container.sandbox/dial"
/// Shutdown the sandbox service process.
case shutdown = "com.apple.container.sandbox/shutdown"
/// Get statistics for the sandbox.
case statistics = "com.apple.container.sandbox/statistics"
/// Copy a file or directory into the container.
case copyIn = "com.apple.container.sandbox/copyIn"
/// Copy a file or directory out of the container.
case copyOut = "com.apple.container.sandbox/copyOut"
}
@@ -23,8 +23,8 @@ import ContainerizationOS
import Foundation
import TerminalProgress
/// A client for interacting with a single sandbox.
public struct SandboxClient: Sendable {
/// A client for interacting with a container runtime service instance.
public struct RuntimeClient: Sendable {
static let label = "com.apple.container.runtime"
public static func machServiceLabel(runtime: String, id: String) -> String {
@@ -45,12 +45,12 @@ public struct SandboxClient: Sendable {
self.client = client
}
/// Create a SandboxClient by ID and runtime string. The returned client is ready to be used
/// Create a RuntimeClient by ID and runtime string. The returned client is ready to be used
/// without additional steps.
public static func create(id: String, runtime: String, timeout: Duration = XPCClient.xpcRegistrationTimeout) async throws -> SandboxClient {
public static func create(id: String, runtime: String, timeout: Duration = XPCClient.xpcRegistrationTimeout) async throws -> RuntimeClient {
let label = Self.machServiceLabel(runtime: runtime, id: id)
let client = XPCClient(service: label)
let request = XPCMessage(route: SandboxRoutes.createEndpoint.rawValue)
let request = XPCMessage(route: RuntimeRoutes.createEndpoint.rawValue)
let response: XPCMessage
do {
@@ -62,30 +62,30 @@ public struct SandboxClient: Sendable {
cause: error
)
}
guard let endpoint = response.endpoint(key: SandboxKeys.sandboxServiceEndpoint.rawValue) else {
guard let endpoint = response.endpoint(key: RuntimeKeys.runtimeServiceEndpoint.rawValue) else {
throw ContainerizationError(
.internalError,
message: "failed to get endpoint for sandbox service"
message: "failed to get endpoint for runtime service"
)
}
let endpointConnection = xpc_connection_create_from_endpoint(endpoint)
let xpcClient = XPCClient(connection: endpointConnection, label: label)
return SandboxClient(id: id, runtime: runtime, client: xpcClient)
return RuntimeClient(id: id, runtime: runtime, client: xpcClient)
}
}
// Runtime Methods
extension SandboxClient {
extension RuntimeClient {
public func bootstrap(
stdio: [FileHandle?],
networkBootstrapInfos: [NetworkBootstrapInfo],
dynamicEnv: [String: String] = [:]
) async throws {
let request = XPCMessage(route: SandboxRoutes.bootstrap.rawValue)
let request = XPCMessage(route: RuntimeRoutes.bootstrap.rawValue)
for (i, h) in stdio.enumerated() {
let key: SandboxKeys = try {
let key: RuntimeKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
@@ -102,10 +102,10 @@ extension SandboxClient {
do {
let dynamicEnv = try JSONEncoder().encode(dynamicEnv)
request.set(key: SandboxKeys.dynamicEnv.rawValue, value: dynamicEnv)
request.set(key: RuntimeKeys.dynamicEnv.rawValue, value: dynamicEnv)
let infosData = try JSONEncoder().encode(networkBootstrapInfos)
request.set(key: SandboxKeys.networkBootstrapInfos.rawValue, value: infosData)
request.set(key: RuntimeKeys.networkBootstrapInfos.rawValue, value: infosData)
try await self.client.send(request)
} catch {
throw ContainerizationError(
@@ -117,7 +117,7 @@ extension SandboxClient {
}
public func state() async throws -> SandboxSnapshot {
let request = XPCMessage(route: SandboxRoutes.state.rawValue)
let request = XPCMessage(route: RuntimeRoutes.state.rawValue)
let response: XPCMessage
do {
response = try await self.client.send(request)
@@ -132,13 +132,13 @@ extension SandboxClient {
}
public func createProcess(_ id: String, config: ProcessConfiguration, stdio: [FileHandle?]) async throws {
let request = XPCMessage(route: SandboxRoutes.createProcess.rawValue)
request.set(key: SandboxKeys.id.rawValue, value: id)
let request = XPCMessage(route: RuntimeRoutes.createProcess.rawValue)
request.set(key: RuntimeKeys.id.rawValue, value: id)
let data = try JSONEncoder().encode(config)
request.set(key: SandboxKeys.processConfig.rawValue, value: data)
request.set(key: RuntimeKeys.processConfig.rawValue, value: data)
for (i, h) in stdio.enumerated() {
let key: SandboxKeys = try {
let key: RuntimeKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
@@ -165,8 +165,8 @@ extension SandboxClient {
}
public func startProcess(_ id: String) async throws {
let request = XPCMessage(route: SandboxRoutes.start.rawValue)
request.set(key: SandboxKeys.id.rawValue, value: id)
let request = XPCMessage(route: RuntimeRoutes.start.rawValue)
request.set(key: RuntimeKeys.id.rawValue, value: id)
do {
try await self.client.send(request)
} catch {
@@ -179,10 +179,10 @@ extension SandboxClient {
}
public func stop(options: ContainerStopOptions) async throws {
let request = XPCMessage(route: SandboxRoutes.stop.rawValue)
let request = XPCMessage(route: RuntimeRoutes.stop.rawValue)
let data = try JSONEncoder().encode(options)
request.set(key: SandboxKeys.stopOptions.rawValue, value: data)
request.set(key: RuntimeKeys.stopOptions.rawValue, value: data)
do {
try await self.client.send(request)
@@ -196,9 +196,9 @@ extension SandboxClient {
}
public func kill(_ id: String, signal: Int64) async throws {
let request = XPCMessage(route: SandboxRoutes.kill.rawValue)
request.set(key: SandboxKeys.id.rawValue, value: id)
request.set(key: SandboxKeys.signal.rawValue, value: signal)
let request = XPCMessage(route: RuntimeRoutes.kill.rawValue)
request.set(key: RuntimeKeys.id.rawValue, value: id)
request.set(key: RuntimeKeys.signal.rawValue, value: signal)
do {
try await self.client.send(request)
@@ -212,10 +212,10 @@ extension SandboxClient {
}
public func resize(_ id: String, size: Terminal.Size) async throws {
let request = XPCMessage(route: SandboxRoutes.resize.rawValue)
request.set(key: SandboxKeys.id.rawValue, value: id)
request.set(key: SandboxKeys.width.rawValue, value: UInt64(size.width))
request.set(key: SandboxKeys.height.rawValue, value: UInt64(size.height))
let request = XPCMessage(route: RuntimeRoutes.resize.rawValue)
request.set(key: RuntimeKeys.id.rawValue, value: id)
request.set(key: RuntimeKeys.width.rawValue, value: UInt64(size.width))
request.set(key: RuntimeKeys.height.rawValue, value: UInt64(size.height))
do {
try await self.client.send(request)
@@ -229,8 +229,8 @@ extension SandboxClient {
}
public func wait(_ id: String) async throws -> ExitStatus {
let request = XPCMessage(route: SandboxRoutes.wait.rawValue)
request.set(key: SandboxKeys.id.rawValue, value: id)
let request = XPCMessage(route: RuntimeRoutes.wait.rawValue)
request.set(key: RuntimeKeys.id.rawValue, value: id)
let response: XPCMessage
do {
@@ -242,14 +242,14 @@ extension SandboxClient {
cause: error
)
}
let code = response.int64(key: SandboxKeys.exitCode.rawValue)
let date = response.date(key: SandboxKeys.exitedAt.rawValue)
let code = response.int64(key: RuntimeKeys.exitCode.rawValue)
let date = response.date(key: RuntimeKeys.exitedAt.rawValue)
return ExitStatus(exitCode: Int32(code), exitedAt: date)
}
public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: SandboxRoutes.dial.rawValue)
request.set(key: SandboxKeys.port.rawValue, value: UInt64(port))
let request = XPCMessage(route: RuntimeRoutes.dial.rawValue)
request.set(key: RuntimeKeys.port.rawValue, value: UInt64(port))
let response: XPCMessage
do {
@@ -261,7 +261,7 @@ extension SandboxClient {
cause: error
)
}
guard let fh = response.fileHandle(key: SandboxKeys.fd.rawValue) else {
guard let fh = response.fileHandle(key: RuntimeKeys.fd.rawValue) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
@@ -271,7 +271,7 @@ extension SandboxClient {
}
public func shutdown() async throws {
let request = XPCMessage(route: SandboxRoutes.shutdown.rawValue)
let request = XPCMessage(route: RuntimeRoutes.shutdown.rawValue)
do {
_ = try await self.client.send(request)
@@ -285,11 +285,11 @@ extension SandboxClient {
}
public func copyIn(source: String, destination: String, mode: UInt32, createParents: Bool = true) async throws {
let request = XPCMessage(route: SandboxRoutes.copyIn.rawValue)
request.set(key: SandboxKeys.sourcePath.rawValue, value: source)
request.set(key: SandboxKeys.destinationPath.rawValue, value: destination)
request.set(key: SandboxKeys.fileMode.rawValue, value: UInt64(mode))
request.set(key: SandboxKeys.createParents.rawValue, value: createParents)
let request = XPCMessage(route: RuntimeRoutes.copyIn.rawValue)
request.set(key: RuntimeKeys.sourcePath.rawValue, value: source)
request.set(key: RuntimeKeys.destinationPath.rawValue, value: destination)
request.set(key: RuntimeKeys.fileMode.rawValue, value: UInt64(mode))
request.set(key: RuntimeKeys.createParents.rawValue, value: createParents)
do {
try await self.client.send(request, responseTimeout: .seconds(300))
@@ -303,10 +303,10 @@ extension SandboxClient {
}
public func copyOut(source: String, destination: String, createParents: Bool = true) async throws {
let request = XPCMessage(route: SandboxRoutes.copyOut.rawValue)
request.set(key: SandboxKeys.sourcePath.rawValue, value: source)
request.set(key: SandboxKeys.destinationPath.rawValue, value: destination)
request.set(key: SandboxKeys.createParents.rawValue, value: createParents)
let request = XPCMessage(route: RuntimeRoutes.copyOut.rawValue)
request.set(key: RuntimeKeys.sourcePath.rawValue, value: source)
request.set(key: RuntimeKeys.destinationPath.rawValue, value: destination)
request.set(key: RuntimeKeys.createParents.rawValue, value: createParents)
do {
try await self.client.send(request, responseTimeout: .seconds(300))
@@ -320,7 +320,7 @@ extension SandboxClient {
}
public func statistics() async throws -> ContainerStats {
let request = XPCMessage(route: SandboxRoutes.statistics.rawValue)
let request = XPCMessage(route: RuntimeRoutes.statistics.rawValue)
let response: XPCMessage
do {
@@ -333,7 +333,7 @@ extension SandboxClient {
)
}
guard let data = response.dataNoCopy(key: SandboxKeys.statistics.rawValue) else {
guard let data = response.dataNoCopy(key: RuntimeKeys.statistics.rawValue) else {
throw ContainerizationError(
.internalError,
message: "no statistics data returned"
@@ -346,7 +346,7 @@ extension SandboxClient {
extension XPCMessage {
public func id() throws -> String {
let id = self.string(key: SandboxKeys.id.rawValue)
let id = self.string(key: RuntimeKeys.id.rawValue)
guard let id else {
throw ContainerizationError(
.invalidArgument,
@@ -357,7 +357,7 @@ extension XPCMessage {
}
func sandboxSnapshot() throws -> SandboxSnapshot {
let data = self.dataNoCopy(key: SandboxKeys.snapshot.rawValue)
let data = self.dataNoCopy(key: RuntimeKeys.snapshot.rawValue)
guard let data else {
throw ContainerizationError(
.invalidArgument,
@@ -368,7 +368,7 @@ extension XPCMessage {
}
public func networkBootstrapInfos() throws -> [NetworkBootstrapInfo] {
guard let data = self.dataNoCopy(key: SandboxKeys.networkBootstrapInfos.rawValue) else {
guard let data = self.dataNoCopy(key: RuntimeKeys.networkBootstrapInfos.rawValue) else {
throw ContainerizationError(.invalidArgument, message: "missing networkBootstrapInfos in bootstrap message")
}
return try JSONDecoder().decode([NetworkBootstrapInfo].self, from: data)
@@ -23,6 +23,8 @@ public struct RuntimeConfiguration: Codable, Sendable {
static let runtimeConfigurationFilename = "runtime-configuration.json"
public let path: URL
// TODO: Remove runtime-specific fields (initialFilesystem, kernel, containerRootFilesystem).
// These should be encoded into the opaque `runtimeData` field by the CLI.
public let initialFilesystem: Filesystem
public let kernel: Kernel
public let containerConfiguration: ContainerConfiguration?
@@ -63,14 +65,15 @@ public struct RuntimeConfiguration: Codable, Sendable {
public static func readRuntimeConfiguration(from runtimeConfigurationPath: URL) throws -> RuntimeConfiguration {
let configurationPath = runtimeConfigurationPath.appendingPathComponent(RuntimeConfiguration.runtimeConfigurationFilename)
guard FileManager.default.fileExists(atPath: configurationPath.path) else {
let data: Data
do {
data = try Data(contentsOf: configurationPath)
} catch {
throw ContainerizationError(
.notFound,
message: "runtime configuration file not found at path: \(configurationPath.path)"
)
}
let data = try Data(contentsOf: configurationPath)
return try JSONDecoder().decode(RuntimeConfiguration.self, from: data)
}
}
@@ -14,7 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
public enum SandboxKeys: String {
public enum RuntimeKeys: String {
/// ID key.
case id
/// Vsock port number key.
@@ -27,8 +27,8 @@ public enum SandboxKeys: String {
case fd
/// Options for stopping a container key.
case stopOptions
/// An endpoint to talk to a sandbox service.
case sandboxServiceEndpoint
/// An endpoint to talk to the runtime service.
case runtimeServiceEndpoint
/// Process request keys.
case signal
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
/// XPC routes exposed by the runtime service.
public enum RuntimeRoutes: String {
// MARK: - Service lifecycle
/// Create an XPC endpoint for communicating with the runtime service.
case createEndpoint = "com.apple.container.runtime/createEndpoint"
/// Shut down the runtime service process. Requires the sandbox to be stopped first.
case shutdown = "com.apple.container.runtime/shutdown"
// MARK: - Sandbox lifecycle
/// Bootstrap the sandbox: create the VM, configure networks, and boot the guest.
case bootstrap = "com.apple.container.runtime/bootstrap"
/// Stop the sandbox and all processes running inside it.
case stop = "com.apple.container.runtime/stop"
/// Return the current state of the sandbox.
case state = "com.apple.container.runtime/state"
/// Get resource usage statistics for the sandbox.
case statistics = "com.apple.container.runtime/statistics"
/// Open a vsock connection to a port inside the sandbox.
case dial = "com.apple.container.runtime/dial"
// MARK: - Process management
/// Register a new process inside the sandbox (used by exec).
case createProcess = "com.apple.container.runtime/createProcess"
/// Start a registered process inside the sandbox.
case start = "com.apple.container.runtime/start"
/// Send a signal to a process inside the sandbox.
case kill = "com.apple.container.runtime/kill"
/// Resize the PTY of a process inside the sandbox.
case resize = "com.apple.container.runtime/resize"
/// Wait for a process inside the sandbox to exit.
case wait = "com.apple.container.runtime/wait"
/// Execute a new process in the sandbox.
case exec = "com.apple.container.runtime/exec"
// MARK: - File Management
/// Copy a file or directory into the container.
case copyIn = "com.apple.container.runtime/copyIn"
/// Copy a file or directory out of the container.
case copyOut = "com.apple.container.runtime/copyOut"
}
@@ -15,14 +15,16 @@
//===----------------------------------------------------------------------===//
import ContainerResource
import ContainerSandboxService
import ContainerRuntimeClient
import ContainerXPC
import Containerization
/// Isolated container network interface strategy. This strategy prohibits
/// container to container networking, but it is the only approach that
/// works for macOS Sequoia.
struct IsolatedInterfaceStrategy: InterfaceStrategy {
public struct IsolatedInterfaceStrategy: InterfaceStrategy {
public init() {}
public func toInterface(attachment: Attachment, interfaceIndex: Int, additionalData: XPCMessage?) -> Interface {
let ipv4Gateway = interfaceIndex == 0 ? attachment.ipv4Gateway : nil
return NATInterface(
@@ -15,7 +15,7 @@
//===----------------------------------------------------------------------===//
import ContainerResource
import ContainerSandboxService
import ContainerRuntimeClient
import ContainerXPC
import Containerization
import ContainerizationError
@@ -25,7 +25,7 @@ import vmnet
/// Interface strategy for containers that use macOS's custom network feature.
@available(macOS 26, *)
struct NonisolatedInterfaceStrategy: InterfaceStrategy {
public struct NonisolatedInterfaceStrategy: InterfaceStrategy {
private let log: Logger
public init(log: Logger) {
@@ -18,7 +18,7 @@ import ContainerNetworkServiceClient
import ContainerOS
import ContainerPersistence
import ContainerResource
import ContainerSandboxServiceClient
import ContainerRuntimeClient
import ContainerXPC
import Containerization
import ContainerizationError
@@ -37,7 +37,7 @@ import struct ContainerizationOCI.Mount
import struct ContainerizationOCI.Process
/// An XPC service that manages the lifecycle of a single VM-backed container.
public actor SandboxService {
public actor RuntimeService {
private let connection: xpc_connection_t
private let root: URL
private let interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy]
@@ -116,7 +116,7 @@ public actor SandboxService {
///
/// - Returns: An XPC message with the following parameters:
/// - endpoint: An XPC endpoint that can be used to communicate
/// with the sandbox service.
/// with the runtime service.
@Sendable
public func createEndpoint(_ message: XPCMessage) async throws -> XPCMessage {
self.log.debug("enter", metadata: ["func": "\(#function)"])
@@ -124,7 +124,7 @@ public actor SandboxService {
let endpoint = xpc_endpoint_create(self.connection)
let reply = message.reply()
reply.set(key: SandboxKeys.sandboxServiceEndpoint.rawValue, value: endpoint)
reply.set(key: RuntimeKeys.runtimeServiceEndpoint.rawValue, value: endpoint)
return reply
}
@@ -139,8 +139,6 @@ public actor SandboxService {
self.log.debug("enter", metadata: ["func": "\(#function)"])
defer { self.log.debug("exit", metadata: ["func": "\(#function)"]) }
// Create the bundle if it doesn't exist yet
// Create the bundle if it doesn't exist yet
if !self.bundleExists(at: self.root) {
try self.createBundle()
@@ -365,12 +363,12 @@ public actor SandboxService {
let reply = message.reply()
let data = try JSONEncoder().encode(containerStats)
reply.set(key: SandboxKeys.statistics.rawValue, value: data)
reply.set(key: RuntimeKeys.statistics.rawValue, value: data)
return reply
}
}
/// Shutdown the SandboxService.
/// Shutdown the RuntimeService.
///
/// - Parameters:
/// - message: An XPC message with no parameters.
@@ -602,8 +600,8 @@ public actor SandboxService {
case .running:
let id = try message.id()
let ctr = try getContainer()
let width = message.uint64(key: SandboxKeys.width.rawValue)
let height = message.uint64(key: SandboxKeys.height.rawValue)
let width = message.uint64(key: RuntimeKeys.width.rawValue)
let height = message.uint64(key: RuntimeKeys.height.rawValue)
if id != ctr.container.id {
guard let processInfo = self.processes[id] else {
@@ -655,7 +653,7 @@ public actor SandboxService {
self.log.debug("enter", metadata: ["func": "\(#function)"])
defer { self.log.debug("exit", metadata: ["func": "\(#function)"]) }
guard let id = message.string(key: SandboxKeys.id.rawValue) else {
guard let id = message.string(key: RuntimeKeys.id.rawValue) else {
throw ContainerizationError(.invalidArgument, message: "missing id in wait xpc message")
}
@@ -663,8 +661,8 @@ public actor SandboxService {
self.waitForExit(id: id, cont: cc)
}
let reply = message.reply()
reply.set(key: SandboxKeys.exitCode.rawValue, value: Int64(exitStatus.exitCode))
reply.set(key: SandboxKeys.exitedAt.rawValue, value: exitStatus.exitedAt)
reply.set(key: RuntimeKeys.exitCode.rawValue, value: Int64(exitStatus.exitCode))
reply.set(key: RuntimeKeys.exitedAt.rawValue, value: exitStatus.exitedAt)
return reply
}
@@ -682,20 +680,20 @@ public actor SandboxService {
self.log.info("`copyIn` xpc handler")
switch self.state {
case .running, .booted:
guard let source = message.string(key: SandboxKeys.sourcePath.rawValue) else {
guard let source = message.string(key: RuntimeKeys.sourcePath.rawValue) else {
throw ContainerizationError(
.invalidArgument,
message: "no source path supplied for copyIn"
)
}
guard let destination = message.string(key: SandboxKeys.destinationPath.rawValue) else {
guard let destination = message.string(key: RuntimeKeys.destinationPath.rawValue) else {
throw ContainerizationError(
.invalidArgument,
message: "no destination path supplied for copyIn"
)
}
let mode = UInt32(message.uint64(key: SandboxKeys.fileMode.rawValue))
let createParents = message.bool(key: SandboxKeys.createParents.rawValue)
let mode = UInt32(message.uint64(key: RuntimeKeys.fileMode.rawValue))
let createParents = message.bool(key: RuntimeKeys.createParents.rawValue)
let ctr = try getContainer()
try await ctr.container.copyIn(
@@ -727,20 +725,20 @@ public actor SandboxService {
self.log.info("`copyOut` xpc handler")
switch self.state {
case .running, .booted:
guard let source = message.string(key: SandboxKeys.sourcePath.rawValue) else {
guard let source = message.string(key: RuntimeKeys.sourcePath.rawValue) else {
throw ContainerizationError(
.invalidArgument,
message: "no source path supplied for copyOut"
)
}
guard let destination = message.string(key: SandboxKeys.destinationPath.rawValue) else {
guard let destination = message.string(key: RuntimeKeys.destinationPath.rawValue) else {
throw ContainerizationError(
.invalidArgument,
message: "no destination path supplied for copyOut"
)
}
let createParents = message.bool(key: SandboxKeys.createParents.rawValue)
let createParents = message.bool(key: RuntimeKeys.createParents.rawValue)
let ctr = try getContainer()
try await ctr.container.copyOut(
@@ -773,7 +771,7 @@ public actor SandboxService {
switch self.state {
case .running, .booted:
let port = message.uint64(key: SandboxKeys.port.rawValue)
let port = message.uint64(key: RuntimeKeys.port.rawValue)
guard port > 0 else {
throw ContainerizationError(
.invalidArgument,
@@ -785,7 +783,7 @@ public actor SandboxService {
let fh = try await ctr.container.dialVsock(port: UInt32(port))
let reply = message.reply()
reply.set(key: SandboxKeys.fd.rawValue, value: fh)
reply.set(key: RuntimeKeys.fd.rawValue, value: fh)
return reply
default:
throw ContainerizationError(
@@ -1252,11 +1250,11 @@ public actor SandboxService {
extension XPCMessage {
fileprivate func signal() throws -> Int64 {
self.int64(key: SandboxKeys.signal.rawValue)
self.int64(key: RuntimeKeys.signal.rawValue)
}
fileprivate func stopOptions() throws -> ContainerStopOptions {
guard let data = self.dataNoCopy(key: SandboxKeys.stopOptions.rawValue) else {
guard let data = self.dataNoCopy(key: RuntimeKeys.stopOptions.rawValue) else {
throw ContainerizationError(.invalidArgument, message: "empty StopOptions")
}
return try JSONDecoder().decode(ContainerStopOptions.self, from: data)
@@ -1264,36 +1262,36 @@ extension XPCMessage {
fileprivate func setState(_ state: SandboxSnapshot) throws {
let data = try JSONEncoder().encode(state)
self.set(key: SandboxKeys.snapshot.rawValue, value: data)
self.set(key: RuntimeKeys.snapshot.rawValue, value: data)
}
fileprivate func stdio() -> [FileHandle?] {
var handles = [FileHandle?](repeating: nil, count: 3)
if let stdin = self.fileHandle(key: SandboxKeys.stdin.rawValue) {
if let stdin = self.fileHandle(key: RuntimeKeys.stdin.rawValue) {
handles[0] = stdin
}
if let stdout = self.fileHandle(key: SandboxKeys.stdout.rawValue) {
if let stdout = self.fileHandle(key: RuntimeKeys.stdout.rawValue) {
handles[1] = stdout
}
if let stderr = self.fileHandle(key: SandboxKeys.stderr.rawValue) {
if let stderr = self.fileHandle(key: RuntimeKeys.stderr.rawValue) {
handles[2] = stderr
}
return handles
}
fileprivate func setFileHandle(_ handle: FileHandle) {
self.set(key: SandboxKeys.fd.rawValue, value: handle)
self.set(key: RuntimeKeys.fd.rawValue, value: handle)
}
fileprivate func processConfig() throws -> ProcessConfiguration {
guard let data = self.dataNoCopy(key: SandboxKeys.processConfig.rawValue) else {
guard let data = self.dataNoCopy(key: RuntimeKeys.processConfig.rawValue) else {
throw ContainerizationError(.invalidArgument, message: "empty process configuration")
}
return try JSONDecoder().decode(ProcessConfiguration.self, from: data)
}
fileprivate func dynamicEnv() throws -> [String: String] {
let data = self.dataNoCopy(key: SandboxKeys.dynamicEnv.rawValue)
let data = self.dataNoCopy(key: RuntimeKeys.dynamicEnv.rawValue)
let dynamicEnv = try data.map { try JSONDecoder().decode([String: String].self, from: $0) } ?? [:]
return dynamicEnv
}
@@ -1428,7 +1426,7 @@ extension FileHandle: @retroactive ReaderStream, @retroactive Writer {
// MARK: State handler and bundle creation helpers
extension SandboxService {
extension RuntimeService {
private func initializeWaiters(for id: String) throws {
guard waiters[id] == nil else {
throw ContainerizationError(.invalidState, message: "waiter for \(id) already initialized")
@@ -1508,7 +1506,7 @@ extension SandboxService {
case stopping
/// Once a stop is successful, .stopping will transition to .stopped.
case stopped
/// .shuttingDown will be the last state the sandbox service will ever be in. Shortly
/// .shuttingDown will be the last state the runtime service will ever be in. Shortly
/// afterwards the process will exit.
case shuttingDown
}
@@ -15,8 +15,8 @@
//===----------------------------------------------------------------------===//
import ContainerResource
import ContainerRuntimeLinuxTypes
import ContainerSandboxServiceClient
import ContainerRuntimeClient
import ContainerRuntimeLinuxClient
import Containerization
import Foundation
import Testing
+3 -2
View File
@@ -18,8 +18,9 @@ opts+=("--allow-writing-to-directory" "$1")
opts+=("generate-documentation")
opts+=("--target" "ContainerAPIService")
opts+=("--target" "ContainerAPIClient")
opts+=("--target" "ContainerSandboxService")
opts+=("--target" "ContainerSandboxServiceClient")
opts+=("--target" "ContainerRuntimeClient")
opts+=("--target" "ContainerRuntimeLinuxClient")
opts+=("--target" "ContainerRuntimeLinuxServer")
opts+=("--target" "ContainerNetworkService")
opts+=("--target" "ContainerNetworkServiceClient")
opts+=("--target" "ContainerImagesService")