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