Merge pull request 'fix(rc.d): supervisor-aware stop + bring clawdie.in to parity (Sam & Claude)' (#18) from fix/rc.d-stop-supervisor into main

Reviewed-on: #18
This commit is contained in:
clawdie 2026-06-04 10:50:35 +02:00
commit 3aee4637d8
2 changed files with 137 additions and 13 deletions

View file

@ -9,9 +9,8 @@
# /usr/local/etc/clawdie/clawdie.env) or set them in rc.conf below. # /usr/local/etc/clawdie/clawdie.env) or set them in rc.conf below.
# #
# clawdie runs in the FOREGROUND (no self-daemonize, no pidfile), so rc.d runs # clawdie runs in the FOREGROUND (no self-daemonize, no pidfile), so rc.d runs
# it under daemon(8), which backgrounds it, writes the supervisor pidfile, # it under daemon(8), which backgrounds it, restarts on crash, drops privileges
# restarts on crash, drops privileges to the clawdie user, and redirects # to the clawdie user, and redirects stdout/stderr (tracing) to a logfile.
# stdout/stderr (tracing) to a logfile.
# #
# Install: # Install:
# pw groupadd clawdie # pw groupadd clawdie
@ -47,18 +46,29 @@ load_rc_config $name
: ${clawdie_env_file:="/usr/local/etc/clawdie/clawdie.env"} : ${clawdie_env_file:="/usr/local/etc/clawdie/clawdie.env"}
pidfile="${clawdie_run_dir}/clawdie.pid" pidfile="${clawdie_run_dir}/clawdie.pid"
# Supervisor pidfile (the daemon(8) parent). Kept distinct from the child
# pidfile so `stop` can target the supervisor — see clawdie_stop.
supervisor_pidfile="${clawdie_run_dir}/clawdie-supervisor.pid"
# Supervise via daemon(8): -P supervisor pidfile, -r restart on exit, -t title, # Run clawdie under daemon(8):
# -u drop privileges, -o append stdout/stderr to the logfile. # -P supervisor pidfile (the daemon(8) parent — used by stop)
# -p child pidfile (writes the clawdie PID — used by start/status)
# -r restart on crash, -t process title, -u drop to the clawdie user,
# -o append stdout/stderr to log.
command="/usr/sbin/daemon" command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -t ${name} -u ${clawdie_user} \ command_args="-P ${supervisor_pidfile} -p ${pidfile} -r -t ${name} -u ${clawdie_user} \
-o ${clawdie_logfile} ${clawdie_program}" -o ${clawdie_logfile} ${clawdie_program}"
# rc.subr matches the pidfile process against ${procname}; that is daemon(8), # Match the child binary so `service clawdie status` finds OUR process via the
# the supervised parent — not clawdie itself. # child pidfile, not the generic /usr/sbin/daemon supervisor (which would
procname="/usr/sbin/daemon" # collide with tailscaled, colibri_daemon, and other daemon(8) services).
procname="clawdie"
start_precmd="clawdie_prestart" start_precmd="clawdie_prestart"
start_postcmd="clawdie_poststart"
stop_cmd="clawdie_stop"
stop_postcmd="clawdie_poststop"
extra_commands="health"
clawdie_prestart() clawdie_prestart()
{ {
@ -89,4 +99,79 @@ clawdie_prestart()
fi fi
} }
clawdie_poststart()
{
# Wait for the Herdr socket to appear (daemon forks, child binds socket).
local timeout=10
local waited=0
while [ ! -S "${clawdie_socket}" ] && [ $waited -lt $timeout ]; do
sleep 1
waited=$((waited + 1))
done
if [ -S "${clawdie_socket}" ]; then
echo "clawdie socket ready after ${waited}s"
else
echo "WARNING: clawdie socket not ready after ${timeout}s"
fi
}
clawdie_stop()
{
# daemon(8) -r restarts the child if it is killed directly, so a plain
# SIGTERM to the child pidfile would just be undone. Stop the supervisor
# instead: on SIGTERM it forwards the signal to the child and exits without
# restarting it.
local _sup=""
[ -f "${supervisor_pidfile}" ] && _sup=$(cat "${supervisor_pidfile}" 2>/dev/null)
if [ -n "${_sup}" ] && kill -0 "${_sup}" 2>/dev/null; then
echo "Stopping ${name} (daemon(8) supervisor pid ${_sup})."
kill -TERM "${_sup}" 2>/dev/null
local _n=0
while kill -0 "${_sup}" 2>/dev/null && [ ${_n} -lt 30 ]; do
sleep 1
_n=$((_n + 1))
done
if kill -0 "${_sup}" 2>/dev/null; then
echo "Supervisor did not exit in time; sending SIGKILL."
kill -KILL "${_sup}" 2>/dev/null
fi
else
echo "${name} is not running."
fi
# Belt-and-suspenders: terminate the child if it somehow outlived the
# supervisor (e.g. supervisor SIGKILLed before it could clean up).
local _ch=""
[ -f "${pidfile}" ] && _ch=$(cat "${pidfile}" 2>/dev/null)
if [ -n "${_ch}" ] && kill -0 "${_ch}" 2>/dev/null; then
kill -TERM "${_ch}" 2>/dev/null
fi
rm -f "${supervisor_pidfile}" "${pidfile}"
}
clawdie_poststop()
{
# Clean up tmpfs artifacts on graceful shutdown.
if [ -S "${clawdie_socket}" ]; then
rm -f "${clawdie_socket}"
fi
}
health_cmd="clawdie_health"
clawdie_health()
{
if [ -S "${clawdie_socket}" ]; then
if printf '{"cmd":"status"}\n' | nc -U "${clawdie_socket}" -w 2 >/dev/null 2>&1; then
echo "clawdie is healthy (socket responding)"
return 0
else
echo "clawdie socket exists but not responding"
return 1
fi
else
echo "clawdie socket not found"
return 1
fi
}
run_rc_command "$1" run_rc_command "$1"

View file

@ -47,12 +47,17 @@ load_rc_config $name
: ${colibri_cost_mode:="smart"} : ${colibri_cost_mode:="smart"}
pidfile="${colibri_daemon_run_dir}/colibri-daemon.pid" pidfile="${colibri_daemon_run_dir}/colibri-daemon.pid"
# Supervisor pidfile (the daemon(8) parent). Kept distinct from the child
# pidfile so `stop` can target the supervisor — see colibri_daemon_stop.
supervisor_pidfile="${colibri_daemon_run_dir}/colibri-daemon-supervisor.pid"
# Run colibri-daemon under daemon(8): -p child pidfile (writes colibri-daemon PID), # Run colibri-daemon under daemon(8):
# -r restart on exit, -t process title, -u drop to the colibri user, -o append # -P supervisor pidfile (the daemon(8) parent — used by stop)
# stdout/stderr to log. # -p child pidfile (writes colibri-daemon PID — used by start/status)
# -r restart on crash, -t process title, -u drop to the colibri user,
# -o append stdout/stderr to log.
command="/usr/sbin/daemon" command="/usr/sbin/daemon"
command_args="-p ${pidfile} -r -t ${name} -u ${colibri_daemon_user} \ command_args="-P ${supervisor_pidfile} -p ${pidfile} -r -t ${name} -u ${colibri_daemon_user} \
-o ${colibri_daemon_logfile} ${colibri_daemon_program}" -o ${colibri_daemon_logfile} ${colibri_daemon_program}"
# Use the child's process name so rc.subr can find the right process via the # Use the child's process name so rc.subr can find the right process via the
@ -62,6 +67,7 @@ procname="colibri-daemon"
start_precmd="colibri_daemon_prestart" start_precmd="colibri_daemon_prestart"
start_postcmd="colibri_daemon_poststart" start_postcmd="colibri_daemon_poststart"
stop_cmd="colibri_daemon_stop"
stop_postcmd="colibri_daemon_poststop" stop_postcmd="colibri_daemon_poststop"
extra_commands="health" extra_commands="health"
@ -100,6 +106,39 @@ colibri_daemon_poststart()
fi fi
} }
colibri_daemon_stop()
{
# daemon(8) -r restarts the child if it is killed directly, so a plain
# SIGTERM to the child pidfile would just be undone. Stop the supervisor
# instead: on SIGTERM it forwards the signal to the child and exits without
# restarting it.
local _sup=""
[ -f "${supervisor_pidfile}" ] && _sup=$(cat "${supervisor_pidfile}" 2>/dev/null)
if [ -n "${_sup}" ] && kill -0 "${_sup}" 2>/dev/null; then
echo "Stopping ${name} (daemon(8) supervisor pid ${_sup})."
kill -TERM "${_sup}" 2>/dev/null
local _n=0
while kill -0 "${_sup}" 2>/dev/null && [ ${_n} -lt 30 ]; do
sleep 1
_n=$((_n + 1))
done
if kill -0 "${_sup}" 2>/dev/null; then
echo "Supervisor did not exit in time; sending SIGKILL."
kill -KILL "${_sup}" 2>/dev/null
fi
else
echo "${name} is not running."
fi
# Belt-and-suspenders: terminate the child if it somehow outlived the
# supervisor (e.g. supervisor SIGKILLed before it could clean up).
local _ch=""
[ -f "${pidfile}" ] && _ch=$(cat "${pidfile}" 2>/dev/null)
if [ -n "${_ch}" ] && kill -0 "${_ch}" 2>/dev/null; then
kill -TERM "${_ch}" 2>/dev/null
fi
rm -f "${supervisor_pidfile}" "${pidfile}"
}
colibri_daemon_poststop() colibri_daemon_poststop()
{ {
# Clean up tmpfs artifacts on graceful shutdown. # Clean up tmpfs artifacts on graceful shutdown.