157 lines
6.4 KiB
Bash
Executable File
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
|