diff --git a/scripts/penguin-harness.service b/scripts/penguin-harness.service new file mode 100644 index 0000000..b4ee456 --- /dev/null +++ b/scripts/penguin-harness.service @@ -0,0 +1,40 @@ +# ============================================================================= +# PenguinHarness systemd unit (for hosts with a real systemd) +# +# The unit delegates to scripts/penguin-service.sh in "run" (foreground) mode, +# so node discovery, .env loading and the entry path are handled by the script +# and only ONE path needs adjusting below: /path/to/penguin-harness +# +# Install: +# sudo cp scripts/penguin-harness.service /etc/systemd/system/ +# # sửa ExecStart nếu repo không nằm ở /home//penguin-harness +# sudo systemctl daemon-reload +# sudo systemctl enable --now penguin-harness +# +# The one-time seeded admin password is printed to the journal on first start +# with an empty data root: +# journalctl -u penguin-harness | grep "initial password" +# ============================================================================= +[Unit] +Description=PenguinHarness web service (agent builder) +Documentation=https://penguin.ooo/docs +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +# Run as the user that owns the repo and the data root (~/.penguin/data). +User=locpham +WorkingDirectory=/home/locpham/penguin-harness +ExecStart=/home/locpham/penguin-harness/scripts/penguin-service.sh run +Restart=on-failure +RestartSec=3 +TimeoutStopSec=15 +# Console = journald: `journalctl -u penguin-harness -f` +StandardOutput=journal +StandardError=journal +# Hardening (safe for this service) +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target diff --git a/scripts/penguin-service.sh b/scripts/penguin-service.sh new file mode 100755 index 0000000..ad33492 --- /dev/null +++ b/scripts/penguin-service.sh @@ -0,0 +1,171 @@ +#!/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