OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / distribute / hypertps-win64 / scripts / daemon-session.sh

161 lines · bash ref:
#!/usr/bin/env bash
# daemon-session — durable lifecycle for hyper-tps --daemon-only sessions.
# Replaces ad-hoc `setsid hyper-tps --daemon-only … & / pkill / hand-typed admin-cli`.
# See spec/daemon-session.md. Self-contained; scp to any fleet machine and run.
#
#   daemon-session start <roomId|prefix> [--mode room|anchor|pin] [--expect-identity <name>]
#   daemon-session stop    <slug>
#   daemon-session status  [<slug>]
#   daemon-session restart <slug>
#   daemon-session logs    <slug> [-f]
#   daemon-session query   <slug> <method> [jsonParams]
#
# Conventions: ~/.hyper-tps/sessions/<slug>/{pid(=process-group id),log,meta.json}
set -uo pipefail

SESS_ROOT="${HYPER_TPS_SESSIONS:-$HOME/.hyper-tps/sessions}"
LAUNCHER="${HYPER_TPS_LAUNCHER:-$HOME/.local/bin/hyper-tps}"
SOCK="${KEET_SOCK:-/tmp/keet-daemon.sock}"

_die() { echo "daemon-session: $*" >&2; exit 1; }
_slug() { echo "$1" | tr -cd '[:alnum:]' | cut -c1-24; }
_sdir() { echo "$SESS_ROOT/$(_slug "$1")"; }

# First existing admin-cli.cjs (source tree, then installed app tree).
_resolve_cli() {
  local c
  for c in "$(dirname "$0")/admin-cli.cjs" \
           "$HOME/.local/share/store/apps/hyper-tps/scripts/admin-cli.cjs" \
           "/storage/projects/code/godot/hyper-tps/scripts/admin-cli.cjs"; do
    [ -f "$c" ] && { echo "$c"; return 0; }
  done
  return 1
}

# A raw JSON-RPC to the daemon via admin-cli. Args: method [jsonParams]
_rpc() {
  local cli; cli="$(_resolve_cli)" || { echo '{"error":"admin-cli not found"}'; return 1; }
  KEET_SOCK="$SOCK" node "$cli" raw "$1" "${2:-{}}" 2>/dev/null
}

# Is process group $1 alive? (kill -0 on the leader pid; pgid == leader pid here)
_alive() { [ -n "${1:-}" ] && kill -0 "$1" 2>/dev/null; }

_meta_get() { # <sdir> <key>
  grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$1/meta.json" 2>/dev/null | head -1 | sed -E 's/.*: *"([^"]*)".*/\1/'
}

cmd_start() {
  local room="$1"; shift
  local mode="room" expect=""
  while [ $# -gt 0 ]; do case "$1" in
    --mode) mode="$2"; shift 2;;
    --expect-identity) expect="$2"; shift 2;;
    *) _die "unknown start flag: $1";;
  esac; done
  [ -n "$room" ] || _die "start <roomId|prefix> required"
  local slug sdir; slug="$(_slug "$room")"; sdir="$(_sdir "$room")"
  mkdir -p "$sdir"

  # Idempotent: a live session already? (one daemon per ~/.config/keet-cli anyway)
  if [ -f "$sdir/pid" ] && _alive "$(cat "$sdir/pid")"; then
    echo "already running (slug=$slug pgid=$(cat "$sdir/pid")); use 'restart' to relaunch"
    return 0
  fi

  # Mode → launcher flag. room = game-driven (accepts a PREFIX); anchor/pin =
  # daemon-driven (need the FULL roomId; anchor also grows the indexer set).
  local flag
  case "$mode" in
    room)   flag="--room-id";;
    anchor) flag="--anchor-room";;
    pin)    flag="--pin-room";;
    *) _die "mode must be room|anchor|pin";;
  esac

  : > "$sdir/log"
  # setsid → new process group; the launched pid IS the group-leader (pgid).
  DISPLAY="${DISPLAY:-:0}" setsid "$LAUNCHER" --daemon-only "$flag" "$room" \
     >> "$sdir/log" 2>&1 < /dev/null &
  local pgid=$!
  echo "$pgid" > "$sdir/pid"
  printf '{"machine":"%s","roomArg":"%s","mode":"%s","flag":"%s","startedAt":"%s","pgid":%s}\n' \
    "$(hostname)" "$room" "$mode" "$flag" "$(date -u +%FT%TZ 2>/dev/null || echo unknown)" "$pgid" > "$sdir/meta.json"
  echo "started slug=$slug pgid=$pgid mode=$mode room=$room  log=$sdir/log"

  # Preflight (best-effort, non-fatal): report identity + room open state once up.
  local i
  for i in $(seq 1 20); do [ -S "$SOCK" ] && grep -qiE 'Active identity|_start_mesh|state-base-v3' "$sdir/log" && break; sleep 3; done
  local ident; ident="$(grep -oE 'Active identity: [a-z0-9-]+' "$sdir/log" | tail -1 | awk '{print $3}')"
  [ -n "$ident" ] && echo "  identity: $ident"
  if [ -n "$expect" ] && [ -n "$ident" ] && [ "$ident" != "$expect" ]; then
    echo "  ⚠ identity MISMATCH: expected '$expect', got '$ident' (NOT switching — see NEVER_CHANGE_IDENTITIES)"
  fi
}

