Files
ServerKit/scripts/lib/init.sh
T
Juan DenisandClaude Opus 4.8 ff35375b50 feat(install): Phase 3 — multi-distro abstraction, init/env detection, templated unit
Hardening plan §6. Reduces the distro-specific if/else surface and handles
edge environments:

- Dependency manifest (scripts/deps/manifest.yaml): declarative per-family
  package/python/node/docker/init spec. Consumed directly by the Phase 5
  Python bootstrap; install.sh mirrors it inline (it can't parse YAML before
  Python exists in the curl|bash path).

- New sourceable libs (built in parallel, each shellcheck-clean + unit-tested):
  - scripts/lib/pkg.sh  — pkg_detect/refresh/install/is_installed across
    apt/dnf/yum/zypper/pacman/apk (PKG_MGR_OVERRIDE + PKG_DRY_RUN for tests).
  - scripts/lib/init.sh — init_detect + start/stop/enable/disable/reload/
    is_active across systemd/openrc/runit/sysvinit/none (INIT_OVERRIDE/DRY_RUN).
  - scripts/lib/env.sh  — is_container/is_wsl/has_systemd (override-friendly).

- ID_LIKE handling (G5/6.5): install.sh's os_family_from() (now a pure,
  testable function reading ${SERVERKIT_OS_RELEASE}) maps unknown IDs via
  ID_LIKE to debian/rhel/fedora/suse/arch/alpine — rhel before fedora so RHEL
  clones pick the RHEL Docker repo.

- zypper + pacman + apk (G6/6.6): choose_pkg_manager/refresh_pkg_index/pkg_add,
  provision_docker and provision_python gained suse/arch/alpine branches;
  Docker enable/start is now init-aware (systemd OR OpenRC) so it no longer
  aborts on Alpine.

- Templated systemd unit (6.7): serverkit-backend.service → templates/
  serverkit-backend.service.in, rendered (render_service_unit) at install and
  in refresh_config so a custom SERVERKIT_DIR/venv/log path is honored.
  install_service warns-and-skips enable when has_systemd is false (G5).

- nginx TLS floor as a reversible conf.d snippet (6.8): install.sh + update.sh
  prefer /etc/nginx/conf.d/serverkit-tls.conf when nginx.conf doesn't already
  declare ssl_protocols (RHEL/Fedora/SUSE), falling back to the in-place edit
  on Debian/Ubuntu to avoid a 'duplicate ssl_protocols' error. uninstall
  removes only that snippet.

Tests: new scripts/test/test_lib.sh (13) + 4 new test_install.sh cases
(os_family_from, render_service_unit, harden_global_tls A/B). 13+15+10 pass;
shellcheck error+warning clean across all new/changed shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:46:38 -04:00

236 lines
8.6 KiB
Bash

