Daemonizing Bash

A daemon is a program that runs in the background. It sounds simple, but becoming a proper daemon requires disconnecting from the controlling terminal, protecting against signals, and closing file descriptors.

This post explores how to do that in pure Bash.

It’s a bad idea

There are better languages for writing daemons than Bash. Almost any other language, really. Writing a daemon implies sufficient complexity that your program has already outgrown Bash, with or without daemonization.

Additionally there are some worthy external tools, for example daemonize will run a given program as a daemon, it’s available in Linux, and it works fine for Bash scripts.

But that doesn’t mean it isn’t fun

It was a bad idea to bicycle down that hill with a sweatshirt looped around the handlebars. Did that stop you? Of course not. And your bike mostly survived the crash, and you eventually regained consciousness, and the parts you can remember make a good story, so having learned that lesson, let’s carry on.

Five steps to daemonhood

A program that wants to daemonize itself has two primary tasks: Fork to the background to return control to the shell, and prevent undesirable interaction between the process and the host. Rich Stevens enumerated the detailed steps in his classic Advanced Programming in the UNIX Environment. Here’s my summary of his formula with implementation notes for Bash.

Step 1: Fork process

Call fork (to guarantee the child is not a process group leader, necessary for setsid) and have the parent exit (to allow control to return to the shell).

Forking in Bash means putting a command in the background using &.

To put a sequence of commands in the background, we can use a subshell ( commands ) & or a shell function:

childfunc() {
  do cool stuff here
}

# Run the function in a forked process.
childfunc &

Step 2: Disconnect terminal

Call setsid to create a new session so the child has no controlling terminal. This simultaneously prevents the child from gaining access to the controlling terminal (using /dev/tty) and protects the child from signals originating from the controlling terminal (HUP and INT, for example).

Bash provides no method to call the setsid syscall for the current process. We have two less-than-ideal alternatives:

  1. The util-linux package provides an external setsid command but this daemonizes an external program rather than the currently running script. It also makes collecting the PID of the child tricky because the setsid command will fork internally.

    Having said all that, if your application allows you to use the setsid command, it’s a good choice because Bash can’t otherwise fully protect against the child process opening /dev/tty. It’s still a good idea to redirect std* to prevent stray output to the terminal.

  2. Lacking the setsid syscall, there are steps we can take to partially protect the child process from the effects of the controlling terminal:

    1. Redirect std* to files or /dev/null
    2. Guard against HUP and INT by signal handler in child
    3. Guard against HUP by disown -h in parent

    Unfortunately without setsid there is no way to guard completely against a subchild opening /dev/tty until the terminal emulator exits, then /dev/tty will become unavailable.

Step 3: Change directory

Change working directory to / to prevent the daemon from holding a mounted filesystem open.

Finally, something Bash is really suited for: cd 😂

Step 4: Clear umask

Set umask to 0 to clear file mode creation mask.

I have to admit that I can’t understand the point of this, in Bash or any other language. It seems to me that the child will either set its umask explicitly before creating files, or it will set individual file permissions explicitly, or it will fall back on the caller’s umask. In the last case, I want my inherited umask, not the wide-open zero.

If anybody wants to explain a good reason for step 4, I’m all ears… Until then, it’s commented out in my implementation below.

Step 5: Close files

Close unneeded file descriptors.

This step is fun in Bash using eval and brace expansion:

eval exec {3..255}\>\&-

Implementation

With those notes in-hand, here’s my implementation. There are two functions here, daemonize for an external command using setsid, daemonize-job for a function in the running script.

# redirect tty fds to /dev/null
redirect-std() {
  [[ -t 0 ]] && exec </dev/null
  [[ -t 1 ]] && exec >/dev/null
  [[ -t 2 ]] && exec 2>/dev/null
}

# close all non-std* fds
close-fds() {
  eval exec {3..255}\>\&-
}

# full daemonization of external command with setsid
daemonize() {
  (                   # 1. fork
    redirect-std      # 2.1. redirect stdin/stdout/stderr before setsid
    cd /              # 3. ensure cwd isn't a mounted fs
    # umask 0         # 4. umask (leave this to caller)
    close-fds         # 5. close unneeded fds
    exec setsid "$@"
  ) &
}

# daemonize without setsid, keeps the child in the jobs table
daemonize-job() {
  (                   # 1. fork
    redirect-std      # 2.2.1. redirect stdin/stdout/stderr
    trap '' 1 2       # 2.2.2. guard against HUP and INT (in child)
    cd /              # 3. ensure cwd isn't a mounted fs
    # umask 0         # 4. umask (leave this to caller)
    close-fds         # 5. close unneeded fds
    if [[ $(type -t "$1") != file ]]; then
      "$@"
    else
      exec "$@"
    fi
  ) &
  disown -h $!       # 2.2.3. guard against HUP (in parent)
}

Originally posted on the n01se blog.