# daemon.sh — start (or reuse) the dev keet-daemon. SOURCE it, do not run it.
#
# REPO=<repo> . "$REPO/scripts/daemon.sh"
# ensure_daemon [--pin-room <id> ...] # sets started_daemon=0|1
# stop_daemon_if_started # stops ONLY a daemon ensure_daemon started
#
# WHY ONE FILE. dev-run.sh started the daemon and dev-editor.sh deliberately did
# not -- but the editor's F5 runs the GAME, and the game needs the daemon: with no
# daemon every durable write lives only in GameState's mirror and is lost on quit.
# Home (docs/home-world-design.md) is a keet room, so the daemon always runs; both
# scripts now take it from here, the same way engine.sh is the one engine path.
#
# Moved from dev-run.sh (2026-10-05). Two behaviour changes, both fixes: the
# started daemon is now actually stopped (see the pidfile note in ensure_daemon),
# and the pin message counts rooms, not --pin-room arguments (it said 2 for 1).
#
# ⚠ THE SOURCE TREE'S node_modules IS NOT ASSEMBLED BY ANYTHING BUT CI. After a
# keet-core bump (holepunchto-deps) or a keet-daemon change, re-run ci.yaml's
# overlay-keet-core + copy-keet-daemon steps on this tree, or this daemon runs a
# stale keet-core and every room write fails with "Autobase is closing"
# (docs/home-world-design.md §8; accepted limitation until the C/C++ port).
#
# Overrides: HYPER_TPS_BARE, KEET_SOCK, HYPER_TPS_DEV_LOG, HYPER_TPS_DEV_PIDFILE.
# Messages are prefixed with DAEMON_TAG (default: the calling script's name).
BARE="${HYPER_TPS_BARE:-$(command -v bare || echo "$HOME/.local/share/store/apps/hyper-tps/runtime/bare")}"
SOCK="${KEET_SOCK:-/tmp/keet-daemon.sock}"
LOG="${HYPER_TPS_DEV_LOG:-/tmp/hyper-tps-dev-daemon.log}"
DAEMON_PIDFILE="${HYPER_TPS_DEV_PIDFILE:-/tmp/hyper-tps-dev-daemon.pid}"
DAEMON_TAG="${DAEMON_TAG:-$(basename "${0%.sh}")}"
started_daemon=0
# A live socket, not merely a socket FILE — index.js makes the same distinction,
# because a daemon killed with -9 leaves the node behind and everything after
# would then wait on something that will never answer.
# python3, NOT `bare -e`: bare cannot resolve node_modules from -e source, so a
# `require('bare-net')` probe aborts with SIGABRT (134) from any cwd — which is
# neither "alive" nor "dead" and would have made every branch below wrong.
_daemon_alive() {
python3 -c "
import socket,sys
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.settimeout(2)
try:
s.connect('$SOCK'); s.close(); sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null
}
ensure_daemon() {
local pin_rooms=("$@")
if _daemon_alive; then
echo "$DAEMON_TAG: keet-daemon already running — reusing it"
return 0
fi
# Clear a stale node so the client is not left retrying a dead socket.
[ -S "$SOCK" ] && rm -f "$SOCK"
echo "$DAEMON_TAG: starting keet-daemon from source -> $LOG"
[ ${#pin_rooms[@]} -gt 0 ] && echo "$DAEMON_TAG: pinning $(( ${#pin_rooms[@]} / 2 )) room(s) so their StateBase is warm"
# THE PID IS WRITTEN BY THE DAEMON'S OWN SHELL, AFTER setsid. This used to be
# `setsid "$BARE" ... & echo $! >pidfile`, and $! is NOT the daemon: setsid
# forks when its caller leads a process group, so $! was the setsid parent,
# which exits at once. Measured 2026-10-05 (scripts/test/daemon-sh.test.sh):
# pidfile 386559, daemon 386560 in its own group. The stop below then signalled
# a group that did not exist, silently, and dev-run.sh had never once stopped
# the daemon it started. `$$` inside setsid is the session leader, and `exec`
# keeps that pid for bare.
( cd "$REPO" && DAEMON_PIDFILE="$DAEMON_PIDFILE" setsid sh -c 'echo $$ >"$DAEMON_PIDFILE"; exec "$0" "$@"' \
"$BARE" index.js --daemon-only --no-game "${pin_rooms[@]}" >"$LOG" 2>&1 & )
started_daemon=1
for _ in $(seq 1 60); do
_daemon_alive && break
sleep 0.5
done
if _daemon_alive; then
echo "$DAEMON_TAG: keet-daemon ready"
else
echo "$DAEMON_TAG: keet-daemon did NOT come up — see $LOG" >&2
echo "$DAEMON_TAG: continuing without it; room state will not load" >&2
fi
}
# Leave a daemon we did not start alone: it may be a pinned fleet peer, and
# killing someone else's always-on node to end a dev session would be rude and
# hard to notice.
stop_daemon_if_started() {
if [ "$started_daemon" = 1 ] && [ -f "$DAEMON_PIDFILE" ]; then
local pid; pid="$(cat "$DAEMON_PIDFILE")"
echo "$DAEMON_TAG: stopping the keet-daemon we started (pgid $pid)"
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null
rm -f "$DAEMON_PIDFILE"
started_daemon=0
fi
}