Skip to content

Commit 6242706

Browse files
authored
Fixes for install root and plugin detection. (#467)
- Sets up API server as source of truth for installation root, similarly to what was done for the data root. `system start` establishes the install root, setting the environment variable `CONTAINER_INSTALL_ROOT` when launching the API server. - The API server propagates the environment variable when launching helpers, and returns the install root to the CLI via the health check XPC. - Includes several fixes for detecting plugins that use app bundle layout.
1 parent d242864 commit 6242706

File tree

16 files changed

+287
-58
lines changed

16 files changed

+287
-58
lines changed

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,11 @@ let package = Package(
223223
.product(name: "ContainerizationOCI", package: "containerization"),
224224
.product(name: "ContainerizationOS", package: "containerization"),
225225
.product(name: "ArgumentParser", package: "swift-argument-parser"),
226-
"ContainerNetworkService",
227-
"ContainerPersistence",
228226
"ContainerImagesServiceClient",
229-
"TerminalProgress",
227+
"ContainerNetworkService",
228+
"ContainerPlugin",
230229
"ContainerXPC",
230+
"TerminalProgress",
231231
]
232232
),
233233
.testTarget(

Sources/APIServer/APIServer.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ struct APIServer: AsyncParsableCommand {
4545

4646
var appRoot = ApplicationRoot.url
4747

48+
var installRoot = InstallRoot.url
49+
4850
static func releaseVersion() -> String {
4951
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
5052
}
@@ -124,11 +126,11 @@ struct APIServer: AsyncParsableCommand {
124126
}
125127

126128
private func initializePluginLoader(log: Logger) throws -> PluginLoader {
127-
let installRoot = CommandLine.executablePathUrl
128-
.deletingLastPathComponent()
129-
.appendingPathComponent("..")
130-
.standardized
131-
log.info("initializing plugin loader", metadata: ["installRoot": "\(installRoot.path(percentEncoded: false))"])
129+
log.info(
130+
"initializing plugin loader",
131+
metadata: [
132+
"installRoot": "\(installRoot.path(percentEncoded: false))"
133+
])
132134

133135
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
134136
log.info("detecting user plugins directory", metadata: ["path": "\(pluginsURL.path(percentEncoded: false))"])
@@ -162,13 +164,11 @@ struct APIServer: AsyncParsableCommand {
162164
log.info("discovered plugin directory", metadata: ["path": "\(pluginDirectory.path(percentEncoded: false))"])
163165
}
164166

165-
let statePath = PluginLoader.defaultPluginResourcePath(root: appRoot)
166-
try FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)
167-
return PluginLoader(
167+
return try PluginLoader(
168168
appRoot: appRoot,
169+
installRoot: installRoot,
169170
pluginDirectories: pluginDirectories,
170171
pluginFactories: pluginFactories,
171-
defaultResourcePath: statePath,
172172
log: log
173173
)
174174
}
@@ -194,7 +194,7 @@ struct APIServer: AsyncParsableCommand {
194194
}
195195

196196
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
197-
let svc = HealthCheckHarness(appRoot: appRoot, log: log)
197+
let svc = HealthCheckHarness(appRoot: appRoot, installRoot: installRoot, log: log)
198198
routes[XPCRoute.ping] = svc.ping
199199
}
200200

Sources/APIServer/HealthCheck/HealthCheckHarness.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@ import Logging
2323

2424
actor HealthCheckHarness {
2525
private let appRoot: URL
26+
private let installRoot: URL
2627
private let log: Logger
2728

28-
public init(appRoot: URL, log: Logger) {
29+
public init(appRoot: URL, installRoot: URL, log: Logger) {
2930
self.appRoot = appRoot
31+
self.installRoot = installRoot
3032
self.log = log
3133
}
3234

3335
@Sendable
3436
func ping(_ message: XPCMessage) async -> XPCMessage {
3537
let reply = message.reply()
3638
reply.set(key: .appRoot, value: appRoot.absoluteString)
39+
reply.set(key: .installRoot, value: installRoot.absoluteString)
3740
reply.set(key: .apiServerVersion, value: APIServer.releaseVersion())
3841
reply.set(key: .apiServerCommit, value: get_git_commit().map { String(cString: $0) } ?? "unknown")
3942
return reply

Sources/CLI/Application.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,22 +158,19 @@ struct Application: AsyncParsableCommand {
158158
installRootPluginsURL,
159159
].compactMap { $0 }
160160

161-
let pluginFactories = [
162-
DefaultPluginFactory()
161+
let pluginFactories: [any PluginFactory] = [
162+
DefaultPluginFactory(),
163+
AppBundlePluginFactory(),
163164
]
164165

165166
guard let systemHealth = try? await ClientHealthCheck.ping(timeout: .seconds(10)) else {
166167
throw ContainerizationError(.timeout, message: "unable to retrieve application data root from API server")
167168
}
168-
let statePath = PluginLoader.defaultPluginResourcePath(root: systemHealth.appRoot)
169-
guard (try? FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)) != nil else {
170-
throw ContainerizationError(.invalidState, message: "unable to create plugin state directory")
171-
}
172-
return PluginLoader(
169+
return try PluginLoader(
173170
appRoot: systemHealth.appRoot,
171+
installRoot: systemHealth.installRoot,
174172
pluginDirectories: pluginDirectories,
175173
pluginFactories: pluginFactories,
176-
defaultResourcePath: statePath,
177174
log: log
178175
)
179176
}

Sources/CLI/System/SystemStart.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ extension Application {
2929
abstract: "Start `container` services"
3030
)
3131

32-
@Option(name: .shortAndLong, help: "Path to the `container-apiserver` binary")
33-
var path: String = Bundle.main.executablePath ?? ""
34-
3532
@Option(
3633
name: .shortAndLong,
3734
help: "Application data directory",
3835
transform: { URL(filePath: $0) })
39-
var appRoot: URL = ApplicationRoot.defaultURL
36+
var appRoot = ApplicationRoot.defaultURL
37+
38+
@Option(
39+
name: .long,
40+
help: "Path to the installation root directory",
41+
transform: { URL(filePath: $0) })
42+
public var installRoot = InstallRoot.defaultURL
4043

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

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

5660
var args = [executableUrl.absolutePath()]
5761

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

6974
let logURL = apiServerDataUrl.appending(path: "apiserver.log")
7075
let plist = LaunchPlist(

Sources/CLI/System/SystemStatus.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ extension Application {
4040

4141
// Now ping our friendly daemon. Fail after 10 seconds with no response.
4242
do {
43-
print("Verifying apiserver is running...")
4443
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
4544
print("apiserver is running")
4645
print("application data root: \(systemHealth.appRoot.path(percentEncoded: false))")
46+
print("application install root: \(systemHealth.installRoot.path(percentEncoded: false))")
4747
print("container-apiserver version: \(systemHealth.apiServerVersion)")
4848
print("container-apiserver commit: \(systemHealth.apiServerCommit)")
4949
} catch {

Sources/ContainerClient/Core/ClientHealthCheck.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ extension ClientHealthCheck {
3434
guard let appRootValue = reply.string(key: .appRoot), let appRoot = URL(string: appRootValue) else {
3535
throw ContainerizationError(.internalError, message: "failed to decode appRoot in health check")
3636
}
37+
guard let installRootValue = reply.string(key: .installRoot), let installRoot = URL(string: installRootValue) else {
38+
throw ContainerizationError(.internalError, message: "failed to decode installRoot in health check")
39+
}
3740
guard let apiServerVersion = reply.string(key: .apiServerVersion) else {
3841
throw ContainerizationError(.internalError, message: "failed to decode apiServerVersion in health check")
3942
}
4043
guard let apiServerCommit = reply.string(key: .apiServerCommit) else {
4144
throw ContainerizationError(.internalError, message: "failed to decode apiServerCommit in health check")
4245
}
43-
return .init(appRoot: appRoot, apiServerVersion: apiServerVersion, apiServerCommit: apiServerCommit)
46+
return .init(appRoot: appRoot, installRoot: installRoot, apiServerVersion: apiServerVersion, apiServerCommit: apiServerCommit)
4447
}
4548
}

Sources/ContainerClient/Core/SystemHealth.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public struct SystemHealth: Sendable, Codable {
2121
/// The full pathname of the application data root.
2222
public let appRoot: URL
2323

24+
/// The full pathname of the application install root.
25+
public let installRoot: URL
26+
2427
/// The release version of the container services.
2528
public let apiServerVersion: String
2629

Sources/ContainerClient/XPC+.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public enum XPCKeys: String {
5454
/// Health check request.
5555
case ping
5656
case appRoot
57+
case installRoot
5758
case apiServerVersion
5859
case apiServerCommit
5960

Sources/ContainerPlugin/ApplicationRoot.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import Foundation
1818

19+
/// Provides the application data root path.
1920
public struct ApplicationRoot {
2021
public static let environmentName = "CONTAINER_APP_ROOT"
2122

0 commit comments

Comments
 (0)