# shellcheck shell=bash
#
# scripts/lib/init.sh — sourceable init-system / service-control abstraction.
#
# ServerKit installs and updates on a grab-bag of Linux distros, and the way you
# start/stop/enable a long-running service differs by init system: systemd on
# most modern boxes, OpenRC on Alpine/Gentoo, runit on Void, and plain SysV
# init.d scripts on older or stripped-down hosts. The installer/updater must be
# able to drive whichever one is present without the call sites caring which —
# otherwise every script that touches the `serverkit` service would grow its own
# `if command -v systemctl ...` ladder. This library hides that behind one small
# verb-based API so callers say "start this service" and the right thing happens.
#
# Contract (these names/behaviors are depended on by callers and unit tests —
# do not rename them or add extra public functions):
# init_detect → echoes: systemd|openrc|runit|sysvinit|none
# init_start <service> → start a service
# init_stop <service> → stop a service
# init_enable <service> → enable at boot
# init_disable <service> → disable at boot
# init_reload [service] → daemon-reload (no arg) / reload a service
# init_is_active <service> → return 0 if running, 1 otherwise (no sudo)
#
# Honors:
# INIT_OVERRIDE=... force the detected init system (used by unit tests)
# INIT_DRY_RUN=1 print intended commands instead of running them
#
# Everything is best-effort: when no init system can be driven, the mutating
# helpers warn loudly and return 1 rather than aborting the caller, and runit's
# enable/disable (which is symlink-based) never aborts even if the symlink op
# fails.
# Cache of the detected init system. Populated lazily, but every public function
# still calls init_detect so that INIT_OVERRIDE/INIT_DRY_RUN flipped per-call in
# tests always take effect — init_detect honours the override before this cache.
_INIT_DETECTED=""
# Prepend sudo when we are not already root and sudo exists. Used only for the
# mutating ops; init_is_active is a read and must work unprivileged.
_init_sudo() {
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
printf 'sudo'
fi
}
# Run a privileged service command, or just print it under dry-run. Kept tiny so
# the dry-run output is exactly the command that would otherwise execute — which
# is what the installer unit tests assert against.
_init_run() {
# Call sites pass "$(_init_sudo)" as the first arg; when root it expands to
# an empty string, which would otherwise show up as a stray leading space in
# the command line. Drop it so the printed/executed command is clean either
# way (and so callers never accidentally exec the empty-string "command").
if [ "$#" -gt 0 ] && [ -z "$1" ]; then
shift
fi
if [ "${INIT_DRY_RUN:-0}" = "1" ]; then
printf ' [init] would run: %s\n' "$*"
return 0
fi
# Discard the tool's own chatter here so callers don't redirect — that
# redirection would also swallow the dry-run line above, which the unit
# tests assert on.
"$@" >/dev/null 2>&1
}
# Detect the active init system. Order matters: systemd wins if it is PID 1, then
# the less-common managers, falling back to SysV and finally "none".
init_detect() {
if [ -n "${INIT_OVERRIDE:-}" ]; then
printf '%s\n' "$INIT_OVERRIDE"
return 0
fi
local comm=""
if [ -d /run/systemd/system ]; then
_INIT_DETECTED="systemd"; printf 'systemd\n'; return 0
fi
if [ -r /proc/1/comm ]; then
read -r comm < /proc/1/comm 2>/dev/null || comm=""
if [ "$comm" = "systemd" ]; then
_INIT_DETECTED="systemd"; printf 'systemd\n'; return 0
fi
fi
if command -v rc-service >/dev/null 2>&1; then
_INIT_DETECTED="openrc"; printf 'openrc\n'; return 0
fi
if command -v sv >/dev/null 2>&1 && { [ -d /etc/runit ] || [ -d /run/runit ]; }; then
_INIT_DETECTED="runit"; printf 'runit\n'; return 0
fi
if [ -d /etc/init.d ] || command -v service >/dev/null 2>&1; then
_INIT_DETECTED="sysvinit"; printf 'sysvinit\n'; return 0
fi
_INIT_DETECTED="none"; printf 'none\n'
return 0
}
# Print "No init system detected" and fail. Centralised so every mutating
# function reports the same actionable message.
_init_no_init() {
local action="$1" svc="$2"
printf ' [init] No init system detected — cannot %s %s; start it manually\n' \
"$action" "$svc" >&2
return 1
}
init_start() {
local svc="$1" init
init="$(init_detect)"
case "$init" in
systemd) _init_run "$(_init_sudo)" systemctl start "$svc" ;;
openrc) _init_run "$(_init_sudo)" rc-service "$svc" start ;;
runit) _init_run "$(_init_sudo)" sv up "$svc" ;;
sysvinit) _init_run "$(_init_sudo)" service "$svc" start ;;
*) _init_no_init "start" "$svc" ;;
esac
}
init_stop() {
local svc="$1" init
init="$(init_detect)"
case "$init" in
systemd) _init_run "$(_init_sudo)" systemctl stop "$svc" ;;
openrc) _init_run "$(_init_sudo)" rc-service "$svc" stop ;;
runit) _init_run "$(_init_sudo)" sv down "$svc" ;;
sysvinit) _init_run "$(_init_sudo)" service "$svc" stop ;;
*) _init_no_init "stop" "$svc" ;;
esac
}
init_enable() {
local svc="$1" init
init="$(init_detect)"
case "$init" in
systemd)
_init_run "$(_init_sudo)" systemctl enable "$svc"
;;
openrc)
_init_run "$(_init_sudo)" rc-update add "$svc"
;;
runit)
# runit enables a service by symlinking it into the supervised dir.
# Best-effort: never abort if the symlink op fails.
_init_run "$(_init_sudo)" ln -s "/etc/sv/$svc" "/etc/service/$svc" || true
;;
sysvinit)
if command -v update-rc.d >/dev/null 2>&1; then
_init_run "$(_init_sudo)" update-rc.d "$svc" enable
elif command -v chkconfig >/dev/null 2>&1; then
_init_run "$(_init_sudo)" chkconfig "$svc" on
else
_init_no_init "enable" "$svc"
fi
;;
*)
_init_no_init "enable" "$svc"
;;
esac
}
init_disable() {
local svc="$1" init
init="$(init_detect)"
case "$init" in
systemd)
_init_run "$(_init_sudo)" systemctl disable "$svc"
;;
openrc)
_init_run "$(_init_sudo)" rc-update del "$svc"
;;
runit)
# Removing the supervised symlink disables the service. Best-effort.
_init_run "$(_init_sudo)" rm -f "/etc/service/$svc" || true
;;
sysvinit)
if command -v update-rc.d >/dev/null 2>&1; then
_init_run "$(_init_sudo)" update-rc.d "$svc" disable
elif command -v chkconfig >/dev/null 2>&1; then
_init_run "$(_init_sudo)" chkconfig "$svc" off
else
_init_no_init "disable" "$svc"
fi
;;
*)
_init_no_init "disable" "$svc"
;;
esac
}
init_reload() {
local svc="${1:-}" init
init="$(init_detect)"
case "$init" in
systemd)
if [ -n "$svc" ]; then
_init_run "$(_init_sudo)" systemctl reload "$svc"
else
_init_run "$(_init_sudo)" systemctl daemon-reload
fi
;;
openrc)
[ -n "$svc" ] && _init_run "$(_init_sudo)" rc-service "$svc" reload
return 0
;;
runit)
[ -n "$svc" ] && _init_run "$(_init_sudo)" sv reload "$svc"
return 0
;;
sysvinit)
[ -n "$svc" ] && _init_run "$(_init_sudo)" service "$svc" reload
return 0
;;
*)
# daemon-reload has no meaning without an init system; a named reload
# is the only mutating intent worth warning about.
if [ -n "$svc" ]; then
_init_no_init "reload" "$svc"
fi
return 0
;;
esac
}
# Report whether a service is currently running. Read-only: no sudo, and the
# tool's own output is discarded so callers can rely purely on the exit status.
init_is_active() {
local svc="$1" init
init="$(init_detect)"
case "$init" in
systemd) systemctl is-active --quiet "$svc" ;;
openrc) rc-service "$svc" status >/dev/null 2>&1 ;;
runit) sv status "$svc" >/dev/null 2>&1 ;;
sysvinit) service "$svc" status >/dev/null 2>&1 ;;
*) return 1 ;;
esac
}