Przejdź do treści

🍏 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

1
2
3
4
5
6
7
8
# 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

Service Status and Information

 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