Published the 2022-01-14 on Willow's blog

How to manage shell daemons

While writing shell script, it is pretty common to need to manage some daemons.

Example inspired from sxmo-utils where we got a sxmo_modemmonitor.sh script that listen to dbus signals to dispatch notifications.

#!/bin/sh

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Voice',type='signal',member='CallAdded'" | \
	while read -r line; do
		notify-send "$line"
	done &

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Messaging',type='signal',member='Added'" | \
	while read -r line; do
		notify-send "$line"
	done &

This script got a huge issue. It will exit itself after reaching the end of the file. That mean you cannot control the two dbus-monitor subprocess anymore. You'll have to kill each of them manually.

#!/bin/sh

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Voice',type='signal',member='CallAdded'" | \
	while read -r line; do
		notify-send "$line"
	done &

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Messaging',type='signal',member='Added'" | \
	while read -r line; do
		notify-send "$line"
	done &

wait
wait

This is a better idea. The two wait will make the script to wait for subjobs to finish. But we still doesnt manage the subprocesses.

If you want to be able to clear subprocesses with Ctrl+c or with a kill signal you will do something like this:

#!/bin/sh

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Voice',type='signal',member='CallAdded'" | \
	while read -r line; do
		notify-send "$line"
	done &
PID1=$!

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Messaging',type='signal',member='Added'" | \
	while read -r line; do
		notify-send "$line"
	done &
PID2=$!

gracefulexit() {
	kill "$PID1"
	kill "$PID2"
	exit 0
}
trap "gracefulexit" INT TERM

wait "$PID1"
wait "$PID2"

This is the most common way to handle subprocess in shell scripts. But it got one main big issue:

`$!` is the pid of the while loop only, not the `dbus-monitor`, not the both of them.

In consequence, killing those PID in the trap handler will leave alone the dbus-monitors subprocesses unmanaged.

So how to manage those ? This is the clean way :

#!/bin/sh

FIFO1="/tmp/fifo1"
mkfifo "$FIFO1"

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Voice',type='signal',member='CallAdded'" \
	>> "$FIFO1" &
PID1=$!

while read -r line; do
	notify-send "$line"
done < "$FIFO1" &
PID2=$!

FIFO2="/tmp/fifo2"
mkfifo "$FIFO2"

dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Messaging',type='signal',member='Added'" \
	>> "$FIFO2" &
PID3=$!

while read -r line; do
	notify-send "$line"
done < "$FIFO2" &
PID4=$!

gracefulexit() {
	kill "$PID1"
	kill "$PID2"
	kill "$PID3"
	kill "$PID4"
	rm "$FIFO1"
	rm "$FIFO2"
	exit 0
}
trap "gracefulexit" INT TERM EXIT

wait "$PID1"
wait "$PID2"
wait "$PID3"
wait "$PID4"

This use named pipe fifo files to separate both commands and to grab each pids.

Here is two tips to make it simple and clean :

#!/bin/sh

daemon_pids_cache="$(mktemp)"
start_daemon() {
	"$@" &
	printf "%s\n" "$!" >> "$daemon_pids_cache"
}
stop_daemons() {
	while read -r PID; do
		kill "$PID"
	done < "$daemon_pids_cache"
	rm "$daemon_pids_cache"
}

PIDS=""

start_daemon dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Voice',type='signal',member='CallAdded'" | \
	while read -r line; do
		notify-send "$line"
	done &
PIDS="$PIDS $!"

start_daemon dbus-monitor --system "interface='org.freedesktop.ModemManager1.Modem.Messaging',type='signal',member='Added'" | \
	while read -r line; do
		notify-send "$line"
	done &
PIDS="$PIDS $!"

gracefulexit() {
	stop_daemons
	for PID in $PIDS; do
		kill "$PID"
	done
	exit 0
}
trap "gracefulexit" INT TERM EXIT

for PID in $PIDS; do
	wait "$PID"
done