Supervised FreeBSD rc.d script for a Go daemon

In this short post, I will show you how to create a FreeBSD rc.d script for a Go program. To make it more interesting, this daemon will be supervised, i.e. if the program terminates for some reason (for example because of an error), the Go program will be restarted.

Ingredients

To begin with, I recommend reading (the excellent) practical rc.d scripting in BSD. FreeBSD rc.d scripts are usually a very simple /bin/sh scripts. What allows the rc.d scripts to remain simple is rc.subr framework, that takes care of more advanced things like managing dependencies.

With rc.subr, we can handle most of the use cases, but because we want to supervise the daemon, we will use the daemon utility.

rc.d script

Let’s see an example rc.d script. Since we use the /usr/sbin/daemon utility to manage the daemon’s life cycle, we can not rely on rc.subr’s pid file creation mechanism, and start, stop, and status procedures. We need to write these three procedures ourselves.

The most important part of the rc.d script is goprogram_start and /usr/sbin/daemon -P ${pidfile} -r -f -u $goprogram_user $command, where we say we want to create a parent pid file (the -P flag), we want the goprogram to be restarted when it exits (the -r flag) and we want it to be run as user goprogram (the -u flag). The goprogram_stop andgoprogram_status procedures have to use the newly created pid file.

#!/bin/sh
#
# PROVIDE: goprogram
# REQUIRE: networking
# KEYWORD: 
 
. /etc/rc.subr
 
name="goprogram"
rcvar="goprogram_enable"
command="/usr/local/goprogram/goprogram"
goprogram_user="goprogram"
pidfile="/var/run/${name}.pid"
 
start_cmd="goprogram_start"
stop_cmd="goprogram_stop"
status_cmd="goprogram_status"

goprogram_start() {
        /usr/sbin/daemon -P ${pidfile} -r -f -u $goprogram_user $command
}

goprogram_stop() {
        if [ -e "${pidfile}" ]; then
                kill -s TERM `cat ${pidfile}`
        else
                echo "${name} is not running"
        fi

}

goprogram_status() {
        if [ -e "${pidfile}" ]; then
                echo "${name} is running as pid `cat ${pidfile}`"
        else
                echo "${name} is not running"
        fi
}
 
load_rc_config $name
: ${goprogram_enable:=no}
 
run_rc_command "$1"

Do not forget to run chmod +x /usr/local/etc/rc.d/goprogram and add goprogram_enable='YES' to /etc/rc.conf file.

Edit: As swills and Freeky pointed out in a reddit discussion, there is a more elegant approach (thank you guys):

#!/bin/sh
#
# PROVIDE: goprogram
# REQUIRE: networking
# KEYWORD:

. /etc/rc.subr

name="goprogram"
rcvar="goprogram_enable"
goprogram_user="goprogram"
goprogram_command="/usr/local/goprogram/goprogram"
pidfile="/var/run/goprogram/${name}.pid"
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f ${goprogram_command}"

load_rc_config $name
: ${goprogram_enable:=no}

run_rc_command "$1"

Test it

Let’s try out our new rc.d script. First, start the newly created daemon:

service goprogram start

Looking at the running processes we see that both, the supervisor and the supervised program are running:

ps aux
USER        PID %CPU %MEM   VSZ   RSS TT  STAT STARTED    TIME COMMAND
goprogram 49036  0.0  0.0 10468  1640  -  SsJ  Mon11   0:00.06 daemon: /usr/local/goprogram/goprogram[49037] (daemon)
goprogram 49037  0.0  0.0 41572 12840  -  IJ   Mon11   0:42.54 /usr/local/goprogram/goprogram

Let’s see what happens when we forcibly kill the supervised program:

kill -9 49037
ps aux
USER        PID %CPU %MEM   VSZ  RSS TT  STAT STARTED    TIME COMMAND
goprogram 49036  0.0  0.0 10468 1644  -  SsJ  Mon11   0:00.07 daemon: /usr/local/goprogram/goprogram[49691] (daemon)
goprogram 49691  0.0  0.0 30940 8788  -  SJ   09:54   0:00.01 /usr/local/goprogram/goprogram

The supervisor detected that the supervised program with the original pid 49037 had exited and restarted the supervised program. The pid of the newly crated instance is 49691.