How Container Integrates with Launchd for Service Management
Container integrates with Launchd by wrapping launchctl commands in a ServiceManager struct that registers XML property lists generated by the LaunchPlist model, enabling native macOS process lifecycle management.
The Apple Container project delegates all service supervision to macOS's native launchd system, avoiding custom process monitoring in favor of the platform's authoritative service manager. This architecture ensures that the Container API server, plugins, and helper processes benefit from Launchd's automatic restart, logging, and security sandboxing capabilities. The integration centers on two Swift components that bridge high-level container operations with low-level launchctl commands.
Core Integration Components
ServiceManager
The ServiceManager struct in Sources/ContainerPlugin/ServiceManager.swift serves as a thin Swift wrapper around the launchctl command-line tool. Rather than interfacing with Launchd directly via XPC, it abstracts common service lifecycle actions by spawning Process instances pointing at /bin/launchctl.
This wrapper implements five critical operations:
- Bootstrap (
register): Loads a property list into Launchd vialaunchctl bootstrap <domain> <plistPath>. - Bootout (
deregister): Stops and unloads a service usinglaunchctl bootout <label>. - Kickstart (
kickstart): Restarts a running service withlaunchctl kickstart -k <label>. - Kill (
kill): Sends signals to processes usinglaunchctl kill <signal> <label>. - Enumeration (
enumerate): Parses the output oflaunchctl listto return loaded service labels.
The struct also provides getDomainString(), which queries launchctl managername to determine the current session type—returning system, gui/<uid>, or user/<uid>—ensuring services load in the correct security context.
LaunchPlist
The LaunchPlist struct in Sources/ContainerPlugin/LaunchPlist.swift models the XML property list files that Launchd consumes. It conforms to Encodable and provides an encode() method that generates valid XML plist documents ready for the bootstrap command.
Key properties mapped to Launchd keys include:
Label: The unique service identifier.ProgramArguments: Array of executable path and arguments.EnvironmentVariables: Dictionary of environment values passed to the service.RunAtLoad: Boolean triggering immediate startup after bootstrap.MachServices: Dictionary enabling XPC communication between Container components.LimitLoadToSessionType: Constraints on which user sessions may load the service.
The struct also supports debugging via the CONTAINER_DEBUG_LAUNCHD_LABEL environment variable, which sets the waitForDebugger flag in the generated plist, causing Launchd to pause service startup until a debugger attaches.
Service Lifecycle Workflow
Container manages services through a six-step workflow that maps directly to Launchd primitives:
-
Determine Launchd Domain
ServiceManager.getDomainString()(lines 24-36 inServiceManager.swift) executeslaunchctl managernameto detect whether the current context isSystem,Aqua, orBackground, then returns the appropriate domain identifier for subsequent commands. -
Generate Property List
When starting a service (e.g., viaSources/ContainerCommands/System/SystemStart.swift), the code constructs aLaunchPlistinstance defining the service label, executable path, and runtime options likekeepAliveormachServices. Theencode()method writes this data to/var/run/container/launchd/<label>.plist. -
Register with Launchd
ServiceManager.register(plistPath:)invokeslaunchctl bootstrap <domain> <plistPath>, instructing Launchd to load the configuration and begin monitoring the service. -
Control Service State
Container sends lifecycle commands throughServiceManager:- Restart:
kickstart(label:)callslaunchctl kickstart -k <label>. - Stop:
deregister(fullServiceLabel:)callslaunchctl bootout <label>. - Signal:
kill(fullServiceLabel:, signal:)callslaunchctl kill <signal> <label>.
- Restart:
-
Query Status
ServiceManager.enumerate()runslaunchctl listand parses the third column to return active service labels.isRegistered(fullServiceLabel:)checks specific label status vialaunchctl list <label>. -
Cleanup on Shutdown
Thescripts/ensure-container-stopped.shscript useslaunchctl managernameto discover the current domain before iterating through container services and executingbootoutto ensure clean termination.
Practical Implementation Example
The following Swift example demonstrates registering a custom container service through Launchd:
import Foundation
import ContainerPlugin
// Configure the Launchd property list
let plist = LaunchPlist(
label: "com.example.container.myservice",
arguments: ["/usr/local/bin/myservice", "--port", "8080"],
runAtLoad: true,
keepAlive: true,
machServices: ["com.example.container.myservice"]
)
// Write the plist to the Launchd directory
let plistData = try plist.encode()
let plistPath = "/var/run/container/launchd/com.example.container.myservice.plist"
try plistData.write(to: URL(fileURLWithPath: plistPath))
// Register with the appropriate Launchd domain
try ServiceManager.register(plistPath: plistPath)
// Restart the service later if needed
let domain = try ServiceManager.getDomainString()
try ServiceManager.kickstart(fullServiceLabel: "\(domain)/com.example.container.myservice")
High-level CLI commands in Sources/ContainerCommands/System/SystemStart.swift and SystemStop.swift wrap these operations, allowing users to execute container start and container stop without manually interacting with launchctl.
Summary
- Container treats Launchd as the single source of truth for process lifecycle, using
ServiceManagerto wraplaunchctlcommands for bootstrap, bootout, andKill operations. - The
LaunchPliststruct inSources/ContainerPlugin/LaunchPlist.swifttype-safely generates XML property lists with support forMachServices, environment variables, and debugging flags. - Domain detection via
launchctl managernameensures services load in the correct security context (system, GUI, or user). - Services are registered by writing plists to
/var/run/container/launchd/and callinglaunchctl bootstrap, enabling automatic restart and monitoring without custom watchdog code.
Frequently Asked Questions
How does Container determine which Launchd domain to use for service registration?
Container calls launchctl managername through ServiceManager.getDomainString() to detect the current session type. This returns system for system-wide daemons, gui/<uid> for user sessions connected to the Aqua window server, or user/<uid> for background sessions, ensuring services run with appropriate permissions and visibility.
What is the difference between bootstrap and kickstart in Container's Launchd integration?
bootstrap (via ServiceManager.register) performs the initial registration of a property list with Launchd, creating the service entry and optionally starting it if RunAtLoad is true. kickstart (via ServiceManager.kickstart) forces an immediate restart of an already registered service, useful for reloading configurations without full deregistration.
Can Container services communicate via XPC, and how is this configured?
Yes, the LaunchPlist struct includes a machServices dictionary that populates the MachServices key in the generated plist. This enables XPC communication between the Container API server and its helper processes, with the service manager using these labels to route inter-process messages.
How can developers debug Container services that are managed by Launchd?
Set the CONTAINER_DEBUG_LAUNCHD_LABEL environment variable before starting the service. When present, LaunchPlist sets the waitForDebugger flag in the generated property list, causing Launchd to pause the service process immediately after startup until a debugger attaches via the specified Mach service label.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →