chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 Foundation
|
||||
import SystemPackage
|
||||
|
||||
/// Provides the application data root path.
|
||||
public struct ApplicationRoot {
|
||||
/// The environment variable that if set, determines the root directory for the application data store.
|
||||
/// Otherwise, the system uses the default "~/Library/Application Support/com.apple.container".
|
||||
public static let environmentName = "CONTAINER_APP_ROOT"
|
||||
|
||||
/// The default root directory used when ``environmentName`` is not set:
|
||||
/// `~/Library/Application Support/com.apple.container`.
|
||||
public static let defaultPath = FilePath(
|
||||
FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!.path(percentEncoded: false)
|
||||
)
|
||||
.appending(FilePath.Component("com.apple.container"))
|
||||
|
||||
/// The resolved root directory path, always lexically normalized.
|
||||
///
|
||||
/// If the environment variable is set to an absolute path, that path is used directly.
|
||||
/// If it is set to a relative path, the path is resolved against the working directory.
|
||||
/// Otherwise, ``defaultPath`` is used.
|
||||
public static let path = FilePath(FileManager.default.currentDirectoryPath).resolve(
|
||||
ProcessInfo.processInfo.environment[environmentName],
|
||||
defaultPath: defaultPath
|
||||
)
|
||||
|
||||
/// The pathname to the root directory
|
||||
public static let pathname = path.string
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 SystemPackage
|
||||
|
||||
extension FilePath {
|
||||
/// Resolves a pathname string relative to this path.
|
||||
///
|
||||
/// The result is lexically normalized — `.` components are removed and `..` components
|
||||
/// collapse the preceding component. Absolute pathnames are returned normalized as-is;
|
||||
/// relative pathnames are appended to `self` before normalizing.
|
||||
///
|
||||
/// - Parameter pathname: The pathname to resolve.
|
||||
/// - Returns: The resolved ``FilePath``, or `nil` if `pathname` is `nil` or empty.
|
||||
package func resolve(_ pathname: String?) -> FilePath? {
|
||||
guard let pathname, !pathname.isEmpty else { return nil }
|
||||
let path = FilePath(pathname)
|
||||
guard !path.isAbsolute else { return path.lexicallyNormalized() }
|
||||
return self.appending(path.components).lexicallyNormalized()
|
||||
}
|
||||
|
||||
/// Resolves a pathname string relative to this path, falling back to a default.
|
||||
///
|
||||
/// The result is lexically normalized — `.` components are removed and `..` components
|
||||
/// collapse the preceding component. Absolute pathnames are returned normalized as-is;
|
||||
/// relative pathnames are appended to `self` before normalizing.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - pathname: The pathname to resolve.
|
||||
/// - defaultPath: The path returned when `pathname` is `nil` or empty.
|
||||
/// - Returns: The resolved ``FilePath``, or `defaultPath` lexically normalized if `pathname` is `nil` or empty.
|
||||
package func resolve(_ pathname: String?, defaultPath: FilePath) -> FilePath {
|
||||
resolve(pathname) ?? defaultPath.lexicallyNormalized()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerVersion
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
/// Provides the application installation root path.
|
||||
public struct InstallRoot {
|
||||
/// The environment variable that if set, determines the root directory for installed application.
|
||||
/// Otherwise, the system computes the install path as the parent of the directory containing the
|
||||
/// application binary (for example, "/usr/local/bin/container" -> "/usr/local").
|
||||
public static let environmentName = "CONTAINER_INSTALL_ROOT"
|
||||
|
||||
/// The default root directory used when the environment variable is not set.
|
||||
///
|
||||
/// Computed as the grandparent of ``CommandLine/executablePath``
|
||||
/// (for example, `/usr/local/bin/container` → `/usr/local`).
|
||||
/// Lexically normalized but not canonical, as symlinks in the executable path are not resolved.
|
||||
public static let defaultPath = CommandLine.executablePath
|
||||
.removingLastComponent()
|
||||
.removingLastComponent()
|
||||
|
||||
/// The resolved root directory path, always lexically normalized.
|
||||
///
|
||||
/// If the environment variable is set to an absolute path, that path is used directly.
|
||||
/// If it is set to a relative path, the path is resolved against the working directory.
|
||||
/// Otherwise, ``defaultPath`` is used.
|
||||
public static let path = FilePath(FileManager.default.currentDirectoryPath).resolve(
|
||||
ProcessInfo.processInfo.environment[environmentName],
|
||||
defaultPath: defaultPath
|
||||
)
|
||||
|
||||
/// The pathname to the root directory
|
||||
public static let pathname = path.string
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
|
||||
public struct LaunchPlist: Encodable {
|
||||
static let debugTarget = "CONTAINER_DEBUG_LAUNCHD_LABEL"
|
||||
|
||||
public enum Domain: String, Codable {
|
||||
case Aqua
|
||||
case Background
|
||||
case System
|
||||
}
|
||||
|
||||
public let label: String
|
||||
public let arguments: [String]
|
||||
|
||||
public let environment: [String: String]?
|
||||
public let cwd: String?
|
||||
public let username: String?
|
||||
public let groupname: String?
|
||||
public let limitLoadToSessionType: [Domain]?
|
||||
public let runAtLoad: Bool?
|
||||
public let stdin: String?
|
||||
public let stdout: String?
|
||||
public let stderr: String?
|
||||
public let disabled: Bool?
|
||||
public let program: String?
|
||||
public let keepAlive: Bool?
|
||||
public let machServices: [String: Bool]?
|
||||
public let waitForDebugger: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case label = "Label"
|
||||
case arguments = "ProgramArguments"
|
||||
case environment = "EnvironmentVariables"
|
||||
case cwd = "WorkingDirectory"
|
||||
case username = "UserName"
|
||||
case groupname = "GroupName"
|
||||
case limitLoadToSessionType = "LimitLoadToSessionType"
|
||||
case runAtLoad = "RunAtLoad"
|
||||
case stdin = "StandardInPath"
|
||||
case stdout = "StandardOutPath"
|
||||
case stderr = "StandardErrorPath"
|
||||
case disabled = "Disabled"
|
||||
case program = "Program"
|
||||
case keepAlive = "KeepAlive"
|
||||
case machServices = "MachServices"
|
||||
case waitForDebugger = "WaitForDebugger"
|
||||
}
|
||||
|
||||
static private func getWaitForDebugger(label: String, fromArg: Bool?) -> Bool? {
|
||||
if let fromArg {
|
||||
return fromArg
|
||||
}
|
||||
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
if let debugTarget = env[Self.debugTarget],
|
||||
label == debugTarget || label.starts(with: debugTarget + ".")
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public init(
|
||||
label: String,
|
||||
arguments: [String],
|
||||
environment: [String: String]? = nil,
|
||||
cwd: String? = nil,
|
||||
username: String? = nil,
|
||||
groupname: String? = nil,
|
||||
limitLoadToSessionType: [Domain]? = nil,
|
||||
runAtLoad: Bool? = nil,
|
||||
stdin: String? = nil,
|
||||
stdout: String? = nil,
|
||||
stderr: String? = nil,
|
||||
disabled: Bool? = nil,
|
||||
program: String? = nil,
|
||||
keepAlive: Bool? = nil,
|
||||
machServices: [String]? = nil,
|
||||
waitForDebugger: Bool? = nil
|
||||
) {
|
||||
self.label = label
|
||||
self.arguments = arguments
|
||||
self.environment = environment
|
||||
self.cwd = cwd
|
||||
self.username = username
|
||||
self.groupname = groupname
|
||||
self.limitLoadToSessionType = limitLoadToSessionType
|
||||
self.runAtLoad = runAtLoad
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.disabled = disabled
|
||||
self.program = program
|
||||
self.keepAlive = keepAlive
|
||||
self.waitForDebugger = Self.getWaitForDebugger(label: label, fromArg: waitForDebugger)
|
||||
if let services = machServices {
|
||||
var machServices: [String: Bool] = [:]
|
||||
for service in services {
|
||||
machServices[service] = true
|
||||
}
|
||||
self.machServices = machServices
|
||||
} else {
|
||||
self.machServices = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LaunchPlist {
|
||||
public func encode() throws -> Data {
|
||||
let enc = PropertyListEncoder()
|
||||
enc.outputFormat = .xml
|
||||
return try enc.encode(self)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Foundation
|
||||
import SystemPackage
|
||||
|
||||
/// Provides the application data root path.
|
||||
public struct LogRoot {
|
||||
/// The environment variable that if set, determines the root directory for log files.
|
||||
/// Otherwise, the application uses the macOS log facility.
|
||||
public static let environmentName = "CONTAINER_LOG_ROOT"
|
||||
|
||||
/// The resolved root directory for log files, or `nil` if the environment variable is not set.
|
||||
///
|
||||
/// When non-nil, the path is always lexically normalized.
|
||||
/// If the environment variable is set to an absolute path, that path is used directly.
|
||||
/// If it is set to a relative path, the path is resolved against the working directory.
|
||||
public static let path = FilePath(FileManager.default.currentDirectoryPath).resolve(
|
||||
ProcessInfo.processInfo.environment[environmentName]
|
||||
)
|
||||
|
||||
/// The pathname to the root directory
|
||||
public static let pathname = path?.string
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 Foundation
|
||||
|
||||
/// Value type that contains the plugin configuration, the parsed name of the
|
||||
/// plugin and whether a CLI surface for the plugin was found.
|
||||
public struct Plugin: Sendable, Codable {
|
||||
private static let machServicePrefix = "com.apple.container."
|
||||
|
||||
/// Pathname to installation directory for plugins.
|
||||
public let binaryURL: URL
|
||||
|
||||
/// Configuration for the plugin.
|
||||
public let config: PluginConfig
|
||||
|
||||
/// Pathname to resources directory for plugins.
|
||||
public let resourceURL: URL?
|
||||
|
||||
public init(binaryURL: URL, config: PluginConfig, resourceURL: URL? = nil) {
|
||||
self.binaryURL = binaryURL
|
||||
self.config = config
|
||||
self.resourceURL = resourceURL
|
||||
}
|
||||
}
|
||||
|
||||
extension Plugin {
|
||||
public var name: String { binaryURL.lastPathComponent }
|
||||
|
||||
public var shouldBoot: Bool {
|
||||
guard let config = self.config.servicesConfig else {
|
||||
return false
|
||||
}
|
||||
|
||||
return config.loadAtBoot
|
||||
}
|
||||
|
||||
public func getLaunchdLabel(instanceId: String? = nil) -> String {
|
||||
// Use the plugin name for the launchd label.
|
||||
guard let instanceId else {
|
||||
return "\(Self.machServicePrefix)\(self.name)"
|
||||
}
|
||||
return "\(Self.machServicePrefix)\(self.name).\(instanceId)"
|
||||
}
|
||||
|
||||
public func getMachServices(instanceId: String? = nil) -> [String] {
|
||||
// Use the service type for the mach service.
|
||||
guard let config = self.config.servicesConfig else {
|
||||
return []
|
||||
}
|
||||
var services = [String]()
|
||||
for service in config.services {
|
||||
let serviceName: String
|
||||
if let instanceId {
|
||||
serviceName = "\(Self.machServicePrefix)\(service.type.rawValue).\(name).\(instanceId)"
|
||||
} else {
|
||||
serviceName = "\(Self.machServicePrefix)\(service.type.rawValue).\(name)"
|
||||
}
|
||||
services.append(serviceName)
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
public func getMachService(instanceId: String? = nil, type: PluginConfig.DaemonPluginType) -> String? {
|
||||
guard hasType(type) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let instanceId else {
|
||||
return "\(Self.machServicePrefix)\(type.rawValue).\(name)"
|
||||
}
|
||||
return "\(Self.machServicePrefix)\(type.rawValue).\(name).\(instanceId)"
|
||||
}
|
||||
|
||||
public func hasType(_ type: PluginConfig.DaemonPluginType) -> Bool {
|
||||
guard let config = self.config.servicesConfig else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard !(config.services.filter { $0.type == type }.isEmpty) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Plugin {
|
||||
public func exec(args: [String]) throws {
|
||||
var args = args
|
||||
let executable = self.binaryURL.path
|
||||
args[0] = executable
|
||||
let argv = args.map { strdup($0) } + [nil]
|
||||
guard execvp(executable, argv) != -1 else {
|
||||
throw POSIXError.fromErrno()
|
||||
}
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
func helpText(padding: Int) -> String {
|
||||
guard !self.name.isEmpty else {
|
||||
return ""
|
||||
}
|
||||
let namePadded = name.padding(toLength: padding, withPad: " ", startingAt: 0)
|
||||
return " " + namePadded + self.config.abstract
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 Foundation
|
||||
import TOML
|
||||
|
||||
/// PluginConfig details all of the fields to describe and register a plugin.
|
||||
/// A plugin is registered by creating a subdirectory `<application-root>/user-plugins`,
|
||||
/// where the name of the subdirectory is the name of the plugin, and then placing a
|
||||
/// file named `config.toml` (or fall back to the legacy `config.json` file) inside with
|
||||
/// the schema below. If `services` is filled in then there MUST be a binary named
|
||||
/// matching the plugin name in a `bin` subdirectory inside the same directory as
|
||||
/// the `config.toml`. An example of a valid plugin directory structure would be
|
||||
/// $ tree foobar
|
||||
/// foobar
|
||||
/// ├── bin
|
||||
/// │ └── foobar
|
||||
/// └── config.toml (`config.json`)
|
||||
public struct PluginConfig: Sendable, Codable {
|
||||
/// Categories of services that can be offered through plugins.
|
||||
public enum DaemonPluginType: String, Sendable, Codable {
|
||||
/// A runtime plugin provides an XPC API through which the lifecycle
|
||||
/// of a **single** container can be managed.
|
||||
/// A runtime daemon plugin would typically also have a counterpart
|
||||
/// CLI plugin which knows how to talk to the API exposed by the runtime plugin.
|
||||
/// The API server ensures that a single instance of the plugin is configured
|
||||
/// for a given container such that the client can communicate with it given an instance id.
|
||||
case runtime
|
||||
/// A network plugin provides an XPC API through which IP address allocations on a given
|
||||
/// network can be managed. The API server ensures that a single instance
|
||||
/// of this plugin is configured for a given network. Similar to the runtime plugin, it typically
|
||||
/// would be accompanied by a CLI plugin that knows how to communicate with the XPC API
|
||||
/// given an instance id.
|
||||
case network
|
||||
/// A core plugin provides an XPC API to manage a given type of resource.
|
||||
/// The API server ensures that there exist only a single running instance
|
||||
/// of this plugin type. A core plugin can be thought of a singleton whose lifecycle
|
||||
/// is tied to that of the API server. Core plugins can be used to expand the base functionality
|
||||
/// provided by the API server. As with the other plugin types, it maybe associated with a client
|
||||
/// side plugin that communicates with the XPC service exposed by the daemon plugin.
|
||||
case core
|
||||
/// Reserved for future use. Currently there is no difference between a core and auxiliary daemon plugin.
|
||||
case auxiliary
|
||||
}
|
||||
|
||||
// An XPC service that the plugin publishes.
|
||||
public struct Service: Sendable, Codable {
|
||||
/// The type of the service the daemon is exposing.
|
||||
/// One plugin can expose multiple services of different types.
|
||||
///
|
||||
/// The plugin MUST expose a MachService at
|
||||
/// `com.apple.container.{type}.{name}.[{id}]` for
|
||||
/// each service that it exposes.
|
||||
public let type: DaemonPluginType
|
||||
/// Optional description of this service.
|
||||
public let description: String?
|
||||
}
|
||||
|
||||
/// Descriptor for the services that the plugin offers.
|
||||
public struct ServicesConfig: Sendable, Codable {
|
||||
/// Load the plugin into launchd when the API server starts.
|
||||
public let loadAtBoot: Bool
|
||||
/// Launch the plugin binary as soon as it loads into launchd.
|
||||
public let runAtLoad: Bool
|
||||
/// The service types that the plugin provides.
|
||||
public let services: [Service]
|
||||
/// An optional parameter that include any command line arguments
|
||||
/// that must be passed to the plugin binary when it is loaded.
|
||||
public let defaultArguments: [String]
|
||||
}
|
||||
|
||||
/// Short description of the plugin surface. This will be displayed as the
|
||||
/// help-text for CLI plugins, and will be returned in API calls to view loaded
|
||||
/// plugins from the daemon.
|
||||
public let abstract: String
|
||||
|
||||
/// Author of the plugin. This is solely metadata.
|
||||
public let author: String?
|
||||
|
||||
/// Services configuration. Specify nil for a CLI plugin, and an empty array for
|
||||
/// that does not publish any XPC services.
|
||||
public let servicesConfig: ServicesConfig?
|
||||
|
||||
public init(abstract: String, author: String?, servicesConfig: ServicesConfig?) {
|
||||
self.abstract = abstract
|
||||
self.author = author
|
||||
self.servicesConfig = servicesConfig
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginConfig {
|
||||
public var isCLI: Bool { self.servicesConfig == nil }
|
||||
}
|
||||
|
||||
extension PluginConfig {
|
||||
/// Initialize from a config file, selecting the decoder based on file extension.
|
||||
/// Supports `.toml` (via TOMLDecoder) and `.json` (via JSONDecoder).
|
||||
public init?(configURL: URL) throws {
|
||||
let fm = FileManager.default
|
||||
if !fm.fileExists(atPath: configURL.path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let data = fm.contents(atPath: configURL.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch configURL.pathExtension {
|
||||
case "toml":
|
||||
guard let content = String(data: data, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
self = try TOMLDecoder().decode(PluginConfig.self, from: content)
|
||||
case "json":
|
||||
self = try JSONDecoder().decode(PluginConfig.self, from: data)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 Foundation
|
||||
import Logging
|
||||
|
||||
/// Describes the configuration and binary file locations for a plugin.
|
||||
public protocol PluginFactory: Sendable {
|
||||
/// 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.
|
||||
public struct DefaultPluginFactory: PluginFactory {
|
||||
// Order matters: earlier entries take priority during config file discovery.
|
||||
private static let configFilenames: [String] = ["config.toml", "config.json"]
|
||||
private let logger: Logger
|
||||
|
||||
public init(logger: Logger) {
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
/// Returns the URL of the first config file found in `directory`, preferring TOML over JSON.
|
||||
static func findConfigURL(in directory: URL, logger: Logger) -> URL? {
|
||||
let fm = FileManager.default
|
||||
for filename in configFilenames {
|
||||
let url = directory.appending(path: filename)
|
||||
if fm.fileExists(atPath: url.path) {
|
||||
if url.pathExtension == "json" {
|
||||
logger.warning(
|
||||
"Plugin using legacy config.json; please migrate to config.toml",
|
||||
metadata: ["path": "\(url.path)"]
|
||||
)
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func create(installURL: URL) throws -> Plugin? {
|
||||
let fm = FileManager.default
|
||||
|
||||
guard let configURL = Self.findConfigURL(in: installURL, logger: logger) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let config = try PluginConfig(configURL: configURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let name = installURL.lastPathComponent
|
||||
let binaryURL = installURL.appending(path: "bin").appending(path: name)
|
||||
guard fm.fileExists(atPath: binaryURL.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resourceURL: URL? = nil
|
||||
if case let url = installURL.appending(path: "resources"), fm.fileExists(atPath: url.path) {
|
||||
resourceURL = url
|
||||
}
|
||||
return Plugin(binaryURL: binaryURL, config: config, resourceURL: resourceURL)
|
||||
}
|
||||
|
||||
public func create(parentURL: URL, name: String) throws -> Plugin? {
|
||||
try create(installURL: parentURL.appendingPathComponent(name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout which uses a macOS application bundle structure.
|
||||
public struct AppBundlePluginFactory: PluginFactory {
|
||||
private static let appSuffix = ".app"
|
||||
private let logger: Logger
|
||||
|
||||
public init(logger: Logger) {
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
public func create(installURL: URL) throws -> Plugin? {
|
||||
let fm = FileManager.default
|
||||
|
||||
let contentResources =
|
||||
installURL
|
||||
.appending(path: "Contents")
|
||||
.appending(path: "Resources")
|
||||
|
||||
guard let configURL = DefaultPluginFactory.findConfigURL(in: contentResources, logger: logger) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let config = try PluginConfig(configURL: configURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let appName = installURL.lastPathComponent
|
||||
guard appName.hasSuffix(Self.appSuffix) else {
|
||||
return nil
|
||||
}
|
||||
let name = String(appName.dropLast(Self.appSuffix.count))
|
||||
let binaryURL =
|
||||
installURL
|
||||
.appending(path: "Contents")
|
||||
.appending(path: "MacOS")
|
||||
.appending(path: name)
|
||||
guard fm.fileExists(atPath: binaryURL.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resourceURL: URL? = nil
|
||||
if case let url = contentResources.appending(path: "resources"), fm.fileExists(atPath: url.path) {
|
||||
resourceURL = url
|
||||
}
|
||||
|
||||
return Plugin(binaryURL: binaryURL, config: config, resourceURL: resourceURL)
|
||||
}
|
||||
|
||||
public func create(parentURL: URL, name: String) throws -> Plugin? {
|
||||
try create(installURL: parentURL.appendingPathComponent("\(name)\(Self.appSuffix)"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 ContainerizationError
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
public struct PluginStateRoot {
|
||||
private let plugin: FilePath.Component
|
||||
|
||||
public init(plugin: String) throws {
|
||||
guard let plugin = FilePath.Component(plugin) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid plugin name \(plugin)")
|
||||
}
|
||||
self.plugin = plugin
|
||||
}
|
||||
|
||||
public var path: FilePath {
|
||||
ApplicationRoot.path
|
||||
.appending(FilePath.Component("plugin-state"))
|
||||
.appending(plugin)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerizationError
|
||||
import Foundation
|
||||
|
||||
public struct ServiceManager {
|
||||
private static func runLaunchctlCommand(args: [String]) throws -> Int32 {
|
||||
let launchctl = Foundation.Process()
|
||||
launchctl.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
||||
launchctl.arguments = args
|
||||
|
||||
let null = FileHandle.nullDevice
|
||||
launchctl.standardOutput = null
|
||||
launchctl.standardError = null
|
||||
|
||||
try launchctl.run()
|
||||
launchctl.waitUntilExit()
|
||||
|
||||
return launchctl.terminationStatus
|
||||
}
|
||||
|
||||
/// Register a service by providing the path to a plist.
|
||||
public static func register(plistPath: String) throws {
|
||||
let domain = try Self.getDomainString()
|
||||
_ = try runLaunchctlCommand(args: ["bootstrap", domain, plistPath])
|
||||
}
|
||||
|
||||
/// Deregister a service by a launchd label.
|
||||
public static func deregister(fullServiceLabel label: String) throws {
|
||||
_ = try runLaunchctlCommand(args: ["bootout", label])
|
||||
}
|
||||
|
||||
/// Deregister a service and pass return status
|
||||
public static func deregister(fullServiceLabel label: String, status: inout Int32) throws {
|
||||
status = try runLaunchctlCommand(args: ["bootout", label])
|
||||
}
|
||||
|
||||
/// Restart a service by a launchd label.
|
||||
public static func kickstart(fullServiceLabel label: String) throws {
|
||||
_ = try runLaunchctlCommand(args: ["kickstart", "-k", label])
|
||||
}
|
||||
|
||||
/// Send a signal to a service by a launchd label.
|
||||
public static func kill(fullServiceLabel label: String, signal: Int32 = 15) throws {
|
||||
_ = try runLaunchctlCommand(args: ["kill", "\(signal)", label])
|
||||
}
|
||||
|
||||
/// Retrieve labels for all loaded launch units.
|
||||
public static func enumerate() throws -> [String] {
|
||||
let launchctl = Foundation.Process()
|
||||
launchctl.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
||||
launchctl.arguments = ["list"]
|
||||
|
||||
let stdoutPipe = Pipe()
|
||||
let stderrPipe = Pipe()
|
||||
launchctl.standardOutput = stdoutPipe
|
||||
launchctl.standardError = stderrPipe
|
||||
|
||||
try launchctl.run()
|
||||
let outputData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
launchctl.waitUntilExit()
|
||||
let status = launchctl.terminationStatus
|
||||
guard status == 0 else {
|
||||
throw ContainerizationError(
|
||||
.internalError, message: "command `launchctl list` failed with status \(status), message: \(String(data: stderrData, encoding: .utf8) ?? "no error message")")
|
||||
}
|
||||
|
||||
guard let outputText = String(data: outputData, encoding: .utf8) else {
|
||||
throw ContainerizationError(
|
||||
.internalError, message: "could not decode output of command `launchctl list`, message: \(String(data: stderrData, encoding: .utf8) ?? "no error message")")
|
||||
}
|
||||
|
||||
// The third field of each line of launchctl list output is the label
|
||||
return outputText.split { $0.isNewline }
|
||||
.map { String($0).split { $0.isWhitespace } }
|
||||
.filter { $0.count >= 3 }
|
||||
.map { String($0[2]) }
|
||||
}
|
||||
|
||||
/// Check if a service has been registered or not.
|
||||
public static func isRegistered(fullServiceLabel label: String) throws -> Bool {
|
||||
let exitStatus = try runLaunchctlCommand(args: ["list", label])
|
||||
return exitStatus == 0
|
||||
}
|
||||
|
||||
private static func getLaunchdSessionType() throws -> String {
|
||||
let launchctl = Foundation.Process()
|
||||
launchctl.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
||||
launchctl.arguments = ["managername"]
|
||||
|
||||
let null = FileHandle.nullDevice
|
||||
let stdoutPipe = Pipe()
|
||||
launchctl.standardOutput = stdoutPipe
|
||||
launchctl.standardError = null
|
||||
|
||||
try launchctl.run()
|
||||
let outputData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
launchctl.waitUntilExit()
|
||||
let status = launchctl.terminationStatus
|
||||
guard status == 0 else {
|
||||
throw ContainerizationError(.internalError, message: "command `launchctl managername` failed with status \(status)")
|
||||
}
|
||||
guard let outputText = String(data: outputData, encoding: .utf8) else {
|
||||
throw ContainerizationError(.internalError, message: "could not decode output of command `launchctl managername`")
|
||||
}
|
||||
return outputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
public static func getDomainString() throws -> String {
|
||||
let currentSessionType = try getLaunchdSessionType()
|
||||
switch currentSessionType {
|
||||
case LaunchPlist.Domain.System.rawValue:
|
||||
return LaunchPlist.Domain.System.rawValue.lowercased()
|
||||
case LaunchPlist.Domain.Background.rawValue:
|
||||
return "user/\(getuid())"
|
||||
case LaunchPlist.Domain.Aqua.rawValue:
|
||||
return "gui/\(getuid())"
|
||||
default:
|
||||
throw ContainerizationError(.internalError, message: "unsupported session type \(currentSessionType)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user