diff --git a/Sources/ContainerCommands/System/SystemStart.swift b/Sources/ContainerCommands/System/SystemStart.swift index 77c9a9d33..b1f71f799 100644 --- a/Sources/ContainerCommands/System/SystemStart.swift +++ b/Sources/ContainerCommands/System/SystemStart.swift @@ -31,13 +31,13 @@ extension Application { @Option( name: .shortAndLong, - help: "Application data directory", + help: "Path to the root directory for application data", transform: { URL(filePath: $0) }) var appRoot = ApplicationRoot.defaultURL @Option( name: .long, - help: "Path to the installation root directory", + help: "Path to the root directory for application executables and plugins", transform: { URL(filePath: $0) }) var installRoot = InstallRoot.defaultURL @@ -54,7 +54,7 @@ extension Application { public func run() async throws { // Without the true path to the binary in the plist, `container-apiserver` won't launch properly. - // TODO: Use plugin loader for API server. + // TODO: Can we use the plugin loader to bootstrap the API server? let executableUrl = CommandLine.executablePathUrl .deletingLastPathComponent() .appendingPathComponent("container-apiserver") diff --git a/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper+Start.swift b/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper+Start.swift new file mode 100644 index 000000000..c85240bbc --- /dev/null +++ b/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper+Start.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ContainerClient +import ContainerLog +import ContainerSandboxService +import ContainerXPC +import Foundation +import Logging +import NIO + +extension RuntimeLinuxHelper { + struct Start: AsyncParsableCommand { + static let label = "com.apple.container.runtime.container-runtime-linux" + + static let configuration = CommandConfiguration( + commandName: "start", + abstract: "Start helper for a Linux container" + ) + + @Flag(name: .long, help: "Enable debug logging") + var debug = false + + @Option(name: .shortAndLong, help: "Sandbox UUID") + var uuid: String + + @Option(name: .shortAndLong, help: "Root directory for the sandbox") + var root: String + + var machServiceLabel: String { + "\(Self.label).\(uuid)" + } + + func run() async throws { + let commandName = Self._commandName + let log = RuntimeLinuxHelper.setupLogger(debug: debug, metadata: ["uuid": "\(uuid)"]) + + log.info("starting \(commandName)") + defer { + log.info("stopping \(commandName)") + } + + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + do { + try adjustLimits() + signal(SIGPIPE, SIG_IGN) + + log.info("configuring XPC server") + let interfaceStrategy: any InterfaceStrategy + if #available(macOS 26, *) { + interfaceStrategy = NonisolatedInterfaceStrategy(log: log) + } else { + interfaceStrategy = IsolatedInterfaceStrategy() + } + + nonisolated(unsafe) let anonymousConnection = xpc_connection_create(nil, nil) + let server = SandboxService( + root: .init(fileURLWithPath: root), + interfaceStrategy: interfaceStrategy, + eventLoopGroup: eventLoopGroup, + connection: anonymousConnection, + log: log + ) + + let endpointServer = XPCServer( + identifier: machServiceLabel, + routes: [ + SandboxRoutes.createEndpoint.rawValue: server.createEndpoint + ], + log: log + ) + + let mainServer = XPCServer( + connection: anonymousConnection, + routes: [ + SandboxRoutes.bootstrap.rawValue: server.bootstrap, + SandboxRoutes.createProcess.rawValue: server.createProcess, + SandboxRoutes.state.rawValue: server.state, + SandboxRoutes.stop.rawValue: server.stop, + SandboxRoutes.kill.rawValue: server.kill, + SandboxRoutes.resize.rawValue: server.resize, + SandboxRoutes.wait.rawValue: server.wait, + SandboxRoutes.start.rawValue: server.startProcess, + SandboxRoutes.dial.rawValue: server.dial, + ], + log: log + ) + + log.info("starting XPC server") + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await endpointServer.listen() + } + group.addTask { + try await mainServer.listen() + } + defer { group.cancelAll() } + + _ = try await group.next() + } + } catch { + log.error("\(commandName) failed", metadata: ["error": "\(error)"]) + try? await eventLoopGroup.shutdownGracefully() + RuntimeLinuxHelper.Start.exit(withError: error) + } + } + + private func adjustLimits() throws { + var limits = rlimit() + guard getrlimit(RLIMIT_NOFILE, &limits) == 0 else { + throw POSIXError(.init(rawValue: errno)!) + } + limits.rlim_cur = 65536 + limits.rlim_max = 65536 + guard setrlimit(RLIMIT_NOFILE, &limits) == 0 else { + throw POSIXError(.init(rawValue: errno)!) + } + } + } +} diff --git a/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift b/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift index 9fb5b2d5f..83413109d 100644 --- a/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift +++ b/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift @@ -15,140 +15,39 @@ //===----------------------------------------------------------------------===// import ArgumentParser -import ContainerClient import ContainerLog -import ContainerNetworkService -import ContainerSandboxService import ContainerVersion -import ContainerXPC -import Containerization -import ContainerizationError -import Foundation import Logging -import NIO +import OSLog @main struct RuntimeLinuxHelper: AsyncParsableCommand { - static let label = "com.apple.container.runtime.container-runtime-linux" - static let configuration = CommandConfiguration( commandName: "container-runtime-linux", abstract: "XPC Service for managing a Linux sandbox", - version: ReleaseVersion.singleLine(appName: "container-runtime-linux") + version: ReleaseVersion.singleLine(appName: "container-runtime-linux"), + subcommands: [ + Start.self + ] ) - @Flag(name: .long, help: "Enable debug logging") - var debug = false - - @Option(name: .shortAndLong, help: "Sandbox UUID") - var uuid: String - - @Option(name: .shortAndLong, help: "Root directory for the sandbox") - var root: String - - var machServiceLabel: String { - "\(Self.label).\(uuid)" - } - - func run() async throws { - let commandName = Self._commandName - let log = setupLogger() - log.info("starting \(commandName)") - defer { - log.info("stopping \(commandName)") - } - - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - do { - try adjustLimits() - signal(SIGPIPE, SIG_IGN) - - log.info("configuring XPC server") - - let interfaceStrategy: any InterfaceStrategy - if #available(macOS 26, *) { - interfaceStrategy = NonisolatedInterfaceStrategy(log: log) - } else { - interfaceStrategy = IsolatedInterfaceStrategy() - } - - nonisolated(unsafe) let anonymousConnection = xpc_connection_create(nil, nil) - let server = SandboxService( - root: .init(fileURLWithPath: root), - interfaceStrategy: interfaceStrategy, - eventLoopGroup: eventLoopGroup, - connection: anonymousConnection, - log: log - ) - - let endpointServer = XPCServer( - identifier: machServiceLabel, - routes: [ - SandboxRoutes.createEndpoint.rawValue: server.createEndpoint - ], - log: log - ) - - let mainServer = XPCServer( - connection: anonymousConnection, - routes: [ - SandboxRoutes.bootstrap.rawValue: server.bootstrap, - SandboxRoutes.createProcess.rawValue: server.createProcess, - SandboxRoutes.state.rawValue: server.state, - SandboxRoutes.stop.rawValue: server.stop, - SandboxRoutes.kill.rawValue: server.kill, - SandboxRoutes.resize.rawValue: server.resize, - SandboxRoutes.wait.rawValue: server.wait, - SandboxRoutes.start.rawValue: server.startProcess, - SandboxRoutes.dial.rawValue: server.dial, - SandboxRoutes.shutdown.rawValue: server.shutdown, - ], - log: log - ) - - log.info("starting XPC server") - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await endpointServer.listen() - } - group.addTask { - try await mainServer.listen() - } - defer { group.cancelAll() } - - _ = try await group.next() - } - } catch { - log.error("\(commandName) failed", metadata: ["error": "\(error)"]) - try? await eventLoopGroup.shutdownGracefully() - RuntimeLinuxHelper.exit(withError: error) - } - } - - private func setupLogger() -> Logger { + package static func setupLogger(debug: Bool, metadata: [String: Logging.Logger.Metadata.Value] = [:]) -> Logging.Logger { LoggingSystem.bootstrap { label in OSLogHandler( label: label, category: "RuntimeLinuxHelper" ) } + var log = Logger(label: "com.apple.container") if debug { log.logLevel = .debug } - log[metadataKey: "uuid"] = "\(uuid)" - return log - } - private func adjustLimits() throws { - var limits = rlimit() - guard getrlimit(RLIMIT_NOFILE, &limits) == 0 else { - throw POSIXError(.init(rawValue: errno)!) - } - limits.rlim_cur = 65536 - limits.rlim_max = 65536 - guard setrlimit(RLIMIT_NOFILE, &limits) == 0 else { - throw POSIXError(.init(rawValue: errno)!) + for (key, val) in metadata { + log[metadataKey: key] = val } + + return log } } diff --git a/Sources/Services/ContainerAPIService/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Containers/ContainersService.swift index 86f361d39..552cc63dd 100644 --- a/Sources/Services/ContainerAPIService/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Containers/ContainersService.swift @@ -522,6 +522,7 @@ public actor ContainersService { path: URL ) throws { let args = [ + "start", "--root", path.path, "--uuid", configuration.id, "--debug",