Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ let package = Package(
.product(name: "ContainerizationOCI", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"ContainerNetworkService",
"ContainerPersistence",
"ContainerImagesServiceClient",
"TerminalProgress",
"ContainerNetworkService",
"ContainerPlugin",
"ContainerXPC",
"TerminalProgress",
]
),
.testTarget(
Expand Down
20 changes: 10 additions & 10 deletions Sources/APIServer/APIServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ struct APIServer: AsyncParsableCommand {

var appRoot = ApplicationRoot.url

var installRoot = InstallRoot.url

static func releaseVersion() -> String {
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
}
Expand Down Expand Up @@ -124,11 +126,11 @@ struct APIServer: AsyncParsableCommand {
}

private func initializePluginLoader(log: Logger) throws -> PluginLoader {
let installRoot = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
log.info("initializing plugin loader", metadata: ["installRoot": "\(installRoot.path(percentEncoded: false))"])
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))"])
Expand Down Expand Up @@ -162,13 +164,11 @@ struct APIServer: AsyncParsableCommand {
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(
return try PluginLoader(
appRoot: appRoot,
installRoot: installRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
defaultResourcePath: statePath,
log: log
)
}
Expand All @@ -194,7 +194,7 @@ struct APIServer: AsyncParsableCommand {
}

private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
let svc = HealthCheckHarness(appRoot: appRoot, log: log)
let svc = HealthCheckHarness(appRoot: appRoot, installRoot: installRoot, log: log)
routes[XPCRoute.ping] = svc.ping
}

Expand Down
5 changes: 4 additions & 1 deletion Sources/APIServer/HealthCheck/HealthCheckHarness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ import Logging

actor HealthCheckHarness {
private let appRoot: URL
private let installRoot: URL
private let log: Logger

public init(appRoot: URL, log: Logger) {
public init(appRoot: URL, installRoot: URL, log: Logger) {
self.appRoot = appRoot
self.installRoot = installRoot
self.log = log
}

@Sendable
func ping(_ message: XPCMessage) async -> XPCMessage {
let reply = message.reply()
reply.set(key: .appRoot, value: appRoot.absoluteString)
reply.set(key: .installRoot, value: installRoot.absoluteString)
reply.set(key: .apiServerVersion, value: APIServer.releaseVersion())
reply.set(key: .apiServerCommit, value: get_git_commit().map { String(cString: $0) } ?? "unknown")
return reply
Expand Down
13 changes: 5 additions & 8 deletions Sources/CLI/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,22 +158,19 @@ struct Application: AsyncParsableCommand {
installRootPluginsURL,
].compactMap { $0 }

let pluginFactories = [
DefaultPluginFactory()
let pluginFactories: [any PluginFactory] = [
DefaultPluginFactory(),
AppBundlePluginFactory(),
]

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(
return try PluginLoader(
appRoot: systemHealth.appRoot,
installRoot: systemHealth.installRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
defaultResourcePath: statePath,
log: log
)
}
Expand Down
19 changes: 12 additions & 7 deletions Sources/CLI/System/SystemStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ extension Application {
abstract: "Start `container` services"
)

@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
var appRoot = ApplicationRoot.defaultURL

@Option(
name: .long,
help: "Path to the installation root directory",
transform: { URL(filePath: $0) })
public var installRoot = InstallRoot.defaultURL

@Flag(name: .long, help: "Enable debug logging for the runtime daemon.")
var debug = false
Expand All @@ -48,10 +51,11 @@ extension Application {

func run() async throws {
// Without the true path to the binary in the plist, `container-apiserver` won't launch properly.
let executableUrl = URL(filePath: path)
.resolvingSymlinksInPath()
// TODO: Use plugin loader for API server.
let executableUrl = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("container-apiserver")
.resolvingSymlinksInPath()

var args = [executableUrl.absolutePath()]

Expand All @@ -64,7 +68,8 @@ extension Application {
var env = ProcessInfo.processInfo.environment.filter { key, _ in
key.hasPrefix("CONTAINER_")
}
env["CONTAINER_APP_ROOT"] = appRoot.path(percentEncoded: false)
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
env[InstallRoot.environmentName] = installRoot.path(percentEncoded: false)

let logURL = apiServerDataUrl.appending(path: "apiserver.log")
let plist = LaunchPlist(
Expand Down
2 changes: 1 addition & 1 deletion Sources/CLI/System/SystemStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ extension Application {

// Now ping our friendly daemon. Fail after 10 seconds with no response.
do {
print("Verifying apiserver is running...")
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
print("apiserver is running")
print("application data root: \(systemHealth.appRoot.path(percentEncoded: false))")
print("application install root: \(systemHealth.installRoot.path(percentEncoded: false))")
print("container-apiserver version: \(systemHealth.apiServerVersion)")
print("container-apiserver commit: \(systemHealth.apiServerCommit)")
} catch {
Expand Down
5 changes: 4 additions & 1 deletion Sources/ContainerClient/Core/ClientHealthCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ extension ClientHealthCheck {
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 installRootValue = reply.string(key: .installRoot), let installRoot = URL(string: installRootValue) else {
throw ContainerizationError(.internalError, message: "failed to decode installRoot 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)
return .init(appRoot: appRoot, installRoot: installRoot, apiServerVersion: apiServerVersion, apiServerCommit: apiServerCommit)
}
}
3 changes: 3 additions & 0 deletions Sources/ContainerClient/Core/SystemHealth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public struct SystemHealth: Sendable, Codable {
/// The full pathname of the application data root.
public let appRoot: URL

/// The full pathname of the application install root.
public let installRoot: URL

/// The release version of the container services.
public let apiServerVersion: String

Expand Down
1 change: 1 addition & 0 deletions Sources/ContainerClient/XPC+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public enum XPCKeys: String {
/// Health check request.
case ping
case appRoot
case installRoot
case apiServerVersion
case apiServerCommit

Expand Down
1 change: 1 addition & 0 deletions Sources/ContainerPlugin/ApplicationRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import Foundation

/// Provides the application data root path.
public struct ApplicationRoot {
public static let environmentName = "CONTAINER_APP_ROOT"

Expand Down
33 changes: 33 additions & 0 deletions Sources/ContainerPlugin/InstallRoot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
// 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

/// Provides the application installation root path.
public struct InstallRoot {
public static let environmentName = "CONTAINER_INSTALL_ROOT"

public static let defaultURL = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized

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)
}
12 changes: 11 additions & 1 deletion Sources/ContainerPlugin/PluginFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ private let configFilename: String = "config.json"

/// Describes the configuration and binary file locations for a plugin.
public protocol PluginFactory: Sendable {
/// Create a plugin conforming to the layout, if possible.
/// Create a plugin from the plugin path, if it conforms to the layout.
func create(installURL: URL) throws -> Plugin?
/// Create a plugin from the plugin parent path and name, if it conforms to the layout.
func create(parentURL: URL, name: String) throws -> Plugin?
}

/// Default layout which uses a Unix-like structure.
Expand All @@ -50,6 +52,10 @@ public struct DefaultPluginFactory: PluginFactory {

return Plugin(binaryURL: binaryURL, config: config)
}

public func create(parentURL: URL, name: String) throws -> Plugin? {
try create(installURL: parentURL.appendingPathComponent(name))
}
}

/// Layout which uses a macOS application bundle structure.
Expand Down Expand Up @@ -90,4 +96,8 @@ public struct AppBundlePluginFactory: PluginFactory {

return Plugin(binaryURL: binaryURL, config: config)
}

public func create(parentURL: URL, name: String) throws -> Plugin? {
try create(installURL: parentURL.appendingPathComponent("\(name)\(Self.appSuffix)"))
}
}
Loading