Select alternate data path with container system start --app-root path. (#419)

Closes #418.
This commit is contained in:
J Logan
2025-08-06 14:49:09 -07:00
committed by GitHub
parent b8965cae43
commit 88223d8add
20 changed files with 253 additions and 135 deletions
+4 -1
View File
@@ -68,7 +68,10 @@ jobs:
- name: Test the container project
run: |
launchctl setenv HTTP_PROXY $HTTP_PROXY
make test cleancontent install-kernel integration
APP_ROOT=$(mktemp -d -p "${RUNNER_TEMP}")
trap 'rm -rf "${APP_ROOT}"; echo Removing data directory ${APP_ROOT}' EXIT
echo "Created data directory ${APP_ROOT}"
make APP_ROOT="${APP_ROOT}" test install-kernel integration
env:
DEVELOPER_DIR: "/Applications/Xcode-latest.app/Contents/Developer"
+25 -16
View File
@@ -28,6 +28,13 @@ DSYM_DIR := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM
DSYM_PATH := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM.zip
CODESIGN_OPTS ?= --force --sign - --timestamp=none
# Conditionally use a temporary data directory for integration tests
ifeq ($(strip $(APP_ROOT)),)
SYSTEM_START_OPTS :=
else
SYSTEM_START_OPTS := --app-root "$(strip $(APP_ROOT))"
endif
MACOS_VERSION := $(shell sw_vers -productVersion)
MACOS_MAJOR := $(shell echo $(MACOS_VERSION) | cut -d. -f1)
@@ -124,28 +131,30 @@ test:
.PHONY: install-kernel
install-kernel:
@bin/container system stop || true
@bin/container system start --enable-kernel-install
@bin/container system start --enable-kernel-install $(SYSTEM_START_OPTS)
.PHONY: integration
integration: init-block
@echo Ensuring apiserver stopped before the CLI integration tests...
@bin/container system stop && sleep 3 && scripts/ensure-container-stopped.sh
@echo Running the integration tests...
@bin/container system start
@echo "Removing any existing containers"
@bin/container rm --all
@echo "Starting CLI integration tests"
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLINetwork
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunLifecycle
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIExecCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLICreateCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIImagesCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunBase
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIBuildBase
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIVolumes
@echo Ensuring apiserver stopped after the CLI integration tests...
@scripts/ensure-container-stopped.sh
bin/container system start $(SYSTEM_START_OPTS) && \
echo "Starting CLI integration tests" && \
{ \
exit_code=0; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLINetwork || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunLifecycle || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIExecCommand || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLICreateCommand || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunCommand || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIImagesCommand || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunBase || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIBuildBase || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIVolumes || exit_code=1 ; \
echo Ensuring apiserver stopped after the CLI integration tests ; \
scripts/ensure-container-stopped.sh ; \
exit $${exit_code} ; \
}
.PHONY: fmt
fmt: swift-fmt update-licenses
+1
View File
@@ -164,6 +164,7 @@ let package = Package(
.product(name: "Containerization", package: "containerization"),
"CVersion",
"ContainerLog",
"ContainerPlugin",
"ContainerXPC",
"ContainerImagesService",
],
+29 -29
View File
@@ -43,16 +43,11 @@ struct APIServer: AsyncParsableCommand {
@Flag(name: .long, help: "Enable debug logging")
var debug = false
@Option(name: .shortAndLong, help: "Daemon root directory")
var root = Self.appRoot.path
var appRoot = ApplicationRoot.url
static let appRoot: URL = {
FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
.appendingPathComponent("com.apple.container")
}()
static func releaseVersion() -> String {
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
}
func run() async throws {
let commandName = Self.configuration.commandName ?? "container-apiserver"
@@ -64,20 +59,18 @@ struct APIServer: AsyncParsableCommand {
do {
log.info("configuring XPC server")
let root = URL(filePath: root)
var routes = [XPCRoute: XPCServer.RouteHandler]()
let pluginLoader = try initializePluginLoader(log: log)
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes)
let containersService = try initializeContainerService(root: root, pluginLoader: pluginLoader, log: log, routes: &routes)
let containersService = try initializeContainerService(pluginLoader: pluginLoader, log: log, routes: &routes)
let networkService = try await initializeNetworkService(
root: root,
pluginLoader: pluginLoader,
log: log,
routes: &routes
)
initializeHealthCheckService(log: log, routes: &routes)
try initializeKernelService(log: log, routes: &routes)
try initializeVolumeService(root: root, containersService: containersService, log: log, routes: &routes)
try initializeVolumeService(containersService: containersService, log: log, routes: &routes)
let server = XPCServer(
identifier: "com.apple.container.apiserver",
@@ -134,7 +127,10 @@ struct APIServer: AsyncParsableCommand {
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
let pluginsURL = PluginLoader.userPluginsDir(root: installRoot)
log.info("initializing plugin loader", metadata: ["installRoot": "\(installRoot.path(percentEncoded: false))"])
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
log.info("detecting user plugins directory", metadata: ["path": "\(pluginsURL.path(percentEncoded: false))"])
var directoryExists: ObjCBool = false
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
@@ -161,10 +157,19 @@ struct APIServer: AsyncParsableCommand {
AppBundlePluginFactory(),
]
log.info("PLUGINS: \(pluginDirectories)")
let statePath = PluginLoader.defaultPluginResourcePath(root: Self.appRoot)
for pluginDirectory in pluginDirectories {
log.info("discovered plugin directory", metadata: ["path": "\(pluginDirectory.path(percentEncoded: false))"])
}
let statePath = PluginLoader.defaultPluginResourcePath(root: appRoot)
try FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)
return PluginLoader(pluginDirectories: pluginDirectories, pluginFactories: pluginFactories, defaultResourcePath: statePath, log: log)
return PluginLoader(
appRoot: appRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
defaultResourcePath: statePath,
log: log
)
}
// First load all of the plugins we can find. Then just expose
@@ -188,20 +193,20 @@ struct APIServer: AsyncParsableCommand {
}
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
let svc = HealthCheckHarness(log: log)
let svc = HealthCheckHarness(appRoot: appRoot, log: log)
routes[XPCRoute.ping] = svc.ping
}
private func initializeKernelService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
let svc = try KernelService(log: log, appRoot: Self.appRoot)
let svc = try KernelService(log: log, appRoot: appRoot)
let harness = KernelHarness(service: svc, log: log)
routes[XPCRoute.installKernel] = harness.install
routes[XPCRoute.getDefaultKernel] = harness.getDefaultKernel
}
private func initializeContainerService(root: URL, pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws -> ContainersService {
private func initializeContainerService(pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws -> ContainersService {
let service = try ContainersService(
root: root,
appRoot: appRoot,
pluginLoader: pluginLoader,
log: log
)
@@ -217,12 +222,11 @@ struct APIServer: AsyncParsableCommand {
}
private func initializeNetworkService(
root: URL,
pluginLoader: PluginLoader,
log: Logger,
routes: inout [XPCRoute: XPCServer.RouteHandler]
) async throws -> NetworksService {
let resourceRoot = root.appendingPathComponent("networks")
let resourceRoot = appRoot.appendingPathComponent("networks")
let service = try await NetworksService(
pluginLoader: pluginLoader,
resourceRoot: resourceRoot,
@@ -245,8 +249,8 @@ struct APIServer: AsyncParsableCommand {
return service
}
private func initializeVolumeService(root: URL, containersService: ContainersService, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
let resourceRoot = root.appendingPathComponent("volumes")
private func initializeVolumeService(containersService: ContainersService, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
let resourceRoot = appRoot.appendingPathComponent("volumes")
let service = try VolumesService(resourceRoot: resourceRoot, containersService: containersService, log: log)
let harness = VolumesHarness(service: service, log: log)
@@ -255,8 +259,4 @@ struct APIServer: AsyncParsableCommand {
routes[XPCRoute.volumeList] = harness.list
routes[XPCRoute.volumeInspect] = harness.inspect
}
private static func releaseVersion() -> String {
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
}
}
@@ -56,8 +56,8 @@ actor ContainersService {
}
}
public init(root: URL, pluginLoader: PluginLoader, log: Logger) throws {
let containerRoot = root.appendingPathComponent("containers")
public init(appRoot: URL, pluginLoader: PluginLoader, log: Logger) throws {
let containerRoot = appRoot.appendingPathComponent("containers")
try FileManager.default.createDirectory(at: containerRoot, withIntermediateDirectories: true)
self.containerRoot = containerRoot
self.pluginLoader = pluginLoader
@@ -200,7 +200,7 @@ actor ContainersService {
]
try loader.registerWithLaunchd(
plugin: plugin,
rootURL: path,
pluginStateRoot: path,
args: args,
instanceId: configuration.id
)
@@ -14,20 +14,28 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import CVersion
import ContainerClient
import ContainerXPC
import Containerization
import Foundation
import Logging
actor HealthCheckHarness {
private let appRoot: URL
private let log: Logger
public init(log: Logger) {
public init(appRoot: URL, log: Logger) {
self.appRoot = appRoot
self.log = log
}
@Sendable
func ping(_ message: XPCMessage) async -> XPCMessage {
message.reply()
let reply = message.reply()
reply.set(key: .appRoot, value: appRoot.absoluteString)
reply.set(key: .apiServerVersion, value: APIServer.releaseVersion())
reply.set(key: .apiServerCommit, value: get_git_commit().map { String(cString: $0) } ?? "unknown")
return reply
}
}
@@ -230,7 +230,7 @@ actor NetworksService {
try await pluginLoader.registerWithLaunchd(
plugin: networkPlugin,
rootURL: store.entityUrl(configuration.id),
pluginStateRoot: store.entityUrl(configuration.id),
args: args,
instanceId: configuration.id
)
+57 -49
View File
@@ -91,50 +91,6 @@ struct Application: AsyncParsableCommand {
defaultSubcommand: DefaultCommand.self
)
static let appRoot: URL = {
FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
.appendingPathComponent("com.apple.container")
}()
static let pluginLoader: PluginLoader = {
let installRoot = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
let pluginsURL = PluginLoader.userPluginsDir(root: installRoot)
var directoryExists: ObjCBool = false
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
// plugins built into the application installed as a macOS app bundle
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")
// plugins built into the application installed as a Unix-like application
let installRootPluginsURL =
installRoot
.appendingPathComponent("libexec")
.appendingPathComponent("container")
.appendingPathComponent("plugins")
.standardized
let pluginDirectories = [
userPluginsURL,
appBundlePluginsURL,
installRootPluginsURL,
].compactMap { $0 }
let pluginFactories = [
DefaultPluginFactory()
]
let statePath = PluginLoader.defaultPluginResourcePath(root: Self.appRoot)
try! FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)
return PluginLoader(pluginDirectories: pluginDirectories, pluginFactories: pluginFactories, defaultResourcePath: statePath, log: log)
}()
public static func main() async throws {
restoreCursorAtExit()
@@ -161,7 +117,8 @@ struct Application: AsyncParsableCommand {
// on the root command will land here.
let containsHelp = fullArgs.contains("-h") || fullArgs.contains("--help")
if fullArgs.count <= 2 && containsHelp {
Self.printModifiedHelpText()
let pluginLoader = try? await createPluginLoader()
await Self.printModifiedHelpText(pluginLoader: pluginLoader)
return
}
let errorAsString: String = String(describing: error)
@@ -174,6 +131,53 @@ struct Application: AsyncParsableCommand {
}
}
static func createPluginLoader() async throws -> PluginLoader {
let installRoot = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
var directoryExists: ObjCBool = false
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
// plugins built into the application installed as a macOS app bundle
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")
// plugins built into the application installed as a Unix-like application
let installRootPluginsURL =
installRoot
.appendingPathComponent("libexec")
.appendingPathComponent("container")
.appendingPathComponent("plugins")
.standardized
let pluginDirectories = [
userPluginsURL,
appBundlePluginsURL,
installRootPluginsURL,
].compactMap { $0 }
let pluginFactories = [
DefaultPluginFactory()
]
guard let systemHealth = try? await ClientHealthCheck.ping(timeout: .seconds(10)) else {
throw ContainerizationError(.timeout, message: "unable to retrieve application data root from API server")
}
let statePath = PluginLoader.defaultPluginResourcePath(root: systemHealth.appRoot)
guard (try? FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)) != nil else {
throw ContainerizationError(.invalidState, message: "unable to create plugin state directory")
}
return PluginLoader(
appRoot: systemHealth.appRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
defaultResourcePath: statePath,
log: log
)
}
static func handleProcess(io: ProcessIO, process: ClientProcess) async throws -> Int32 {
let signals = AsyncSignalHandler.create(notify: Application.signalSet)
return try await withThrowingTaskGroup(of: Int32?.self, returning: Int32.self) { group in
@@ -304,10 +308,14 @@ struct Application: AsyncParsableCommand {
extension Application {
// Because we support plugins, we need to modify the help text to display
// any if we found some.
static func printModifiedHelpText() {
let altered = Self.pluginLoader.alterCLIHelpText(
original: Application.helpMessage(for: Application.self)
)
static func printModifiedHelpText(pluginLoader: PluginLoader?) async {
let original = Application.helpMessage(for: Application.self)
guard let pluginLoader else {
print(original)
print("PLUGINS: not available, run `container system start`")
return
}
let altered = pluginLoader.alterCLIHelpText(original: original)
print(altered)
}
+2 -2
View File
@@ -170,8 +170,8 @@ extension Application {
}
let dockerfile = try Data(contentsOf: URL(filePath: file))
let exportPath = Application.appRoot.appendingPathComponent(".build")
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
let exportPath = systemHealth.appRoot.appendingPathComponent(".build")
let buildID = UUID().uuidString
let tempURL = exportPath.appendingPathComponent(buildID)
try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true, attributes: nil)
+2 -1
View File
@@ -71,7 +71,8 @@ extension Application {
let fetchTask = await taskManager.startTask()
let builderImage: String = ClientDefaults.get(key: .defaultBuilderImage)
let exportsMount: String = Application.appRoot.appendingPathComponent(".build").absolutePath()
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
let exportsMount: String = systemHealth.appRoot.appendingPathComponent(".build").absolutePath()
if !FileManager.default.fileExists(atPath: exportsMount) {
try FileManager.default.createDirectory(
+3 -3
View File
@@ -32,8 +32,9 @@ struct DefaultCommand: AsyncParsableCommand {
func run() async throws {
// See if we have a possible plugin command.
let pluginLoader = try? await Application.createPluginLoader()
guard let command = remaining.first else {
Application.printModifiedHelpText()
await Application.printModifiedHelpText(pluginLoader: pluginLoader)
return
}
@@ -44,8 +45,7 @@ struct DefaultCommand: AsyncParsableCommand {
throw ValidationError("Unknown option '\(command)'")
}
let pluginLoader = Application.pluginLoader
guard let plugin = pluginLoader.findPlugin(name: command), plugin.config.isCLI else {
guard let plugin = pluginLoader?.findPlugin(name: command), plugin.config.isCLI else {
throw ValidationError("failed to find plugin named container-\(command)")
}
// Exec performs execvp (with no fork).
+10 -2
View File
@@ -31,6 +31,12 @@ extension Application {
@Option(name: .shortAndLong, help: "Path to the `container-apiserver` binary")
var path: String = Bundle.main.executablePath ?? ""
@Option(
name: .shortAndLong,
help: "Application data directory",
transform: { URL(filePath: $0) })
var appRoot: URL = ApplicationRoot.defaultURL
@Flag(name: .long, help: "Enable debug logging for the runtime daemon.")
var debug = false
@@ -47,15 +53,17 @@ extension Application {
.appendingPathComponent("container-apiserver")
var args = [executableUrl.absolutePath()]
if debug {
args.append("--debug")
}
let apiServerDataUrl = appRoot.appending(path: "apiserver")
try! FileManager.default.createDirectory(at: apiServerDataUrl, withIntermediateDirectories: true)
let env = ProcessInfo.processInfo.environment.filter { key, _ in
var env = ProcessInfo.processInfo.environment.filter { key, _ in
key.hasPrefix("CONTAINER_")
}
env["CONTAINER_APP_ROOT"] = appRoot.path(percentEncoded: false)
let logURL = apiServerDataUrl.appending(path: "apiserver.log")
let plist = LaunchPlist(
@@ -78,7 +86,7 @@ extension Application {
// Now ping our friendly daemon. Fail if we don't get a response.
do {
print("Verifying apiserver is running...")
try await ClientHealthCheck.ping(timeout: .seconds(10))
_ = try await ClientHealthCheck.ping(timeout: .seconds(10))
} catch {
throw ContainerizationError(
.internalError,
+4 -1
View File
@@ -41,8 +41,11 @@ extension Application {
// Now ping our friendly daemon. Fail after 10 seconds with no response.
do {
print("Verifying apiserver is running...")
try await ClientHealthCheck.ping(timeout: .seconds(10))
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
print("apiserver is running")
print("application data root: \(systemHealth.appRoot.path(percentEncoded: false))")
print("container-apiserver version: \(systemHealth.apiServerVersion)")
print("container-apiserver commit: \(systemHealth.apiServerCommit)")
} catch {
print("apiserver is not running")
Application.exit(withError: ExitCode(1))
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import ContainerXPC
import ContainerizationError
import Foundation
public enum ClientHealthCheck {
@@ -26,9 +27,19 @@ extension ClientHealthCheck {
XPCClient(service: serviceIdentifier)
}
public static func ping(timeout: Duration? = .seconds(5)) async throws {
public static func ping(timeout: Duration? = .seconds(5)) async throws -> SystemHealth {
let client = Self.newClient()
let request = XPCMessage(route: .ping)
try await client.send(request, responseTimeout: timeout)
let reply = try await client.send(request, responseTimeout: timeout)
guard let appRootValue = reply.string(key: .appRoot), let appRoot = URL(string: appRootValue) else {
throw ContainerizationError(.internalError, message: "failed to decode appRoot in health check")
}
guard let apiServerVersion = reply.string(key: .apiServerVersion) else {
throw ContainerizationError(.internalError, message: "failed to decode apiServerVersion in health check")
}
guard let apiServerCommit = reply.string(key: .apiServerCommit) else {
throw ContainerizationError(.internalError, message: "failed to decode apiServerCommit in health check")
}
return .init(appRoot: appRoot, apiServerVersion: apiServerVersion, apiServerCommit: apiServerCommit)
}
}
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
// 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 Foundation
/// Snapshot of the health of container services and resources
public struct SystemHealth: Sendable, Codable {
/// The full pathname of the application data root.
public let appRoot: URL
/// The release version of the container services.
public let apiServerVersion: String
/// The Git commit ID for the container services.
public let apiServerCommit: String
}
+3
View File
@@ -53,6 +53,9 @@ public enum XPCKeys: String {
/// Health check request.
case ping
case appRoot
case apiServerVersion
case apiServerCommit
/// Process request keys.
case signal
@@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
// 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 Foundation
public struct ApplicationRoot {
public static let environmentName = "CONTAINER_APP_ROOT"
public static let defaultURL = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!.appendingPathComponent("com.apple.container")
private static let envPath = ProcessInfo.processInfo.environment[Self.environmentName]
public static let url = envPath.map { URL(fileURLWithPath: $0) } ?? defaultURL
public static let path = url.path(percentEncoded: false)
}
+15 -10
View File
@@ -19,10 +19,7 @@ import Foundation
import Logging
public struct PluginLoader: Sendable {
// A path on disk managed by the PluginLoader, where it stores
// runtime data for loaded plugins. This includes the launchd plists
// and logs files.
private let defaultPluginResourcePath: URL
private let appRoot: URL
private let pluginDirectories: [URL]
@@ -32,7 +29,13 @@ public struct PluginLoader: Sendable {
public typealias PluginQualifier = ((Plugin) -> Bool)
public init(pluginDirectories: [URL], pluginFactories: [PluginFactory], defaultResourcePath: URL, log: Logger? = nil) {
// A path on disk managed by the PluginLoader, where it stores
// runtime data for loaded plugins. This includes the launchd plists
// and logs files.
private let defaultPluginResourcePath: URL
public init(appRoot: URL, pluginDirectories: [URL], pluginFactories: [PluginFactory], defaultResourcePath: URL, log: Logger? = nil) {
self.appRoot = appRoot
self.pluginDirectories = pluginDirectories
self.pluginFactories = pluginFactories
self.log = log
@@ -43,8 +46,8 @@ public struct PluginLoader: Sendable {
root.appending(path: "plugin-state")
}
static public func userPluginsDir(root: URL) -> URL {
root
static public func userPluginsDir(installRoot: URL) -> URL {
installRoot
.appending(path: "libexec")
.appending(path: "container-plugins")
.resolvingSymlinksInPath()
@@ -164,7 +167,7 @@ extension PluginLoader {
extension PluginLoader {
public func registerWithLaunchd(
plugin: Plugin,
rootURL: URL? = nil,
pluginStateRoot: URL? = nil,
args: [String]? = nil,
instanceId: String? = nil
) throws {
@@ -176,11 +179,13 @@ extension PluginLoader {
let id = plugin.getLaunchdLabel(instanceId: instanceId)
log?.info("Registering plugin", metadata: ["id": "\(id)"])
let rootURL = rootURL ?? self.defaultPluginResourcePath.appending(path: plugin.name)
let rootURL = pluginStateRoot ?? self.defaultPluginResourcePath.appending(path: plugin.name)
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
let env = ProcessInfo.processInfo.environment.filter { key, _ in
var env = ProcessInfo.processInfo.environment.filter { key, _ in
key.hasPrefix("CONTAINER_")
}
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
let logUrl = rootURL.appendingPathComponent("service.log")
let plist = LaunchPlist(
label: id,
+8 -13
View File
@@ -19,6 +19,7 @@ import CVersion
import ContainerImagesService
import ContainerImagesServiceClient
import ContainerLog
import ContainerPlugin
import ContainerXPC
import Containerization
import Foundation
@@ -49,16 +50,11 @@ extension ImagesHelper {
@Option(name: .long, help: "XPC service prefix")
var serviceIdentifier: String = "com.apple.container.core.container-core-images"
@Option(name: .shortAndLong, help: "Daemon root directory")
var root = Self.appRoot.path
static let appRoot: URL = {
FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
.appendingPathComponent("com.apple.container")
}()
@Option(
name: .shortAndLong,
help: "Application data directory",
transform: { URL(filePath: $0) })
var appRoot = ApplicationRoot.defaultURL
private static let unpackStrategy = SnapshotStore.defaultUnpackStrategy
@@ -71,10 +67,9 @@ extension ImagesHelper {
}
do {
log.info("configuring XPC server")
let root = URL(filePath: root)
var routes = [String: XPCServer.RouteHandler]()
try self.initializeContentService(root: root, log: log, routes: &routes)
try self.initializeImagesService(root: root, log: log, routes: &routes)
try self.initializeContentService(root: appRoot, log: log, routes: &routes)
try self.initializeImagesService(root: appRoot, log: log, routes: &routes)
let xpc = XPCServer(
identifier: serviceIdentifier,
routes: routes,
@@ -28,6 +28,7 @@ struct PluginLoaderTest {
defer { try? FileManager.default.removeItem(at: tempURL) }
let factory = try setupMock(tempURL: tempURL)
let loader = PluginLoader(
appRoot: URL(filePath: "/foo"),
pluginDirectories: [tempURL],
pluginFactories: [factory], defaultResourcePath: tempURL)
let plugins = loader.findPlugins()
@@ -41,6 +42,7 @@ struct PluginLoaderTest {
defer { try? FileManager.default.removeItem(at: tempURL) }
let factory = try setupMock(tempURL: tempURL)
let loader = PluginLoader(
appRoot: URL(filePath: "/foo"),
pluginDirectories: [tempURL],
pluginFactories: [factory], defaultResourcePath: tempURL
)