12.3. Long-Running Tasks#
12.3.1. Common Pitfalls#
1. Not properly detaching from terminal
# Bad: Process still tied to terminal, killed when shell exits
/usr/local/bin/daemon.sh &
# Better: Use nohup or daemonize
nohup /usr/local/bin/daemon.sh > /dev/null 2>&1 &
# Better: Use systemd
sudo systemctl start mydaemon
2. Zombie processes from not handling signals
# Bad: Daemon doesn't handle SIGTERM, becomes zombie
while true; do
work
sleep 10
done
# Good: Handle signals gracefully
RUNNING=true
trap 'RUNNING=false' TERM
while $RUNNING; do
work
sleep 10
done
3. PID file conflicts
# Bad: Multiple instances can run
# (no lock protection)
# Good: Use lock file
exec 200> "$PID_FILE"
flock -n 200 || {
echo "Already running" >&2
exit 1
}
echo $$ > "$PID_FILE"
4. Logs filling up disk
# Bad: Logs grow indefinitely
daemon_main() {
while true; do
echo "Log entry" >> /var/log/daemon.log
sleep 1
done
}
# Better: Use logrotate or journal
# Let systemd handle logging:
StandardOutput=journal
StandardError=journal
5. Resource leaks in daemon loops
# Bad: File descriptors leak over time
while true; do
exec 3< file.txt
process_file
# Never closed!
sleep 1
done
# Good: Proper resource cleanup
while true; do
exec 3< file.txt
process_file
exec 3>&- # Close file descriptor
sleep 1
done
12.3.2. Real-World Example: System Monitoring Daemon#
#!/bin/bash
set -euo pipefail
# SYSTEM MONITORING DAEMON
# Monitors CPU, memory, disk and alerts on issues
readonly DAEMON_NAME="sysmon"
readonly PID_FILE="/var/run/$DAEMON_NAME.pid"
readonly LOG_FILE="/var/log/$DAEMON_NAME.log"
readonly CONFIG_FILE="/etc/$DAEMON_NAME.conf"
readonly CHECK_INTERVAL=60
# Default configuration
CPU_THRESHOLD=90
MEMORY_THRESHOLD=85
DISK_THRESHOLD=90
ALERT_EMAIL="admin@example.com"
# Load configuration
load_config() {
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
}
# Check system resources
check_resources() {
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print int(100 - $8)}')
local mem_usage=$(free | awk 'NR==2 {print int($3/$2*100)}')
local disk_usage=$(df / | awk 'NR==2 {print int($5)}')
local alerts=""
if [[ $cpu_usage -gt $CPU_THRESHOLD ]]; then
alerts+="⚠ CPU: ${cpu_usage}%\n"
fi
if [[ $mem_usage -gt $MEMORY_THRESHOLD ]]; then
alerts+="⚠ Memory: ${mem_usage}%\n"
fi
if [[ $disk_usage -gt $DISK_THRESHOLD ]]; then
alerts+="⚠ Disk: ${disk_usage}%\n"
fi
if [[ -n "$alerts" ]]; then
{
echo "[$(date)] Alert triggered"
echo -e "$alerts"
} | tee -a "$LOG_FILE" | \
mail -s "System Alert" "$ALERT_EMAIL" 2>/dev/null || true
fi
}
# Signal handlers
DAEMON_RUNNING=true
handle_sigterm() {
DAEMON_RUNNING=false
}
trap handle_sigterm TERM INT
# Start daemon
daemon_main() {
load_config
echo "[$(date)] $DAEMON_NAME started" >> "$LOG_FILE"
while $DAEMON_RUNNING; do
check_resources
sleep "$CHECK_INTERVAL"
done
echo "[$(date)] $DAEMON_NAME stopped" >> "$LOG_FILE"
}
# Control functions
daemon_start() {
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
echo "$DAEMON_NAME is already running" >&2
return 1
fi
daemon_main &
echo $! > "$PID_FILE"
echo "$DAEMON_NAME started (PID: $!)"
}
daemon_stop() {
if [[ -f "$PID_FILE" ]]; then
local pid=$(cat "$PID_FILE")
kill "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
echo "$DAEMON_NAME stopped"
fi
}
daemon_status() {
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
echo "$DAEMON_NAME is running (PID: $(cat $PID_FILE))"
else
echo "$DAEMON_NAME is not running"
return 1
fi
}
# Main
case "${1:-start}" in
start) daemon_start;;
stop) daemon_stop;;
status) daemon_status;;
restart) daemon_stop; daemon_start;;
*) echo "Usage: $0 {start|stop|status|restart}" >&2; exit 1;;
esac
12.3.3. Systemd Services from Bash#
Modern systems use systemd for service management. You can create service files for Bash daemons.
12.3.3.1. Creating a Systemd Service#
# /etc/systemd/system/mydaemon.service
[Unit]
Description=My Custom Bash Daemon
After=network.target
Documentation=man:mydaemon(8)
[Service]
Type=simple
User=nobody
Group=nogroup
ExecStart=/usr/local/bin/mydaemon.sh start
ExecStop=/usr/local/bin/mydaemon.sh stop
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
12.3.3.2. Managing the Service#
# Reload systemd configuration
sudo systemctl daemon-reload
# Enable service (start on boot)
sudo systemctl enable mydaemon
# Start service
sudo systemctl start mydaemon
# Stop service
sudo systemctl stop mydaemon
# Check status
sudo systemctl status mydaemon
# View logs
sudo journalctl -u mydaemon -f
# Disable service
sudo systemctl disable mydaemon
12.3.4. Daemon Best Practices#
12.3.4.1. Proper Signal Handling#
#!/bin/bash
readonly DAEMON_NAME="monitoring_daemon"
readonly LOG_FILE="/var/log/$DAEMON_NAME.log"
# Global flag for graceful shutdown
DAEMON_RUNNING=true
# Handle signals
handle_sigterm() {
echo "[$(date)] Received SIGTERM, shutting down gracefully..." >> "$LOG_FILE"
DAEMON_RUNNING=false
}
handle_sighup() {
echo "[$(date)] Received SIGHUP, reloading config..." >> "$LOG_FILE"
# Reload configuration
}
trap handle_sigterm TERM
trap handle_sighup HUP
# Main daemon loop
daemon_main() {
while $DAEMON_RUNNING; do
# Do work
perform_monitoring >> "$LOG_FILE" 2>&1
# Check status periodically, not in tight loop
sleep 30
done
echo "[$(date)] Daemon exiting" >> "$LOG_FILE"
}
# Start daemon
daemon_main &
12.3.4.2. PID File Management#
#!/bin/bash
readonly PID_FILE="/var/run/mydaemon.pid"
# Lock PID file to prevent concurrent instances
acquire_lock() {
exec 200> "$PID_FILE"
flock -n 200 || {
echo "Another instance already running" >&2
return 1
}
echo $$ > "$PID_FILE"
return 0
}
# Release lock
release_lock() {
flock -u 200
rm -f "$PID_FILE"
}
trap release_lock EXIT
# Main code
acquire_lock || exit 1
# Daemon runs here...
12.3.5. Creating Simple Daemons#
A daemon is a long-running background process. Bash daemons are useful for monitoring, polling, or serving requests.
12.3.5.1. Basic Daemon Structure#
#!/bin/bash
# SIMPLE DAEMON TEMPLATE
# Monitors a resource and takes action
readonly DAEMON_NAME="mydaemon"
readonly PID_FILE="/var/run/$DAEMON_NAME.pid"
readonly LOG_FILE="/var/log/$DAEMON_NAME.log"
daemon_start() {
# Check if already running
if [[ -f "$PID_FILE" ]]; then
local pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "$DAEMON_NAME is already running (PID: $pid)" >&2
return 1
fi
fi
# Start daemon in background
daemon_main &
echo $! > "$PID_FILE"
echo "$DAEMON_NAME started (PID: $!)"
}
daemon_main() {
# Detach from terminal
nohup {
# Main loop
while true; do
do_work
sleep 10
done
} >> "$LOG_FILE" 2>&1 &
}
daemon_stop() {
if [[ -f "$PID_FILE" ]]; then
local pid=$(cat "$PID_FILE")
if kill "$pid" 2>/dev/null; then
rm -f "$PID_FILE"
echo "$DAEMON_NAME stopped"
fi
fi
}
daemon_status() {
if [[ -f "$PID_FILE" ]]; then
local pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "$DAEMON_NAME is running (PID: $pid)"
return 0
fi
fi
echo "$DAEMON_NAME is not running"
return 1
}
# Handle command
case "${1:-}" in
start) daemon_start;;
stop) daemon_stop;;
status) daemon_status;;
restart) daemon_stop; daemon_start;;
*) echo "Usage: $0 {start|stop|status|restart}" >&2; exit 1;;
esac