update_server.sh
Rama main · réplica de lectura del repositorio original.
#!/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