maestro/scripts/install-systemd.sh
oss-sync b1292e34b2
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (edc775f2)
2026-07-06 01:04:12 +00:00

157 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Install MAESTRO as a systemd service so it starts automatically on boot.
#
# Fills in deploy/maestro.service (a template) from the current checkout — node
# path, project directory and run user are auto-detected — then installs and
# enables the unit. systemd supervises `node dist/main.js` directly; it does not
# build, so build first (`scripts/server.sh start` or `npm run build`).
#
# scripts/install-systemd.sh # user service (no root, runs as you) [default]
# scripts/install-systemd.sh --print # preview generated unit, install nothing
# scripts/install-systemd.sh --mode system # system-wide service (needs sudo)
# scripts/install-systemd.sh --run-as bot # run as a specific user (system mode only)
# scripts/install-systemd.sh --name maestro # override service name
# scripts/install-systemd.sh --no-start # enable for boot but don't start now
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
TEMPLATE="$PROJECT_DIR/deploy/maestro.service"
MODE="user" # user | system (default: user — no root, runs as you)
NAME="maestro"
RUN_AS="" # system mode only; default resolved below
PRINT_ONLY=0
NO_START=0
die() { echo "error: $*" >&2; exit 1; }
# Consume the value of a `--opt value` flag, erroring cleanly if it's missing
# (a bare `shift 2` on the last arg would abort under `set -e` with no message).
need_val() { [[ $# -ge 2 ]] || die "$1 requires a value"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) need_val "$@"; MODE="$2"; shift 2 ;;
--mode=*) MODE="${1#*=}"; shift ;;
--name) need_val "$@"; NAME="$2"; shift 2 ;;
--name=*) NAME="${1#*=}"; shift ;;
--run-as) need_val "$@"; RUN_AS="$2"; shift 2 ;;
--run-as=*) RUN_AS="${1#*=}"; shift ;;
--print|--dry-run) PRINT_ONLY=1; shift ;;
--no-start) NO_START=1; shift ;;
-h|--help)
# Print the leading comment block (after the shebang) as usage text.
awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0"
exit 0 ;;
*) die "unknown argument: $1" ;;
esac
done
[[ "$MODE" == "system" || "$MODE" == "user" ]] || die "--mode must be 'system' or 'user' (got '$MODE')"
[[ "$NAME" =~ ^[A-Za-z0-9_.-]+$ ]] || die "invalid --name '$NAME'"
[[ -f "$TEMPLATE" ]] || die "template not found: $TEMPLATE"
# --- Auto-detect substitution values ------------------------------------------
NODE_BIN="$(command -v node || true)"
[[ -n "$NODE_BIN" ]] || die "node not found in PATH"
if [[ "$MODE" == "system" ]]; then
# Run user: explicit flag > invoking user under sudo > current user.
# Never default to root (the app should not build/run as root).
RUN_USER="${RUN_AS:-${SUDO_USER:-$USER}}"
[[ "$RUN_USER" != "root" ]] || echo "warning: run user resolves to 'root'; pass --run-as <user> to run the app as a non-root user." >&2
# Existence is enforced at install time (below), not for --print previews.
else
# user mode: the service belongs to the invoking user's systemd manager.
# Running under sudo would resolve $USER to root and install a stray unit
# under /root — almost never intended. Steer to the right path instead.
if [[ $EUID -eq 0 ]]; then
die "user mode as root would install under /root. Run WITHOUT sudo, or for a machine-wide service use: --mode system --run-as <user>"
fi
[[ -z "$RUN_AS" ]] || echo "warning: --run-as is ignored in user mode (the service always runs as '$USER'); use --mode system to run as another user." >&2
RUN_USER="$USER"
fi
# --- Preflight (skipped for --print) ------------------------------------------
if [[ "$PRINT_ONLY" -eq 0 ]]; then
[[ -f "$PROJECT_DIR/dist/main.js" ]] || die "dist/main.js missing — build first: scripts/server.sh start (or npm run build)"
if [[ "$MODE" == "system" ]]; then
id "$RUN_USER" >/dev/null 2>&1 || die "run user '$RUN_USER' does not exist (pass --run-as <user>)"
fi
fi
# --- Generate the unit from the template --------------------------------------
generate_unit() {
# Substitute placeholders. PROJECT_DIR/NODE_BIN are absolute paths without
# '|' or '&', safe as sed replacements.
sed \
-e "s|@PROJECT_DIR@|$PROJECT_DIR|g" \
-e "s|@NODE@|$NODE_BIN|g" \
-e "s|@RUN_USER@|$RUN_USER|g" \
"$TEMPLATE" | grep -v '^#' | grep -v '^$' \
| if [[ "$MODE" == "user" ]]; then
# A user manager owns the process — drop User=, use user-session targets,
# and don't wait on the system-only network-online.target.
sed \
-e '/^User=/d' \
-e '/^Wants=network-online.target/d' \
-e 's|^After=network-online.target|After=default.target|' \
-e 's|^WantedBy=multi-user.target|WantedBy=default.target|'
else
cat
fi
}
UNIT_TEXT="$(generate_unit)"
if [[ "$PRINT_ONLY" -eq 1 ]]; then
echo "# --- generated ${NAME}.service (mode=${MODE}) ---"
echo "$UNIT_TEXT"
exit 0
fi
# --- Install ------------------------------------------------------------------
if [[ "$MODE" == "system" ]]; then
UNIT_PATH="/etc/systemd/system/${NAME}.service"
if [[ $EUID -eq 0 ]]; then
priv() { "$@"; }
else
command -v sudo >/dev/null 2>&1 || die "system install needs root; re-run with sudo"
priv() { sudo "$@"; }
fi
echo "Installing $UNIT_PATH (run user: $RUN_USER)…"
printf '%s\n' "$UNIT_TEXT" | priv tee "$UNIT_PATH" >/dev/null
priv systemctl daemon-reload
if [[ "$NO_START" -eq 1 ]]; then
priv systemctl enable "$NAME"
echo "Enabled for boot. Start later with: sudo systemctl start $NAME"
else
priv systemctl enable --now "$NAME"
fi
echo "Done. Status: systemctl status $NAME"
echo " Logs: journalctl -u $NAME -f"
else
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
UNIT_PATH="$UNIT_DIR/${NAME}.service"
mkdir -p "$UNIT_DIR"
echo "Installing $UNIT_PATH"
printf '%s\n' "$UNIT_TEXT" > "$UNIT_PATH"
systemctl --user daemon-reload
# Linger lets the user service run at boot without an active login session.
if ! loginctl enable-linger "$USER" 2>/dev/null; then
echo "warning: could not enable linger for '$USER'; the service may not start until you log in." >&2
echo " Retry with: sudo loginctl enable-linger $USER" >&2
fi
if [[ "$NO_START" -eq 1 ]]; then
systemctl --user enable "$NAME"
echo "Enabled for boot. Start later with: systemctl --user start $NAME"
else
systemctl --user enable --now "$NAME"
fi
echo "Done. Status: systemctl --user status $NAME"
echo " Logs: journalctl --user -u $NAME -f"
fi