cmd_stop() {
  local slug sdir; slug="$(_slug "$1")"; sdir="$(_sdir "$1")"
  [ -f "$sdir/pid" ] || _die "no session '$slug'"
  local pgid; pgid="$(cat "$sdir/pid")"
  if _alive "$pgid"; then
    kill -TERM "-$pgid" 2>/dev/null   # negative = whole process group (no orphans)
    local i; for i in $(seq 1 10); do _alive "$pgid" || break; sleep 1; done
    _alive "$pgid" && kill -KILL "-$pgid" 2>/dev/null
    echo "stopped slug=$slug pgid=$pgid"
  else
    echo "slug=$slug not running (stale pid $pgid)"
  fi
  rm -f "$sdir/pid"
}

cmd_status() {
  local one="${1:-}"
  mkdir -p "$SESS_ROOT"
  local sdir slug pgid up
  for sdir in "$SESS_ROOT"/*/; do
    [ -d "$sdir" ] || continue
    slug="$(basename "$sdir")"
    [ -n "$one" ] && [ "$slug" != "$(_slug "$one")" ] && continue
    pgid="$(cat "$sdir/pid" 2>/dev/null || echo -)"
    up=down; _alive "$pgid" && up=UP
    printf '● %-24s %-4s pgid=%-7s mode=%-6s room=%s\n' \
      "$slug" "$up" "$pgid" "$(_meta_get "$sdir" mode)" "$(_meta_get "$sdir" roomArg)"
    if [ "$up" = UP ] && [ -S "$SOCK" ]; then
      # Resolve full roomId from the game log, then query state.diag.
      local rid; rid="$(grep -oE 'joining room_id=[a-z0-9]+' "$sdir/log" | tail -1 | cut -d= -f2)"
      if [ -n "$rid" ]; then
        local d; d="$(_rpc state.diag "{\"roomId\":\"$rid\",\"open\":true}")"
        echo "    indexers=$(echo "$d"|grep -oE '"indexers": [0-9]+'|grep -oE '[0-9]+') writable=$(echo "$d"|grep -oE '"writable": (true|false)'|grep -oE '(true|false)') baseLength=$(echo "$d"|grep -oE '"baseLength": [0-9]+'|grep -oE '[0-9]+') broken=$(echo "$d"|grep -oE '"broken": [^,]*'|sed 's/.*: //')"
      fi
    fi
  done
}

cmd_restart() { cmd_stop "$1" 2>/dev/null; sleep 2
  local sdir; sdir="$(_sdir "$1")"
  local room mode; room="$(_meta_get "$sdir" roomArg)"; mode="$(_meta_get "$sdir" mode)"
  [ -n "$room" ] || _die "no meta to restart '$1'"
  cmd_start "$room" --mode "${mode:-room}"
}

cmd_logs() { local sdir; sdir="$(_sdir "$1")"; [ -f "$sdir/log" ] || _die "no log for '$1'"
  if [ "${2:-}" = "-f" ]; then tail -f "$sdir/log"; else tail -40 "$sdir/log"; fi; }

cmd_query() { local sdir rid; sdir="$(_sdir "$1")"
  rid="$(grep -oE 'joining room_id=[a-z0-9]+' "$sdir/log" 2>/dev/null | tail -1 | cut -d= -f2)"
  local params="${3:-{}}"
  # Convenience: if params omitted but method needs a room, inject the resolved id.
  [ "$params" = "{}" ] && [ -n "$rid" ] && case "$2" in state.*|members*|send|read) params="{\"roomId\":\"$rid\"}";; esac
  _rpc "$2" "$params"
}

case "${1:-}" in
  start)   shift; cmd_start "$@";;
  stop)    shift; cmd_stop "$@";;
  status)  shift; cmd_status "$@";;
  restart) shift; cmd_restart "$@";;
  logs)    shift; cmd_logs "$@";;
  query)   shift; cmd_query "$@";;
  *) echo "usage: daemon-session {start <room> [--mode room|anchor|pin] [--expect-identity <name>]|stop <slug>|status [slug]|restart <slug>|logs <slug> [-f]|query <slug> <method> [json]}" >&2; exit 1;;
esac