mirror of
https://github.com/apple/container.git
synced 2026-07-16 22:47:09 +00:00
23b058c53c
## Type of Change - [x] Bug fix ## Motivation and Context We setup logging for the services in `container` using OSLog. See [here](https://github.com/apple/container/blob/73709232d2705b7008b7380fe90a373059b6074a/Sources/Helpers/APIServer/APIServer.swift#L31). This makes it unnecessary to redirect stderr and stdio for these services. Additionally, there are some cases where failure to open or write to the path given for StandardErrorPath or StandardOutPath in a service's plist could result in a failure to start a service through `launchctl`. Related to https://github.com/apple/container/discussions/713 ## Testing - [x] Tested locally Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
248 lines
8.9 KiB
Swift
248 lines
8.9 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 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
|
|
|
|
public struct PluginLoader: Sendable {
|
|
private let appRoot: URL
|
|
|
|
private let installRoot: URL
|
|
|
|
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,
|
|
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.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 func registerWithLaunchd(
|
|
plugin: Plugin,
|
|
pluginStateRoot: URL? = nil,
|
|
args: [String]? = nil,
|
|
instanceId: String? = nil
|
|
) 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)
|
|
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
|
|
var env = ProcessInfo.processInfo.environment.filter { key, _ in
|
|
key.hasPrefix("CONTAINER_")
|
|
}
|
|
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
|
|
env[InstallRoot.environmentName] = installRoot.path(percentEncoded: false)
|
|
|
|
let plist = LaunchPlist(
|
|
label: id,
|
|
arguments: [plugin.binaryURL.path] + (args ?? 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)
|
|
}
|
|
}
|