FreeBSD rc.d service for hermes gateway (daemon(8) supervision) #1
3 changed files with 174 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
145
packaging/freebsd/hermes_daemon.in
Normal file
145
packaging/freebsd/hermes_daemon.in
Normal 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"
|
||||
|
|
@ -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)"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue