#!/usr/bin/env bash # Start / stop the local reachy-mini daemon cleanly for tests. # # WHY: pkill -9 leaks GStreamer semaphores and skips the daemon's # shutdown procedure (sleep pose, mDNS unregister, peer connection # teardown). That can leave the local WebRTC pipe in a broken state # until the laptop reboots — observed 2026-05-06. SIGTERM lets the # daemon run its proper shutdown. # # Usage: # tests/scripts/daemon.sh start # start, wait for "Daemon started successfully" # tests/scripts/daemon.sh stop # SIGTERM, wait for clean exit # tests/scripts/daemon.sh restart # stop, then start set -euo pipefail LOG=/tmp/reachy-daemon.log VENV="${VENV:-$HOME/.virtualenvs/mini}" START_TIMEOUT=60 STOP_TIMEOUT=30 cmd="${1:-}" start() { if pgrep -f reachy-mini-daemon >/dev/null; then echo "daemon already running (PID $(pgrep -f reachy-mini-daemon | head -1))" return 0 fi # Append (not truncate) so prior sessions' shutdown logs survive for # debugging. A separator marks the boundary. { echo "" echo "=== daemon start at $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" } >> "$LOG" # shellcheck disable=SC1091 source "$VENV/bin/activate" nohup reachy-mini-daemon >> "$LOG" 2>&1 & disown local pid=$! echo "daemon starting (PID $pid)" local deadline=$(( $(date +%s) + START_TIMEOUT )) # Look ONLY at the lines after the most recent "=== daemon start at" # marker. Reset matched=0 on each new marker so we don't trigger on a # success line from a previous session that's still in the log. until awk '/=== daemon start at /{found=1; matched=0} found && /Daemon started successfully/{matched=1} END{exit !matched}' "$LOG" 2>/dev/null; do if ! kill -0 "$pid" 2>/dev/null; then echo "daemon died during startup; tail of log:" >&2 tail -20 "$LOG" >&2 return 1 fi if [ "$(date +%s)" -gt "$deadline" ]; then echo "daemon failed to come up in ${START_TIMEOUT}s" >&2 tail -20 "$LOG" >&2 return 1 fi sleep 1 done echo "daemon ready" } stop() { local pid pid=$(pgrep -f reachy-mini-daemon | head -1 || true) if [ -z "$pid" ]; then echo "no daemon to stop" return 0 fi # SIGINT, not SIGTERM. The daemon's full shutdown procedure (put # Reachy Mini to sleep, tear down GStreamer pipelines, unregister # from the central signaling) is wired to KeyboardInterrupt, which # SIGINT raises and SIGTERM does not always trigger reliably across # Python versions and entry-point wrappers. Behaves identically to # CTRL-C in a foreground shell. echo "sending SIGINT to $pid" kill -INT "$pid" local deadline=$(( $(date +%s) + STOP_TIMEOUT )) while kill -0 "$pid" 2>/dev/null; do if [ "$(date +%s)" -gt "$deadline" ]; then # Last resort. Document the failure loud and proud — if we # ever fall through to SIGKILL, semaphores leak and the # local WebRTC pipe state is suspect. echo "daemon did not exit in ${STOP_TIMEOUT}s after SIGINT; sending SIGKILL (will leak GST resources)" >&2 kill -KILL "$pid" return 1 fi sleep 1 done # Verify the FULL shutdown sequence ran. The daemon walks through: # "Putting Reachy Mini to sleep..." ← motors safe-pose # "Daemon stopped successfully." ← clean teardown # Both should appear after the latest start marker. If only the # second is present, something in the sleep procedure failed and # the next session may inherit a half-broken state. local sleep_line=0 stopped_line=0 if awk '/=== daemon start at /{f=1; sleep_seen=0; stopped_seen=0} f && /Putting Reachy Mini to sleep/{sleep_seen=1} f && /Daemon stopped successfully/{stopped_seen=1} END{exit !(sleep_seen && stopped_seen)}' "$LOG" 2>/dev/null; then echo "daemon stopped cleanly (sleep pose + full teardown ran)" elif awk '/=== daemon start at /{f=1; m=0} f && /Daemon stopped successfully/{m=1} END{exit !m}' "$LOG" 2>/dev/null; then echo "WARN: daemon exited but did NOT run sleep pose; next session may be unhealthy" >&2 tail -15 "$LOG" >&2 else echo "WARN: daemon exited without clean-shutdown markers in log; tail:" >&2 tail -15 "$LOG" >&2 fi } case "$cmd" in start) start ;; stop) stop ;; restart) stop && start ;; *) echo "usage: $0 {start|stop|restart}" >&2; exit 2 ;; esac