🍏 macOS Launchd and Services
Launchd is macOS's unified service management framework that replaces traditional Unix init systems, cron, and inetd. Understanding launchd is essential for managing services and automating tasks on macOS.
🎯 Launchd Architecture Overview
Launchd Components
Launchd serves as the unified service management system for macOS, handling:
- System daemons
- User agents
- Scheduled jobs (replacing cron)
- On-demand services (replacing inetd)
- Event-triggered tasks
| # Launchd process hierarchy
# PID 1: /sbin/launchd (master process)
# Child processes: All system services and user processes
# Launchd domains
# System: /System/Library/LaunchDaemons/, /Library/LaunchDaemons/
# User: ~/Library/LaunchAgents/
# Session: Per-login session agents
|
Launchd vs Traditional Unix Services
1
2
3
4
5
6
7
8
9
10
11
12
13 | # Traditional Unix approach
# init/cron/inetd for different service types
# macOS launchd unified approach
# Single framework for all service management
# Service definition files
# Unix: Various configuration formats
# macOS: Unified plist format
# Process management
# Unix: Manual process control
# macOS: Automatic process management
|
🔧 Launchd Configuration Files
Property List (plist) Structure
Launchd services are defined using XML property list files with standardized keys.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required Keys -->
<key>Label</key>
<string>com.example.myservice</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/myservice</string>
<string>--config</string>
<string>/etc/myservice.conf</string>
</array>
<!-- Optional Keys -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>/var/lib/myservice</string>
<key>StandardOutPath</key>
<string>/var/log/myservice.log</string>
<key>StandardErrorPath</key>
<string>/var/log/myservice.err</string>
</dict>
</plist>
|
Common Launchd Keys
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | # Essential Keys
Label # Unique service identifier
ProgramArguments # Command and arguments to execute
RunAtLoad # Start service at load time
# Startup Control
KeepAlive # Restart if service exits
SuccessfulExit # Keep alive based on exit code
Crashed # Restart if crashed
NetworkState # Start when network available
# Triggering Events
StartInterval # Run at regular intervals (seconds)
StartCalendarInterval # Run at specific calendar times
WatchPaths # Run when files change
QueueDirectories # Run when directories have files
# Environment and Resources
EnvironmentVariables # Set environment variables
WorkingDirectory # Set working directory
Umask # Set file creation mask
# I/O Management
StandardOutPath # Redirect stdout
StandardErrorPath # Redirect stderr
SoftResourceLimits # Resource limits
HardResourceLimits # Hard resource limits
|
📋 Service Management Commands
Basic Launchctl Operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # List services
launchctl list # List user services
sudo launchctl list # List system services
launchctl list | grep com.example # Filter specific services
# Load/unload services
launchctl load ~/Library/LaunchAgents/com.example.service.plist
launchctl unload ~/Library/LaunchAgents/com.example.service.plist
sudo launchctl load /Library/LaunchDaemons/com.example.daemon.plist
sudo launchctl unload /Library/LaunchDaemons/com.example.daemon.plist
# Start/stop services
launchctl start com.example.service
launchctl stop com.example.service
# Enable/disable services
launchctl enable gui/$(id -u)/com.example.service
launchctl disable gui/$(id -u)/com.example.service
# Bootstrap/destroy services (macOS 10.10+)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.service.plist
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.example.service.plist
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | # Detailed service information
launchctl print gui/$(id -u)/com.example.service
sudo launchctl print system/com.example.daemon
# Service state checking
launchctl kickstart -p com.example.service # Check if running
launchctl kill SIGTERM gui/$(id -u)/com.example.service # Send signal
# System-wide information
launchctl dumpstate # Dump all launchd state
sudo launchctl manageruid # Show manager UID
launchctl managername # Show manager name
# Performance monitoring
launchctl procinfo pid # Process information
launchctl blame service # Show service load time
|
🛠️ Creating Launchd Services
User Agent Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.backup-agent</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/scripts/backup.sh</string>
</array>
<key>RunAtLoad</key>
<false/>
<key>StartInterval</key>
<integer>3600</integer> <!-- Run every hour -->
<key>WorkingDirectory</key>
<string>/Users/username</string>
<key>StandardOutPath</key>
<string>/Users/username/Library/Logs/backup-agent.log</string>
<key>StandardErrorPath</key>
<string>/Users/username/Library/Logs/backup-agent.err</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>ThrottleInterval</key>
<integer>30</integer> <!-- Minimum time between launches -->
</dict>
</plist>
|
System Daemon Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.webserver</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/nginx</string>
<string>-c</string>
<string>/usr/local/etc/nginx/nginx.conf</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>UserName</key>
<string>_www</string>
<key>GroupName</key>
<string>_www</string>
<key>WorkingDirectory</key>
<string>/usr/local/var</string>
<key>StandardOutPath</key>
<string>/usr/local/var/log/nginx.log</string>
<key>StandardErrorPath</key>
<string>/usr/local/var/log/nginx.err</string>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockServiceName</key>
<string>http</string>
</dict>
</dict>
</dict>
</plist>
|
📊 Scheduled Tasks (Cron Replacement)
Calendar-Triggered Jobs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.daily-report</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/scripts/daily-report.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/username/Library/Logs/daily-report.log</string>
<key>StandardErrorPath</key>
<string>/Users/username/Library/Logs/daily-report.err</string>
</dict>
</plist>
|
Complex Calendar Scheduling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | <!-- Multiple schedule times -->
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>17</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
<!-- Weekly schedule -->
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>1</integer> <!-- Monday -->
<key>Hour</key>
<integer>8</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<!-- Monthly schedule -->
<key>StartCalendarInterval</key>
<dict>
<key>Day</key>
<integer>1</integer> <!-- First day of month -->
<key>Hour</key>
<integer>0</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
|
🔍 Event-Triggered Services
File System Monitoring
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.file-watcher</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/scripts/process-files.sh</string>
</array>
<key>WatchPaths</key>
<array>
<string>/Users/username/Downloads</string>
</array>
<key>QueueDirectories</key>
<array>
<string>/Users/username/ProcessingQueue</string>
</array>
<key>StandardOutPath</key>
<string>/Users/username/Library/Logs/file-watcher.log</string>
</dict>
</plist>
|
Network State Triggers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.network-service</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/scripts/network-up.sh</string>
</array>
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
<true/>
</dict>
<key>StandardOutPath</key>
<string>/Users/username/Library/Logs/network-service.log</string>
</dict>
</plist>
|
🎨 Advanced Launchd Features
On-Demand Network Services
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.ssh-proxy</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-D</string>
<string>1080</string>
<string>-N</string>
<string>proxy-server</string>
</array>
<key>Sockets</key>
<dict>
<key>SOCKSProxy</key>
<dict>
<key>SockNodeName</key>
<string>localhost</string>
<key>SockServiceName</key>
<string>1080</string>
<key>SockType</key>
<string>stream</string>
</dict>
</dict>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
<key>SessionCreate</key>
<true/>
</dict>
</plist>
|
Resource Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.resource-limited-service</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/memory-intensive-app</string>
</array>
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>1024</integer>
<key>NumberOfProcesses</key>
<integer>50</integer>
</dict>
<key>HardResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>2048</integer>
</dict>
<key>EnvironmentVariables</key>
<dict>
<key>MALLOC_LIMIT</key>
<string>100M</string>
</dict>
<key>Umask</key>
<integer>022</integer>
</dict>
</plist>
|
🧪 Service Management Scripts
Service Deployment Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 | #!/bin/bash
# deploy-service.sh - Deploy launchd service
SERVICE_NAME="$1"
SERVICE_PLIST="$2"
SERVICE_TYPE="$3" # agent or daemon
if [ -z "$SERVICE_NAME" ] || [ -z "$SERVICE_PLIST" ]; then
echo "Usage: $0 <service-name> <plist-file> [agent|daemon]"
exit 1
fi
# Set default service type
SERVICE_TYPE="${SERVICE_TYPE:-agent}"
# Validate plist file
if [ ! -f "$SERVICE_PLIST" ]; then
echo "Error: Plist file not found: $SERVICE_PLIST"
exit 1
fi
# Validate service name matches plist
PLIST_LABEL=$(defaults read "$SERVICE_PLIST" Label 2>/dev/null)
if [ "$PLIST_LABEL" != "$SERVICE_NAME" ]; then
echo "Warning: Service name ($SERVICE_NAME) doesn't match plist label ($PLIST_LABEL)"
fi
# Deploy based on type
case "$SERVICE_TYPE" in
agent)
TARGET_DIR="$HOME/Library/LaunchAgents"
BOOTSTRAP_DOMAIN="gui/$(id -u)"
;;
daemon)
TARGET_DIR="/Library/LaunchDaemons"
BOOTSTRAP_DOMAIN="system"
if [ "$EUID" -ne 0 ]; then
echo "Error: Root privileges required for daemon deployment"
exit 1
fi
;;
*)
echo "Error: Invalid service type. Use 'agent' or 'daemon'"
exit 1
;;
esac
# Create target directory
mkdir -p "$TARGET_DIR"
# Copy plist file
cp "$SERVICE_PLIST" "$TARGET_DIR/"
chmod 644 "$TARGET_DIR/$(basename "$SERVICE_PLIST")"
# Load service
if launchctl bootstrap "$BOOTSTRAP_DOMAIN" "$TARGET_DIR/$(basename "$SERVICE_PLIST")"; then
echo "Service $SERVICE_NAME deployed successfully"
else
echo "Error: Failed to deploy service $SERVICE_NAME"
exit 1
fi
# Show service status
launchctl print "$BOOTSTRAP_DOMAIN/$SERVICE_NAME"
|
Service Monitoring Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | #!/bin/bash
# monitor-services.sh - Monitor launchd services
SERVICES_FILE="${1:-/etc/monitor-services.list}"
# Default services to monitor
DEFAULT_SERVICES=(
"com.apple.finder"
"com.apple.Dock"
"com.apple.Safari"
)
# Read services from file or use defaults
if [ -f "$SERVICES_FILE" ]; then
readarray -t SERVICES < "$SERVICES_FILE"
else
SERVICES=("${DEFAULT_SERVICES[@]}")
fi
echo "=== Launchd Service Monitor ==="
echo "Timestamp: $(date)"
echo
for service in "${SERVICES[@]}"; do
echo "Service: $service"
# Check if service is loaded
if launchctl list | grep -q "$service"; then
echo " Status: Loaded"
# Get service PID if running
service_info=$(launchctl list | grep "$service")
pid=$(echo "$service_info" | awk '{print $1}')
if [ "$pid" != "-" ]; then
echo " PID: $pid"
echo " Process: $(ps -p "$pid" -o comm= 2>/dev/null || echo 'Unknown')"
else
echo " Status: Not Running"
fi
else
echo " Status: Not Loaded"
fi
echo
done
# System-wide statistics
echo "=== System Statistics ==="
echo "Total Services: $(launchctl list | wc -l)"
echo "Running Services: $(launchctl list | grep -v '-' | wc -l)"
echo "Load Average: $(sysctl -n vm.loadavg | tr -d '{}')"
|
🔧 Troubleshooting
Common Launchd Issues
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | # Service won't start
# 1. Check plist syntax
plutil -lint ~/Library/LaunchAgents/service.plist
# 2. Check service logs
tail -f ~/Library/Logs/service.log
# 3. Check launchd errors
sudo launchctl error $(launchctl list | grep service | awk '{print $3}')
# Service crashes frequently
# 1. Check crash logs
log show --predicate 'eventMessage contains "service"' --last 1h
# 2. Adjust KeepAlive settings
# Consider using SuccessfulExit instead of simple KeepAlive
# Service loads but doesn't run
# 1. Verify ProgramArguments path
# 2. Check working directory permissions
# 3. Validate environment variables
# Permission issues
# 1. Check plist file permissions (644)
# 2. Verify executable permissions
# 3. Check user/group settings for daemons
|
Debugging Commands
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | # Enable verbose logging
sudo launchctl log level debug
# Monitor launchd activity
log stream --predicate 'subsystem == "com.apple.launchd"'
# Check service configuration
launchctl print gui/$(id -u)/service-name
# Validate plist files
plutil -lint /path/to/service.plist
# Show service dependencies
launchctl print-cache
# Reset launchd cache
sudo launchctl remove service-name
sudo launchctl load service.plist
|
🧾 Summary Commands
Essential Launchd Commands
1
2
3
4
5
6
7
8
9
10
11
12 | # Service management
launchctl load/unload plist-file # Load/unload service
launchctl start/stop service-name # Start/stop service
launchctl list # List services
# Modern commands (macOS 10.10+)
launchctl bootstrap/bootout domain path # Bootstrap/destroy
launchctl enable/disable service # Enable/disable
# Information gathering
launchctl print domain/service # Detailed service info
plutil -lint plist-file # Validate plist
|
Service Deployment Checklist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | # Service deployment checklist
deploy_service_checklist() {
local service_name="$1"
local plist_file="$2"
echo "=== Service Deployment Checklist ==="
# 1. Validate plist syntax
if plutil -lint "$plist_file" >/dev/null; then
echo "✓ Plist syntax valid"
else
echo "✗ Plist syntax invalid"
return 1
fi
# 2. Check service name consistency
local plist_label
plist_label=$(defaults read "$plist_file" Label 2>/dev/null)
if [ "$plist_label" = "$service_name" ]; then
echo "✓ Service name consistent"
else
echo "⚠ Service name mismatch: $plist_label vs $service_name"
fi
# 3. Verify executable exists
local program
program=$(defaults read "$plist_file" Program 2>/dev/null)
if [ -n "$program" ] && [ ! -x "$program" ]; then
echo "✗ Program not executable: $program"
return 1
fi
# 4. Check working directory
local work_dir
work_dir=$(defaults read "$plist_file" WorkingDirectory 2>/dev/null)
if [ -n "$work_dir" ] && [ ! -d "$work_dir" ]; then
echo "✗ Working directory not found: $work_dir"
fi
echo "✓ Service ready for deployment"
}
|
🧾 See Also