#!/usr/bin/env bash # ============================================================================= # PenguinHarness service script (portable) # ----------------------------------------------------------------------------- # Starts / stops / restarts the penguin-harness web server as a background # daemon. Works anywhere: the repo location, node binary and .env are # auto-detected (no hardcoded /workspace paths). # # scripts/penguin-service.sh start|stop|status|restart|run # # run = foreground mode (for systemd Type=simple, ExecStart=.../run) # # Console output (including the one-time seeded admin password) goes to: # ~/.penguin/penguin-service.log # # Configuration comes from .env at the repo root (HOST / PORT / PENGUIN_WEB_DIST). # ============================================================================= set -u # --- Self-locating paths: BASE = repo root (parent of scripts/) --------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BASE="$(cd "$SCRIPT_DIR/.." && pwd)" # --- Node discovery: $PENGUIN_NODE > known nvm paths > PATH ------------------- if [ -n "${PENGUIN_NODE:-}" ]; then NODE="$PENGUIN_NODE" elif [ -x /workspace/.nvm/versions/node/v24.18.0/bin/node ]; then NODE=/workspace/.nvm/versions/node/v24.18.0/bin/node elif [ -x "$HOME/.nvm/versions/node/v24.18.0/bin/node" ]; then NODE="$HOME/.nvm/versions/node/v24.18.0/bin/node" elif command -v node >/dev/null 2>&1; then NODE="$(command -v node)" else echo "Không tìm thấy node. Cài Node >= 24 hoặc đặt biến PENGUIN_NODE." >&2 exit 1 fi ENTRY="$BASE/packages/server/dist/index.js" ENVFILE="$BASE/.env" PENGUIN_HOME="${PENGUIN_HOME:-$HOME/.penguin/data}" LOG="${PENGUIN_SERVICE_LOG:-$HOME/.penguin/penguin-service.log}" PIDFILE="${PENGUIN_SERVICE_PID:-$HOME/.penguin/penguin-service.pid}" # --- Sanity checks ------------------------------------------------------------ if [ ! -f "$ENTRY" ]; then echo "Không thấy bản build: $ENTRY" >&2 echo "Chạy trước: cd \"$BASE\" && pnpm install && pnpm build" >&2 exit 1 fi if [ ! -f "$ENVFILE" ]; then echo "Lưu ý: không có $ENVFILE — server sẽ nghe trên HOST=127.0.0.1." >&2 echo "Tạo .env với nội dung 'HOST=0.0.0.0' để mở mọi interface." >&2 fi # --- Load .env (HOST / PORT / PENGUIN_WEB_DIST ...) and export ----------------- if [ -f "$ENVFILE" ]; then set -a # shellcheck disable=SC1090 . "$ENVFILE" set +a fi export PENGUIN_HOME PORT="${PORT:-7364}" # True only while the process is really alive: `kill -0` alone would also # succeed on a zombie (exited but not yet reaped). A zombie (state Z in # /proc//stat) counts as dead. proc_alive() { local pid="$1" kill -0 "$pid" 2>/dev/null || return 1 [ "$(awk '{print $3}' "/proc/$pid/stat" 2>/dev/null)" != "Z" ] } is_running() { [ -f "$PIDFILE" ] || return 1 local pid pid="$(cat "$PIDFILE" 2>/dev/null)" || return 1 [ -n "$pid" ] && proc_alive "$pid" } start() { if is_running; then echo "PenguinHarness đang chạy (pid $(cat "$PIDFILE"))." return 0 fi mkdir -p "$(dirname "$LOG")" "$(dirname "$PIDFILE")" echo "[$(date '+%Y-%m-%d %H:%M:%S')] starting penguin-server (HOST=${HOST:-127.0.0.1} PORT=$PORT PENGUIN_HOME=$PENGUIN_HOME)" >>"$LOG" # setsid detaches from the controlling terminal; all fds go to the log file. setsid "$NODE" "$ENTRY" >>"$LOG" 2>&1 "$PIDFILE" # Wait for readiness (up to 30s) — any HTTP answer counts. local i for i in $(seq 1 30); do if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/"; then break fi sleep 1 done if is_running; then echo "PenguinHarness đã khởi động: http://0.0.0.0:$PORT (pid $(cat "$PIDFILE"))" echo "Log (console): $LOG" # In mật khẩu admin khởi tạo ra console nếu có (chỉ in 1 lần khi seed). grep -h "initial password" "$LOG" 2>/dev/null | tail -1 else echo "Khởi động THẤT BẠI — xem log: $LOG" >&2 tail -20 "$LOG" >&2 rm -f "$PIDFILE" return 1 fi } stop() { if ! is_running; then echo "PenguinHarness không đang chạy." rm -f "$PIDFILE" return 0 fi local pid pid="$(cat "$PIDFILE")" echo "Đang dừng penguin-server (pid $pid)..." kill -TERM "$pid" 2>/dev/null local i for i in $(seq 1 10); do proc_alive "$pid" || break sleep 1 done if proc_alive "$pid"; then echo "Không dừng được trong 10s, buộc dừng (SIGKILL)." >&2 kill -KILL "$pid" 2>/dev/null fi # Reap any zombie so it cannot linger in the process table. wait "$pid" 2>/dev/null rm -f "$PIDFILE" echo "Đã dừng." } status() { if is_running; then echo "PenguinHarness: ĐANG CHẠY (pid $(cat "$PIDFILE"))" echo " URL: http://0.0.0.0:$PORT" echo " Log: $LOG" if grep -h "initial password" "$LOG" 2>/dev/null | tail -1 | grep -q .; then echo " Admin khởi tạo: $(grep -h 'initial password' "$LOG" | tail -1)" fi return 0 fi echo "PenguinHarness: KHÔNG chạy." return 1 } case "${1:-}" in start) start ;; stop) stop ;; restart) stop; start ;; status) status ;; run) # Foreground mode for systemd (Type=simple): exec replaces the shell so # systemd tracks the real server process and Restart= works. exec "$NODE" "$ENTRY" ;; *) echo "Sử dụng: $0 {start|stop|status|restart|run}" exit 2 ;; esac