Fix FreeBSD rc.d restart behavior and poller lifecycle
This commit is contained in:
12
.env.example
12
.env.example
@@ -38,6 +38,18 @@ WEBMENTION_SENDER_ORIGIN=
|
||||
# Example: http://127.0.0.1:3000/webmention-sender
|
||||
WEBMENTION_SENDER_ENDPOINT=
|
||||
|
||||
# Wait up to this many seconds for endpoint readiness before first poll
|
||||
WEBMENTION_SENDER_READY_TIMEOUT=60
|
||||
|
||||
# Graceful stop timeout for webmention poller during shutdown (seconds)
|
||||
WEBMENTION_SENDER_STOP_TIMEOUT=5
|
||||
|
||||
# Graceful stop timeout for Indiekit process during shutdown (seconds)
|
||||
INDIEKIT_STOP_TIMEOUT=20
|
||||
|
||||
# If parent process is FreeBSD daemon(8), terminate it during shutdown (1/0)
|
||||
KILL_DAEMON_PARENT_ON_SHUTDOWN=1
|
||||
|
||||
# Optional webmentions proxy endpoint settings
|
||||
# Default mount path in indiekit.config.mjs is /webmentions-api
|
||||
WEBMENTIONS_PROXY_MOUNT_PATH=/webmentions-api
|
||||
|
||||
11
README.md
11
README.md
@@ -147,6 +147,17 @@
|
||||
- Use `start.example.sh` as the tracked template and keep real credentials in environment variables (or `.env` on the server).
|
||||
- Startup scripts parse `.env` with the `dotenv` parser (not shell `source`), so values containing spaces are handled safely.
|
||||
- `start.example.sh` includes an optional background webmention sender polling loop for bare-metal deployments (including FreeBSD jails).
|
||||
- For FreeBSD service management, use `indiekit.rcd.example` as a template for `/usr/local/etc/rc.d/indiekit`.
|
||||
- Important: do not use `daemon -r` in the rc.d command args. Let `service indiekit restart` control restart behavior; `-r` can keep the supervisor alive during stop/restart.
|
||||
- The rc.d template uses daemon supervisor pidfile `-P` (and child pidfile `-p`) and supports `indiekit_stop_timeout` in `rc.conf` (default `20` seconds).
|
||||
- FreeBSD rc.d install example:
|
||||
|
||||
```sh
|
||||
install -m 0555 /usr/local/indiekit/indiekit.rcd.example /usr/local/etc/rc.d/indiekit
|
||||
sysrc indiekit_enable=YES
|
||||
service indiekit restart
|
||||
```
|
||||
|
||||
- FreeBSD jail env example for auto-send polling:
|
||||
|
||||
```sh
|
||||
|
||||
73
indiekit.rcd.example
Normal file
73
indiekit.rcd.example
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: indiekit
|
||||
# REQUIRE: NETWORKING LOGIN
|
||||
# KEYWORD: shutdown
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="indiekit"
|
||||
rcvar="${name}_enable"
|
||||
|
||||
pidfile="/var/run/${name}.pid"
|
||||
child_pidfile="/var/run/${name}.child.pid"
|
||||
logfile="/var/log/${name}.log"
|
||||
|
||||
command="/usr/sbin/daemon"
|
||||
procname="/usr/sbin/daemon"
|
||||
required_files="/usr/local/indiekit/start.sh"
|
||||
|
||||
# Important: do not use daemon -r here. rc.d should own restart behavior.
|
||||
command_args="-P ${pidfile} -p ${child_pidfile} -o ${logfile} -u indiekit /usr/local/indiekit/start.sh"
|
||||
|
||||
extra_commands="reload"
|
||||
sig_reload="HUP"
|
||||
|
||||
start_precmd="${name}_prestart"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
indiekit_prestart()
|
||||
{
|
||||
touch "${logfile}"
|
||||
chown indiekit:indiekit "${logfile}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
indiekit_stop()
|
||||
{
|
||||
if [ ! -r "${pidfile}" ]; then
|
||||
echo "${name} not running?"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_pid="$(cat "${pidfile}" 2>/dev/null || true)"
|
||||
if [ -z "${_pid}" ] || ! kill -0 "${_pid}" 2>/dev/null; then
|
||||
rm -f "${pidfile}" "${child_pidfile}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_timeout="${indiekit_stop_timeout:-20}"
|
||||
case "${_timeout}" in
|
||||
''|*[!0-9]*) _timeout=20 ;;
|
||||
esac
|
||||
|
||||
echo "Stopping ${name}."
|
||||
kill -TERM "${_pid}" 2>/dev/null || true
|
||||
|
||||
while kill -0 "${_pid}" 2>/dev/null; do
|
||||
if [ "${_timeout}" -le 0 ]; then
|
||||
echo "${name} stop timeout; forcing kill" >&2
|
||||
kill -KILL "${_pid}" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
_timeout=$((_timeout - 1))
|
||||
done
|
||||
|
||||
rm -f "${pidfile}" "${child_pidfile}"
|
||||
}
|
||||
|
||||
load_rc_config "${name}"
|
||||
: ${indiekit_enable:="NO"}
|
||||
|
||||
run_rc_command "$1"
|
||||
300
start.example.sh
300
start.example.sh
@@ -3,6 +3,233 @@ set -eu
|
||||
|
||||
cd /usr/local/indiekit
|
||||
NODE_BIN="${NODE_BIN:-/usr/local/bin/node}"
|
||||
WEBMENTION_POLL_PID=""
|
||||
INDIEKIT_PID=""
|
||||
SHUTDOWN_IN_PROGRESS=0
|
||||
WEBMENTION_STOP_TIMEOUT="${WEBMENTION_SENDER_STOP_TIMEOUT:-5}"
|
||||
INDIEKIT_STOP_TIMEOUT="${INDIEKIT_STOP_TIMEOUT:-20}"
|
||||
WEBMENTION_READY_TIMEOUT="${WEBMENTION_SENDER_READY_TIMEOUT:-60}"
|
||||
KILL_DAEMON_PARENT_ON_SHUTDOWN="${KILL_DAEMON_PARENT_ON_SHUTDOWN:-1}"
|
||||
|
||||
case "$WEBMENTION_STOP_TIMEOUT" in
|
||||
''|*[!0-9]*) WEBMENTION_STOP_TIMEOUT=5 ;;
|
||||
esac
|
||||
|
||||
case "$INDIEKIT_STOP_TIMEOUT" in
|
||||
''|*[!0-9]*) INDIEKIT_STOP_TIMEOUT=20 ;;
|
||||
esac
|
||||
|
||||
case "$WEBMENTION_READY_TIMEOUT" in
|
||||
''|*[!0-9]*) WEBMENTION_READY_TIMEOUT=60 ;;
|
||||
esac
|
||||
|
||||
case "$KILL_DAEMON_PARENT_ON_SHUTDOWN" in
|
||||
''|*[!0-9]*) KILL_DAEMON_PARENT_ON_SHUTDOWN=1 ;;
|
||||
esac
|
||||
|
||||
is_pid_alive() {
|
||||
_pid="$1"
|
||||
|
||||
if [ -z "$_pid" ] || ! kill -0 "$_pid" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# FreeBSD can report zombies as existing PIDs; exclude them from "alive".
|
||||
if command -v ps >/dev/null 2>&1; then
|
||||
_state="$(ps -o stat= -p "$_pid" 2>/dev/null || true)"
|
||||
case "$_state" in
|
||||
*Z*) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_for_pid_exit() {
|
||||
_pid="$1"
|
||||
_timeout="$2"
|
||||
_elapsed=0
|
||||
|
||||
while is_pid_alive "$_pid"; do
|
||||
if [ "$_elapsed" -ge "$_timeout" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
_elapsed=$((_elapsed + 1))
|
||||
done
|
||||
|
||||
wait "$_pid" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
stop_webmention_poller() {
|
||||
if [ -n "${WEBMENTION_POLL_PID}" ] && is_pid_alive "${WEBMENTION_POLL_PID}"; then
|
||||
kill "${WEBMENTION_POLL_PID}" 2>/dev/null || true
|
||||
|
||||
if ! wait_for_pid_exit "${WEBMENTION_POLL_PID}" "${WEBMENTION_STOP_TIMEOUT}"; then
|
||||
kill -9 "${WEBMENTION_POLL_PID}" 2>/dev/null || true
|
||||
wait "${WEBMENTION_POLL_PID}" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
WEBMENTION_POLL_PID=""
|
||||
}
|
||||
|
||||
stop_indiekit_server() {
|
||||
if [ -n "${INDIEKIT_PID}" ] && is_pid_alive "${INDIEKIT_PID}"; then
|
||||
kill "${INDIEKIT_PID}" 2>/dev/null || true
|
||||
|
||||
if ! wait_for_pid_exit "${INDIEKIT_PID}" "${INDIEKIT_STOP_TIMEOUT}"; then
|
||||
echo "[indiekit] Shutdown timeout after ${INDIEKIT_STOP_TIMEOUT}s; forcing kill" >&2
|
||||
kill -9 "${INDIEKIT_PID}" 2>/dev/null || true
|
||||
wait "${INDIEKIT_PID}" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
INDIEKIT_PID=""
|
||||
}
|
||||
|
||||
stop_daemon_parent() {
|
||||
if [ "$KILL_DAEMON_PARENT_ON_SHUTDOWN" != "1" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v ps >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
_ppid="${PPID:-}"
|
||||
if [ -z "$_ppid" ] || ! kill -0 "$_ppid" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
|
||||
_parent_cmd="$(ps -o command= -p "$_ppid" 2>/dev/null || true)"
|
||||
|
||||
case "$_parent_cmd" in
|
||||
daemon:\ *\(daemon\)*|*/daemon\ *)
|
||||
kill "$_ppid" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
start_webmention_poller() {
|
||||
if [ "${WEBMENTION_SENDER_AUTO_POLL:-1}" != "1" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
WEBMENTION_SENDER_HOST="${WEBMENTION_SENDER_HOST:-127.0.0.1}"
|
||||
WEBMENTION_SENDER_PORT="${WEBMENTION_SENDER_PORT:-${PORT:-3000}}"
|
||||
WEBMENTION_SENDER_PATH="${WEBMENTION_SENDER_MOUNT_PATH:-/webmention-sender}"
|
||||
WEBMENTION_SENDER_ORIGIN="${WEBMENTION_SENDER_ORIGIN:-${PUBLICATION_URL:-${SITE_URL:-}}}"
|
||||
WEBMENTION_SENDER_INTERVAL="${WEBMENTION_SENDER_POLL_INTERVAL:-300}"
|
||||
|
||||
case "$WEBMENTION_SENDER_PATH" in
|
||||
/*) ;;
|
||||
*) WEBMENTION_SENDER_PATH="/$WEBMENTION_SENDER_PATH" ;;
|
||||
esac
|
||||
|
||||
case "$WEBMENTION_SENDER_INTERVAL" in
|
||||
''|*[!0-9]*) WEBMENTION_SENDER_INTERVAL=300 ;;
|
||||
esac
|
||||
|
||||
WEBMENTION_SENDER_ORIGIN="${WEBMENTION_SENDER_ORIGIN%/}"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "[webmention] curl not found; skipping auto-send polling" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "$WEBMENTION_SENDER_ORIGIN" ]; then
|
||||
echo "[webmention] SITE_URL/PUBLICATION_URL missing; skipping auto-send polling" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
WEBMENTION_SENDER_ENDPOINT="${WEBMENTION_SENDER_ENDPOINT:-http://${WEBMENTION_SENDER_HOST}:${WEBMENTION_SENDER_PORT}${WEBMENTION_SENDER_PATH}}"
|
||||
|
||||
# Wait for the local endpoint to answer (any HTTP status) before polling.
|
||||
WEBMENTION_READY_ELAPSED=0
|
||||
while true; do
|
||||
if ! is_pid_alive "${INDIEKIT_PID}"; then
|
||||
echo "[webmention] Indiekit exited before poller startup; skipping" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
WEBMENTION_READY_CODE="$(
|
||||
curl -sS -o /dev/null -m 2 -w '%{http_code}' "${WEBMENTION_SENDER_ENDPOINT}" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
case "$WEBMENTION_READY_CODE" in
|
||||
''|000) ;;
|
||||
*) break ;;
|
||||
esac
|
||||
|
||||
if [ "$WEBMENTION_READY_ELAPSED" -ge "$WEBMENTION_READY_TIMEOUT" ]; then
|
||||
echo "[webmention] Startup readiness timeout after ${WEBMENTION_READY_TIMEOUT}s; starting poller anyway" >&2
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
WEBMENTION_READY_ELAPSED=$((WEBMENTION_READY_ELAPSED + 1))
|
||||
done
|
||||
|
||||
(
|
||||
echo "[webmention] Starting auto-send polling every ${WEBMENTION_SENDER_INTERVAL}s (${WEBMENTION_SENDER_ENDPOINT})"
|
||||
|
||||
while true; do
|
||||
if ! is_pid_alive "${INDIEKIT_PID}"; then
|
||||
echo "[webmention] Indiekit stopped; exiting poller"
|
||||
break
|
||||
fi
|
||||
|
||||
TOKEN="$({
|
||||
WEBMENTION_ORIGIN="$WEBMENTION_SENDER_ORIGIN" \
|
||||
WEBMENTION_SECRET="$SECRET" \
|
||||
"$NODE_BIN" -e '
|
||||
const jwt = require("jsonwebtoken");
|
||||
const me = process.env.WEBMENTION_ORIGIN;
|
||||
const secret = process.env.WEBMENTION_SECRET;
|
||||
if (!me || !secret) process.exit(1);
|
||||
process.stdout.write(
|
||||
jwt.sign({ me, scope: "update" }, secret, { expiresIn: "5m" }),
|
||||
);
|
||||
' 2>/dev/null;
|
||||
} || true)"
|
||||
|
||||
if [ -n "$TOKEN" ]; then
|
||||
RESULT="$(curl -sS -X POST "${WEBMENTION_SENDER_ENDPOINT}?token=${TOKEN}" 2>&1 || true)"
|
||||
|
||||
if [ -n "$RESULT" ]; then
|
||||
echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - $RESULT"
|
||||
else
|
||||
echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - ok"
|
||||
fi
|
||||
else
|
||||
echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - token generation failed"
|
||||
fi
|
||||
|
||||
sleep "$WEBMENTION_SENDER_INTERVAL"
|
||||
done
|
||||
) &
|
||||
|
||||
WEBMENTION_POLL_PID="$!"
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
if [ "${SHUTDOWN_IN_PROGRESS}" = "1" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
SHUTDOWN_IN_PROGRESS=1
|
||||
trap '' INT TERM HUP
|
||||
|
||||
# Stop poller first so shutdown does not generate connection-refused spam.
|
||||
stop_webmention_poller
|
||||
stop_indiekit_server
|
||||
stop_daemon_parent
|
||||
}
|
||||
|
||||
trap 'shutdown; exit 0' INT TERM HUP
|
||||
|
||||
# Optional: load environment from local .env file
|
||||
# (dotenv syntax, supports spaces in values).
|
||||
@@ -71,66 +298,23 @@ unset DEBUG
|
||||
"${NODE_BIN}" scripts/patch-indieauth-devmode-guard.mjs
|
||||
"${NODE_BIN}" scripts/patch-listening-endpoint-runtime-guards.mjs
|
||||
|
||||
# Optional: poll the webmention sender endpoint in the background.
|
||||
if [ "${WEBMENTION_SENDER_AUTO_POLL:-1}" = "1" ]; then
|
||||
WEBMENTION_SENDER_HOST="${WEBMENTION_SENDER_HOST:-127.0.0.1}"
|
||||
WEBMENTION_SENDER_PORT="${WEBMENTION_SENDER_PORT:-${PORT:-3000}}"
|
||||
WEBMENTION_SENDER_PATH="${WEBMENTION_SENDER_MOUNT_PATH:-/webmention-sender}"
|
||||
WEBMENTION_SENDER_ORIGIN="${WEBMENTION_SENDER_ORIGIN:-${PUBLICATION_URL:-${SITE_URL:-}}}"
|
||||
WEBMENTION_SENDER_INTERVAL="${WEBMENTION_SENDER_POLL_INTERVAL:-300}"
|
||||
"${NODE_BIN}" node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs &
|
||||
INDIEKIT_PID="$!"
|
||||
|
||||
case "$WEBMENTION_SENDER_PATH" in
|
||||
/*) ;;
|
||||
*) WEBMENTION_SENDER_PATH="/$WEBMENTION_SENDER_PATH" ;;
|
||||
esac
|
||||
start_webmention_poller
|
||||
|
||||
case "$WEBMENTION_SENDER_INTERVAL" in
|
||||
''|*[!0-9]*) WEBMENTION_SENDER_INTERVAL=300 ;;
|
||||
esac
|
||||
# Keep the parent shell responsive to TERM/HUP from rc(8)/daemon while the
|
||||
# Node process runs. A blocking wait can delay trap execution on some shells.
|
||||
INDIEKIT_EXIT_CODE=0
|
||||
|
||||
WEBMENTION_SENDER_ORIGIN="${WEBMENTION_SENDER_ORIGIN%/}"
|
||||
while is_pid_alive "${INDIEKIT_PID}"; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "[webmention] curl not found; skipping auto-send polling" >&2
|
||||
elif [ -z "$WEBMENTION_SENDER_ORIGIN" ]; then
|
||||
echo "[webmention] SITE_URL/PUBLICATION_URL missing; skipping auto-send polling" >&2
|
||||
else
|
||||
WEBMENTION_SENDER_ENDPOINT="${WEBMENTION_SENDER_ENDPOINT:-http://${WEBMENTION_SENDER_HOST}:${WEBMENTION_SENDER_PORT}${WEBMENTION_SENDER_PATH}}"
|
||||
set +e
|
||||
wait "${INDIEKIT_PID}"
|
||||
INDIEKIT_EXIT_CODE="$?"
|
||||
set -e
|
||||
|
||||
(
|
||||
echo "[webmention] Starting auto-send polling every ${WEBMENTION_SENDER_INTERVAL}s (${WEBMENTION_SENDER_ENDPOINT})"
|
||||
|
||||
while true; do
|
||||
TOKEN="$({
|
||||
WEBMENTION_ORIGIN="$WEBMENTION_SENDER_ORIGIN" \
|
||||
WEBMENTION_SECRET="$SECRET" \
|
||||
"$NODE_BIN" -e '
|
||||
const jwt = require("jsonwebtoken");
|
||||
const me = process.env.WEBMENTION_ORIGIN;
|
||||
const secret = process.env.WEBMENTION_SECRET;
|
||||
if (!me || !secret) process.exit(1);
|
||||
process.stdout.write(
|
||||
jwt.sign({ me, scope: "update" }, secret, { expiresIn: "5m" }),
|
||||
);
|
||||
' 2>/dev/null;
|
||||
} || true)"
|
||||
|
||||
if [ -n "$TOKEN" ]; then
|
||||
RESULT="$(curl -sS -X POST "${WEBMENTION_SENDER_ENDPOINT}?token=${TOKEN}" 2>&1 || true)"
|
||||
|
||||
if [ -n "$RESULT" ]; then
|
||||
echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - $RESULT"
|
||||
else
|
||||
echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - ok"
|
||||
fi
|
||||
else
|
||||
echo "[webmention] $(date '+%Y-%m-%d %H:%M:%S') - token generation failed"
|
||||
fi
|
||||
|
||||
sleep "$WEBMENTION_SENDER_INTERVAL"
|
||||
done
|
||||
) &
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "${NODE_BIN}" node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs
|
||||
stop_webmention_poller
|
||||
exit "${INDIEKIT_EXIT_CODE}"
|
||||
|
||||
Reference in New Issue
Block a user