Ctrl K
ComunidadGuías…

update_server.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash

# Pulls latest code, makes a one-off DB snapshot (filename pattern:
# meistertracker_YYYYMMDD_HHMMSS.db) under backups/, regenerates TLS certs
# if missing, and restarts the meisterpilze PM2 process.
#
# To restore from a backup, see DEPLOYMENT.md → "Restoring from a backup".
# For off-site replication of backups/, see DEPLOYMENT.md → "Off-site backups
# (REQUIRED)".

# exit immediately if a command exits with a non-zero status
set -e

# Configuration — override via env to run multiple instances under
# distinct PM2 names, or to match a fork's branding. Must match the
# value the server itself reads from PM2_PROCESS_NAME (server.js).
PM2_PROCESS_NAME="${PM2_PROCESS_NAME:-meisterpilze}"

# ---- Helper functions ----

detect_worktree() {
    IS_WORKTREE=false
    if git rev-parse --is-inside-work-tree &>/dev/null; then
        local git_dir
        git_dir="$(git rev-parse --git-dir 2>/dev/null)"
        if [[ "$git_dir" == */.git/worktrees/* ]]; then
            IS_WORKTREE=true
            # Auto-isolate so a worktree run lives alongside prod:
            #   - different PORT (default 3001) so prod on 3000 keeps serving
            #   - different PM2 name so 'pm2 delete' won't touch prod
            #   - WORKTREE_MODE flag so the server can render a UI warning
            : "${PORT:=3001}"
            export PORT
            # Always append -worktree so forks running with a custom
            # PM2_PROCESS_NAME (e.g. "mytracker") still get isolation —
            # otherwise `pm2 delete $PM2_PROCESS_NAME` later in this script
            # would target the fork's prod process. Skip the rewrite if the
            # suffix is already present (re-entrant runs).
            case "$PM2_PROCESS_NAME" in
                *-worktree) ;;
                *) PM2_PROCESS_NAME="${PM2_PROCESS_NAME}-worktree" ;;
            esac
            export PM2_PROCESS_NAME
            export WORKTREE_MODE=1
            echo "┌──────────────────────────────────────────┐"
            echo "│  Running in git worktree                 │"
            echo "│  Git pull will be skipped                │"
            echo "└──────────────────────────────────────────┘"
            echo "  -> Port:     $PORT"
            echo "  -> PM2 name: $PM2_PROCESS_NAME"
            echo "  -> Production on port 3000 will NOT be touched."
        fi
    fi
}

check_node() {
    if ! command -v node &> /dev/null; then
        echo "Error: Node.js is not installed or not in PATH."
        echo "Install it from https://nodejs.org/ or via your package manager."
        exit 1
    fi
    echo "  -> Node.js $(node --version) found."
}

ensure_pm2() {
    if ! command -v pm2 &> /dev/null; then
        echo "PM2 not found, installing globally..."
        npm install -g pm2
    fi
    echo "  -> PM2 $(pm2 --version) found."
}

ensure_certs() {
    if [ -f certs/server.key ] && [ -f certs/server.crt ]; then
        echo "  -> TLS certificates found."
        # LE cert renewal is handled by the server on startup.
        return
    fi
    echo "  -> TLS certificates not found, generating..."
    if ! command -v openssl &> /dev/null; then
        echo "  -> WARNING: openssl not installed — skipping HTTPS setup."
        echo "     Camera scanning on iOS Safari requires HTTPS."
        echo "     Install openssl and run: bash update_server.sh gen-cert"
        return
    fi
    if [ -f gen-cert.sh ]; then
        bash gen-cert.sh
    else
        echo "  -> WARNING: gen-cert.sh not found — skipping HTTPS setup."
    fi
}

backup_data() {
    BACKUP_DIR="backups"
    mkdir -p "$BACKUP_DIR"
    chmod u+w "$BACKUP_DIR"
    local stamp snapshot
    stamp="$(date +%Y%m%d_%H%M%S)"
    if [ -f meistertracker.db ]; then
        snapshot="$BACKUP_DIR/meistertracker_$stamp.db"
        # The same order as START.bat, for the same reason. In WAL mode
        # meistertracker.db is only as current as the last checkpoint; what was
        # committed since is still in meistertracker.db-wal, and `cp` leaves it
        # behind without a word. The sqlite3 CLI does read the WAL, but the
        # setup in DEPLOYMENT.md never installs it, so on a server set up from
        # there the copy was the branch that always ran. node runs the server,
        # so it is always here, and scripts/backup-db.js reads through the WAL
        # (--no-warnings only silences node:sqlite's ExperimentalWarning, not the
        # reason a run failed). Each attempt is an `if`, so its failure falls
        # through to the next one instead of ending the script under `set -e`.
        if node --no-warnings scripts/backup-db.js meistertracker.db "$snapshot" >/dev/null; then
            echo "  -> meistertracker.db backed up to $snapshot (WAL-consistent)."
        elif command -v sqlite3 &>/dev/null \
            && sqlite3 meistertracker.db ".backup '$snapshot'"; then
            echo "  -> meistertracker.db backed up to $snapshot (WAL-consistent via sqlite3)."
        elif cp meistertracker.db "$snapshot"; then
            echo "  -> WARNING: raw file copy only, to $snapshot."
            echo "     Anything still in meistertracker.db-wal is NOT in this snapshot."
            echo "     Why scripts/backup-db.js failed is printed above."
        else
            echo ""
            echo "  ABORTED: no snapshot of meistertracker.db could be written (see above)."
            echo "  The new version is not started without one. The server is still"
            echo "  running the previous code; fix the cause and run this again."
            exit 1
        fi
    else
        echo "  -> No meistertracker.db found, skipping backup."
    fi
    # The photos are not archived here any more. Every update packed all of
    # data/photos again, onto the same disk, and nothing ever removed an
    # archive. The server keeps a second copy under backups/photos as hard
    # links (no extra space), and the USB stick backup copies each photo once.
    # An update does not touch photo files; the database above is what it
    # changes.
}

# The DuckDNS fallback timer is a one-time root step, and one-time root steps are
# the ones that get put off and then forgotten. This never installs anything —
# it cannot, it runs as the application user — it just refuses to let the gap
# stay invisible. Every branch bails out quietly rather than failing a deploy
# over a reminder.
check_duckdns_fallback() {
    if [ "$IS_WORKTREE" = true ]; then return 0; fi
    if ! command -v systemctl >/dev/null 2>&1; then return 0; fi
    # Only worth saying on a machine where DuckDNS is actually in use. --check
    # reads one row and makes no request.
    if ! node scripts/duckdns-fallback.js --check --quiet >/dev/null 2>&1; then return 0; fi

    # Ask systemd what the timer is *doing*, not whether a file exists. A unit
    # that has been disabled or masked leaves its file in /etc/systemd/system
    # untouched, so the presence check reported "installed" for a timer that had
    # not run in months — the one state an operator cannot see, and the one that
    # produces exactly the outage this is here to prevent.
    local state=""
    if systemctl is-enabled meistertracker-duckdns.timer >/dev/null 2>&1 \
       && systemctl is-active meistertracker-duckdns.timer >/dev/null 2>&1; then
        # Running. Say so only if the job behind it keeps failing.
        if systemctl is-failed meistertracker-duckdns.service >/dev/null 2>&1; then
            echo ""
            echo "  NOTE: the DuckDNS fallback timer is installed but its last run failed."
            echo "            journalctl -u meistertracker-duckdns.service -n 20"
            echo ""
        fi
        return 0
    fi
    if [ -f /etc/systemd/system/meistertracker-duckdns.timer ]; then
        state="installed but not running"
    else
        state="not installed"
    fi

    echo ""
    echo "  NOTE: DuckDNS is configured, but the fallback timer is $state."
    echo "        Without it nothing updates the address while this server is"
    echo "        down — which is exactly when the address changes. One-time:"
    echo ""
    echo "            sudo bash scripts/install-duckdns-fallback.sh"
    echo ""
}

# Try the new code against a copy of the real database before anything is
# swapped. See scripts/preflight.js: it snapshots the live file with VACUUM INTO
# (which does not write to the source), runs the pending migrations there, checks
# no table lost rows, and loads every module the server loads. A failure here
# costs nothing — the old server is still running and still serving.
run_preflight() {
    if [ ! -f scripts/preflight.js ]; then
        echo "  -> No preflight script in this version; skipping."
        return 0
    fi
    if node scripts/preflight.js; then
        return 0
    fi
    echo ""
    echo "  ABORTED: the new version did not pass the preflight."
    echo "  Nothing was changed. The server is still running the previous code."
    echo "  The working tree is now at the new commit — to go back:"
    echo "      git reset --hard stable && npm install --production"
    exit 1
}

# Put production back on the last commit that was known to start, and start it.
#
# The crash was already being detected; what was missing is that anything
# happened next. Printing a log and exiting left the server down until somebody
# read it, which for an unattended update at night means until morning.
rollback_to_stable() {
    if [ "$IS_WORKTREE" = true ]; then return 0; fi
    if ! git rev-parse --verify stable >/dev/null 2>&1; then
        echo "  No 'stable' tag to fall back to — leaving the tree as it is."
        echo "  Recover by hand with: git reset --hard <a working commit>"
        return 1
    fi
    echo ""
    echo "  Rolling back to the last version that started ('stable')..."
    if ! git reset --hard stable; then
        echo "  Rollback failed. The tree is still on the new commit."
        return 1
    fi
    npm install --production || true
    pm2 delete "$PM2_PROCESS_NAME" >/dev/null 2>&1 || true
    pm2 start server.js --name "$PM2_PROCESS_NAME" --update-env
    pm2 save
    sleep 3
    if pm2 show "$PM2_PROCESS_NAME" 2>/dev/null | grep -qi 'online'; then
        echo "  -> Rolled back. The previous version is running again."
        echo "     The failing commit is still on origin/main; fix it there."
        return 0
    fi
    echo "  -> The previous version did not start either. This needs a person."
    return 1
}

do_update() {
    echo "==== Meisterpilze Server — Update & Restart ===="
    check_node
    ensure_pm2

    if [ "$IS_WORKTREE" = true ]; then
        echo "[1/6] Skipping git pull (worktree mode)."
    else
        echo "[1/6] Updating code from git (reset to origin/main)..."
        if ! git fetch origin; then
            echo "Error: git fetch failed."
            exit 1
        fi
        if ! git reset --hard origin/main; then
            echo "Error: git reset --hard origin/main failed."
            exit 1
        fi
    fi

    echo "[2/6] Installing dependencies..."
    npm install --production

    echo "[3/6] Backing up data..."
    backup_data

    echo "[4/6] Ensuring TLS certificates..."
    ensure_certs

    echo "[5/6] Checking the new version against your data..."
    run_preflight

    echo "[6/6] Restarting server..."
    if pm2 describe "$PM2_PROCESS_NAME" > /dev/null 2>&1; then
        echo "  -> Process found, deleting for clean restart..."
        pm2 delete "$PM2_PROCESS_NAME"
    fi
    echo "  -> Starting instance..."
    pm2 start server.js --name "$PM2_PROCESS_NAME" --update-env
    pm2 save

    # Wait briefly for the process to initialize, then verify it stayed up.
    # Match START.bat's "tag stable on success" behavior so we have a marker
    # of the last known-good deployment on both platforms.
    sleep 3
    if ! pm2 show "$PM2_PROCESS_NAME" 2>/dev/null | grep -qi 'online'; then
        echo ""
        echo "  ERROR: Server process crashed on startup."
        echo "  Recent error log:"
        pm2 logs "$PM2_PROCESS_NAME" --lines 15 --nostream --err 2>/dev/null || true
        rollback_to_stable
        exit 1
    fi
    if [ "$IS_WORKTREE" != true ] && command -v git &>/dev/null; then
        if git tag -f stable HEAD > /dev/null 2>&1; then
            echo "  -> Tagged current commit as 'stable'."
        fi
    fi

    echo "==== Update Completed Successfully ===="
    echo "Run 'pm2 logs $PM2_PROCESS_NAME' to see output."
    check_duckdns_fallback
}

do_start() {
    echo "==== Meisterpilze Server — Start ===="
    check_node
    ensure_pm2

    # Ensure dependencies are installed
    if [ -f package.json ] && [ ! -d node_modules ]; then
        echo "Installing dependencies..."
        npm install --production
    fi

    echo "Ensuring TLS certificates..."
    ensure_certs

    if pm2 describe "$PM2_PROCESS_NAME" > /dev/null 2>&1; then
        echo "Process already exists, restarting clean..."
        pm2 delete "$PM2_PROCESS_NAME"
    fi
    echo "Starting instance..."
    pm2 start server.js --name "$PM2_PROCESS_NAME" --update-env
    pm2 save
    echo "==== Server Started ===="
    check_duckdns_fallback
}

do_stop() {
    echo "==== Meisterpilze Server — Stop ===="
    ensure_pm2

    if pm2 describe "$PM2_PROCESS_NAME" > /dev/null 2>&1; then
        pm2 stop "$PM2_PROCESS_NAME"
        pm2 delete "$PM2_PROCESS_NAME"
        echo "Server stopped and removed from PM2."
    else
        echo "Process '$PM2_PROCESS_NAME' not found in PM2 — nothing to stop."
    fi
}

do_status() {
    ensure_pm2
    pm2 status
}

do_gen_cert() {
    echo "==== Generating TLS Certificate ===="
    if ! command -v openssl &> /dev/null; then
        echo "Error: openssl is not installed."
        exit 1
    fi
    if [ -f gen-cert.sh ]; then
        bash gen-cert.sh
        echo ""
        echo "Restart the server for HTTPS to take effect:"
        echo "  bash update_server.sh start"
    else
        echo "Error: gen-cert.sh not found."
        exit 1
    fi
}

show_usage() {
    echo "Usage: bash update_server.sh [command]"
    echo ""
    echo "Commands:"
    echo "  (no command)   Update code from GitHub, back up data, restart server"
    echo "  start          Start the server (without pulling updates)"
    echo "  stop           Stop the server"
    echo "  status         Show PM2 process status"
    echo "  gen-cert       Generate self-signed TLS certificate"
    echo "  help           Show this help message"
}

# ---- Main ----

# Detect worktree before dispatching so every subcommand (update/start/stop/
# status) sees the isolated PM2 name + port. Otherwise running 'start' or
# 'stop' from a worktree would silently target the prod PM2 process.
detect_worktree

case "${1:-update}" in
    update)   do_update   ;;
    start)    do_start    ;;
    stop)     do_stop     ;;
    status)   do_status   ;;
    gen-cert) do_gen_cert ;;
    help|-h|--help)  show_usage ;;
    *)
        echo "Unknown command: $1"
        show_usage
        exit 1
        ;;
esac