mirror of
https://github.com/apple/container.git
synced 2026-07-13 21:17:05 +00:00
edd6dee2db
- This fixes the LLVM coverage data not properly being emitted for XPC services. It requires piping the `LLVM_PROFILE_FILE` environment variable through to all the services and plugins. The variable itself also required the "%c" formatter to ensure that it continuously emits coverage data, otherwise when XPC services are killed via "bootout" they do not emit coverage.
285 lines
10 KiB
Swift
285 lines
10 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-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.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import Logging
|
|
import SystemPackage
|
|
|
|
public struct PluginLoader: Sendable {
|
|
private let appRoot: URL
|
|
|
|
private let installRoot: URL
|
|
|
|
private let logRoot: FilePath?
|
|
|
|
private let pluginDirectories: [URL]
|
|
|
|
private let pluginFactories: [PluginFactory]
|
|
|
|
private let log: Logger?
|
|
|
|
public typealias PluginQualifier = ((Plugin) -> Bool)
|
|
|
|
// 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 pluginResourceRoot: URL
|
|
|
|
public init(
|
|
appRoot: URL,
|
|
installRoot: URL,
|
|
logRoot: FilePath?,
|
|
pluginDirectories: [URL],
|
|
pluginFactories: [PluginFactory],
|
|
log: Logger? = nil
|
|
) throws {
|
|
let pluginResourceRoot = appRoot.appendingPathComponent("plugin-state")
|
|
try FileManager.default.createDirectory(at: pluginResourceRoot, withIntermediateDirectories: true)
|
|
self.pluginResourceRoot = pluginResourceRoot
|
|
self.appRoot = appRoot
|
|
self.installRoot = installRoot
|
|
self.logRoot = logRoot
|
|
self.pluginDirectories = pluginDirectories
|
|
self.pluginFactories = pluginFactories
|
|
self.log = log
|
|
}
|
|
|
|
static public func userPluginsDir(installRoot: URL) -> URL {
|
|
installRoot
|
|
.appending(path: "libexec")
|
|
.appending(path: "container-plugins")
|
|
.resolvingSymlinksInPath()
|
|
}
|
|
}
|
|
|
|
extension PluginLoader {
|
|
public func alterCLIHelpText(original: String) -> String {
|
|
var plugins = findPlugins()
|
|
plugins = plugins.filter { $0.config.isCLI }
|
|
guard !plugins.isEmpty else {
|
|
return original
|
|
}
|
|
|
|
var lines = original.split(separator: "\n").map { String($0) }
|
|
|
|
let sectionHeader = "PLUGINS:"
|
|
lines.append(sectionHeader)
|
|
|
|
for plugin in plugins {
|
|
let helpText = plugin.helpText(padding: 24)
|
|
lines.append(helpText)
|
|
}
|
|
|
|
return lines.joined(separator: "\n")
|
|
}
|
|
|
|
/// Scan all plugin directories and detect plugins.
|
|
public func findPlugins() -> [Plugin] {
|
|
let fm = FileManager.default
|
|
|
|
// Maintain a set for tracking shadowed plugins
|
|
var pluginNames = Set<String>()
|
|
var plugins: [Plugin] = []
|
|
|
|
for pluginDir in pluginDirectories {
|
|
// Skip nonexistent plugin parent directories
|
|
if !fm.fileExists(atPath: pluginDir.path) {
|
|
continue
|
|
}
|
|
|
|
// Get all entries under the parent directory
|
|
let resolvedPluginDir = pluginDir.resolvingSymlinksInPath()
|
|
guard
|
|
let urls = try? fm.contentsOfDirectory(
|
|
at: resolvedPluginDir,
|
|
includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey],
|
|
options: .skipsHiddenFiles
|
|
)
|
|
else {
|
|
continue
|
|
}
|
|
|
|
// Filter out all but plugin installation directories
|
|
let installURLs = urls.filter { url in
|
|
if url.isDirectory {
|
|
return true
|
|
}
|
|
|
|
if url.isSymlink {
|
|
var isDirectory: ObjCBool = false
|
|
_ = fm.fileExists(atPath: url.resolvingSymlinksInPath().path(percentEncoded: false), isDirectory: &isDirectory)
|
|
return isDirectory.boolValue
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
for installURL in installURLs {
|
|
do {
|
|
// Create a plugin with the first factory that can grok the layout under the install URL
|
|
guard
|
|
let plugin = try
|
|
(pluginFactories.compactMap {
|
|
try $0.create(installURL: installURL)
|
|
}.first)
|
|
else {
|
|
log?.warning(
|
|
"not installing plugin with missing configuration",
|
|
metadata: [
|
|
"path": "\(installURL.path)"
|
|
]
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Warn and skip if this plugin name has been encountered already
|
|
guard !pluginNames.contains(plugin.name) else {
|
|
log?.warning(
|
|
"not installing shadowed plugin",
|
|
metadata: [
|
|
"path": "\(installURL.path)",
|
|
"name": "\(plugin.name)",
|
|
])
|
|
continue
|
|
}
|
|
|
|
// Add the plugin to the list
|
|
plugins.append(plugin)
|
|
pluginNames.insert(plugin.name)
|
|
} catch {
|
|
log?.warning(
|
|
"not installing plugin with invalid configuration",
|
|
metadata: [
|
|
"path": "\(installURL.path)",
|
|
"error": "\(error)",
|
|
]
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return plugins
|
|
}
|
|
|
|
/// Locate a plugin with a specific name.
|
|
public func findPlugin(name: String, log: Logger? = nil) -> Plugin? {
|
|
do {
|
|
for pluginDirectory in pluginDirectories {
|
|
for PluginFactory in pluginFactories {
|
|
// throw means that the factory is correct but the plugin is broken
|
|
if let plugin = try PluginFactory.create(parentURL: pluginDirectory, name: name) {
|
|
return plugin
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
log?.warning(
|
|
"not installing plugin with invalid configuration",
|
|
metadata: [
|
|
"name": "\(name)",
|
|
"error": "\(error)",
|
|
]
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
extension PluginLoader {
|
|
public static let proxyKeys: Set<String> = {
|
|
var keys: Set<String> = [
|
|
"http_proxy", "HTTP_PROXY",
|
|
"https_proxy", "HTTPS_PROXY",
|
|
"no_proxy", "NO_PROXY",
|
|
]
|
|
#if CONTAINER_COVERAGE
|
|
// Allows LLVM coverage profiling data to be written by launchd-managed
|
|
// helper processes. Compiled in only for coverage enabled builds.
|
|
keys.insert("LLVM_PROFILE_FILE")
|
|
#endif
|
|
return keys
|
|
}()
|
|
|
|
public func registerWithLaunchd(
|
|
plugin: Plugin,
|
|
pluginStateRoot: URL? = nil,
|
|
args: [String]? = nil,
|
|
instanceId: String? = nil,
|
|
debug: Bool = false,
|
|
) throws {
|
|
// We only care about loading plugins that have a service
|
|
// to expose; otherwise, they may just be CLI commands.
|
|
guard let serviceConfig = plugin.config.servicesConfig else {
|
|
return
|
|
}
|
|
|
|
let id = plugin.getLaunchdLabel(instanceId: instanceId)
|
|
log?.info("Registering plugin", metadata: ["id": "\(id)"])
|
|
let rootURL = pluginStateRoot ?? self.pluginResourceRoot.appending(path: plugin.name)
|
|
let resourceURL = plugin.resourceURL
|
|
|
|
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
|
|
|
|
var env = Self.filterEnvironment()
|
|
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
|
|
env[InstallRoot.environmentName] = installRoot.path(percentEncoded: false)
|
|
if let logRoot {
|
|
env[LogRoot.environmentName] =
|
|
logRoot.isAbsolute
|
|
? logRoot.string
|
|
: FilePath(FileManager.default.currentDirectoryPath).appending(logRoot.components).string
|
|
}
|
|
|
|
let processedArgs = (args ?? ["start"]) + (resourceURL.map { ["--resources", $0.path] } ?? []) + (debug ? ["--debug"] : [])
|
|
let plist = LaunchPlist(
|
|
label: id,
|
|
arguments: [plugin.binaryURL.path] + processedArgs + serviceConfig.defaultArguments,
|
|
environment: env,
|
|
limitLoadToSessionType: [.Aqua, .Background, .System],
|
|
runAtLoad: serviceConfig.runAtLoad,
|
|
machServices: plugin.getMachServices(instanceId: instanceId)
|
|
)
|
|
|
|
let plistUrl = rootURL.appendingPathComponent("service.plist")
|
|
let data = try plist.encode()
|
|
try data.write(to: plistUrl)
|
|
try ServiceManager.register(plistPath: plistUrl.path)
|
|
}
|
|
|
|
public func deregisterWithLaunchd(plugin: Plugin, instanceId: String? = nil) throws {
|
|
// We only care about loading plugins that have a service
|
|
// to expose; otherwise, they may just be CLI commands.
|
|
guard plugin.config.servicesConfig != nil else {
|
|
return
|
|
}
|
|
let domain = try ServiceManager.getDomainString()
|
|
let label = "\(domain)/\(plugin.getLaunchdLabel(instanceId: instanceId))"
|
|
log?.info("Deregistering plugin", metadata: ["id": "\(plugin.getLaunchdLabel())"])
|
|
try ServiceManager.deregister(fullServiceLabel: label)
|
|
}
|
|
|
|
public static func filterEnvironment(
|
|
env: [String: String] = ProcessInfo.processInfo.environment,
|
|
additionalAllowKeys: Set<String> = Self.proxyKeys
|
|
) -> [String: String] {
|
|
env.filter { key, _ in
|
|
key.hasPrefix("CONTAINER_") || additionalAllowKeys.contains(key)
|
|
}
|
|
}
|
|
}
|