FreeBSD rc.d service for hermes gateway (daemon(8) supervision) #1

Merged
clawdie merged 2 commits from feat/freebsd-rcd-service into main 2026-06-14 15:21:26 +02:00
3 changed files with 174 additions and 1 deletions

View file

@ -14,6 +14,32 @@ Three targeted changes for FreeBSD native support:
Clipboard (xclip) and voice (ffplay) work on FreeBSD without code changes — xclip and ffmpeg are available via `pkg`.
## Service (rc.d)
After installing, run Hermes as a persistent system service under `daemon(8)`:
```sh
# One-time setup
sudo pw groupadd hermes
sudo pw useradd hermes -g hermes -d /var/db/hermes -s /usr/sbin/nologin
sudo cp packaging/freebsd/hermes_daemon.in /usr/local/etc/rc.d/hermes_daemon
sudo chmod 555 /usr/local/etc/rc.d/hermes_daemon
# Configure Hermes before first start
sudo mkdir -p /var/db/hermes
sudo chown hermes:hermes /var/db/hermes
sudo -u hermes HERMES_HOME=/var/db/hermes hermes setup
sudo -u hermes HERMES_HOME=/var/db/hermes hermes model
# Enable and start
sudo sysrc hermes_daemon_enable=YES
sudo service hermes_daemon start
sudo service hermes_daemon status
```
The service stores config in `/var/db/hermes` (persistent) instead of
`~/.hermes` (tmpfs on live USB). Logs go to `/var/log/hermes/gateway.log`.
## Install
```sh

View file

@ -0,0 +1,145 @@
#!/bin/sh
#
# Hermes Agent — FreeBSD rc.d service
#
# Hermes Agent gateway runs in the FOREGROUND — it does not self-daemonize or
# write a pidfile. rc.d runs it under daemon(8), which backgrounds it, writes
# the child pidfile, restarts on crash, drops privileges to the hermes user,
# and redirects stdout/stderr to a logfile.
#
# Setup (one-time, as root):
# pw groupadd hermes
# pw useradd hermes -g hermes -d /var/db/hermes -s /usr/sbin/nologin
# cp packaging/freebsd/hermes_daemon.in /usr/local/etc/rc.d/hermes_daemon
# chmod 555 /usr/local/etc/rc.d/hermes_daemon
# sysrc hermes_daemon_enable=YES
#
# Before first start, configure Hermes:
# HERMES_HOME=/var/db/hermes su -m hermes -c "hermes setup"
# HERMES_HOME=/var/db/hermes su -m hermes -c "hermes model"
#
# Runtime:
# service hermes_daemon start
# service hermes_daemon status
# service hermes_daemon stop
#
# Requires:
# - hermes-agent Python package installed (pip/uv)
# - hermes binary on PATH at /usr/local/bin/hermes
# - hermes user/group (privilege drop target)
# PROVIDE: hermes_daemon
# REQUIRE: LOGIN cleanvar
# KEYWORD: shutdown
. /etc/rc.subr
name="hermes_daemon"
rcvar="hermes_daemon_enable"
load_rc_config $name
: ${hermes_daemon_enable:="NO"}
: ${hermes_daemon_user:="hermes"}
: ${hermes_daemon_group:="hermes"}
: ${hermes_daemon_program:="/usr/local/bin/hermes"}
: ${hermes_daemon_home:="/var/db/hermes"}
: ${hermes_daemon_run_dir:="/var/run/hermes"}
: ${hermes_daemon_logfile:="/var/log/hermes/gateway.log"}
pidfile="${hermes_daemon_run_dir}/hermes-gateway.pid"
# Supervisor pidfile (the daemon(8) parent). Kept distinct from the child
# pidfile so `stop` can target the supervisor.
supervisor_pidfile="${hermes_daemon_run_dir}/hermes-gateway-supervisor.pid"
# Run hermes gateway under daemon(8):
# -P supervisor pidfile (the daemon(8) parent — used by stop)
# -p child pidfile (writes gateway PID — used by start/status)
# -r restart on crash, -t process title, -u drop to the hermes user,
# -o append stdout/stderr to log.
command="/usr/sbin/daemon"
command_args="-P ${supervisor_pidfile} -p ${pidfile} -r -t ${name} -u ${hermes_daemon_user} \
-o ${hermes_daemon_logfile} ${hermes_daemon_program} gateway run"
# Use the child's process name so rc.subr can find the right process via the
# child pidfile.
procname="hermes"
start_precmd="hermes_daemon_prestart"
stop_cmd="hermes_daemon_stop"
extra_commands="health"
hermes_daemon_prestart()
{
# /var/run is tmpfs on FreeBSD (wiped each boot) — recreate every start.
install -d -o "${hermes_daemon_user}" -g "${hermes_daemon_group}" -m 0750 \
"${hermes_daemon_run_dir}"
install -d -o "${hermes_daemon_user}" -g "${hermes_daemon_group}" -m 0750 \
"${hermes_daemon_home}"
install -d -o "${hermes_daemon_user}" -g "${hermes_daemon_group}" -m 0750 \
"$(/usr/bin/dirname "${hermes_daemon_logfile}")"
# Redirect Hermes config away from the service user's home directory
# (which may be tmpfs or absent) to a persistent path.
export HERMES_HOME="${hermes_daemon_home}"
# Verify Hermes is configured before starting. The gateway exits
# immediately if no provider/model is configured, and daemon(8) -r would
# then respawn it in a tight crash loop. Abort the start instead so the
# operator sees a clear, actionable error.
if [ ! -f "${hermes_daemon_home}/config.yaml" ]; then
echo "ERROR: Hermes config not found at ${hermes_daemon_home}/config.yaml"
echo " Run as operator: HERMES_HOME=${hermes_daemon_home} hermes setup"
echo " Then: HERMES_HOME=${hermes_daemon_home} hermes model"
return 1
fi
}
hermes_daemon_stop()
{
# daemon(8) -r restarts the child if it is killed directly, so 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.
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}"
}
health_cmd="hermes_daemon_health"
hermes_daemon_health()
{
if [ -f "${pidfile}" ]; then
local _pid
_pid=$(cat "${pidfile}" 2>/dev/null)
if [ -n "${_pid}" ] && kill -0 "${_pid}" 2>/dev/null; then
echo "hermes-daemon is healthy (pid ${_pid} alive)"
return 0
fi
fi
echo "hermes-daemon not running"
return 1
}
run_rc_command "$1"

View file

@ -113,4 +113,6 @@ echo "FreeBSD notes:"
echo " - Packages live in /usr/local/bin (ensure it's in PATH)"
echo " - Voice mode: install espeak-ng + ffmpeg (pkg install espeak-ng ffmpeg)"
echo " - Clipboard: install xclip (pkg install xclip)"
echo " - No systemd — use rc.d or run hermes gateway as a foreground service"
echo " - Service: copy packaging/freebsd/hermes_daemon.in to /usr/local/etc/rc.d/"
echo " See README-FreeBSD.md for full rc.d setup"
echo " - Foreground: hermes gateway run (or tmux/screen for persistence)"