diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 9823dca..5909a40 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -71,6 +71,7 @@ on: - "tests/forge-clean-install.sh" - "tests/db-helper-context.sh" - "tests/forge-tag-spelling.sh" + - "tests/forge-deb-services.sh" - "tests/cs-clear.sh" - "tests/forge-appversion-scope.sh" - "tests/forge-singlefile-compressed.sh" @@ -141,6 +142,7 @@ on: - "tests/forge-clean-install.sh" - "tests/db-helper-context.sh" - "tests/forge-tag-spelling.sh" + - "tests/forge-deb-services.sh" - "tests/cs-clear.sh" - "tests/forge-appversion-scope.sh" - "tests/forge-singlefile-compressed.sh" @@ -301,6 +303,8 @@ jobs: run: bash tests/db-helper-context.sh - name: Pinned tag with or without its v tests run: bash tests/forge-tag-spelling.sh + - name: Services after a .deb install tests + run: bash tests/forge-deb-services.sh - name: Screen clear without a terminal tests run: bash tests/cs-clear.sh - name: App version pin and .deb stamp tests diff --git a/lib/API.txt b/lib/API.txt index 26c21d0..01e2d32 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -17,6 +17,7 @@ _docker_setup_lxcfs_visibility _download_source_tarball _forge_asset_urls _forge_curl +_forge_deb_units _forge_descriptor _forge_designated_release _forge_disk_full diff --git a/lib/forge.func b/lib/forge.func index 2cdcbff..8ccceb3 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -2267,6 +2267,13 @@ _forge_is_script_app() { [[ "$fetched" == *"$script"* || "$script" == *"$fetched"* ]] } +# Service units a package ships, templates (foo@) left out; none without systemd. +_forge_deb_units() { + command -v systemctl >/dev/null 2>&1 || return 0 + [[ -d /run/systemd/system ]] || return 0 + dpkg -L "$1" 2>/dev/null | grep -E '/systemd/system/[^/@]+\.service$' | sed 's#.*/##' | sort -u +} + # ------------------------------------------------------------------------------ # fetch_and_deploy_release() # @@ -2460,6 +2467,19 @@ fetch_and_deploy_release() { local dpkg_opts="" [[ "${DPKG_FORCE_CONFOLD:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confold" [[ "${DPKG_FORCE_CONFNEW:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confnew" + # SYSTEMD_OFFLINE skips the package's own service start: restart what ran, start what a fresh install enabled. + local deb_pkg deb_fresh="" unit + local -a deb_was_active=() + deb_pkg="$(dpkg-deb -f "$tmpdir/$filename" Package 2>/dev/null)" + if [[ -n "$deb_pkg" ]]; then + if dpkg -s "$deb_pkg" >/dev/null 2>&1; then + for unit in $(_forge_deb_units "$deb_pkg"); do + systemctl is-active --quiet "$unit" 2>/dev/null && deb_was_active+=("$unit") + done + else + deb_fresh=1 + fi + fi DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y $dpkg_opts "$tmpdir/$filename" || { SYSTEMD_OFFLINE=1 dpkg -i "$tmpdir/$filename" >"$tmpdir/dpkg.log" 2>&1 || { _diagnose_deb_failure "$tmpdir/$filename" "$tmpdir/dpkg.log" @@ -2467,6 +2487,15 @@ fetch_and_deploy_release() { return 100 } } + if [[ -n "$deb_pkg" ]]; then + for unit in $(_forge_deb_units "$deb_pkg"); do + if [[ " ${deb_was_active[*]} " == *" $unit "* ]]; then + systemctl restart "$unit" 2>/dev/null || msg_warn "${unit} did not restart after the ${deb_pkg} update" + elif [[ -n "$deb_fresh" ]] && systemctl is-enabled --quiet "$unit" 2>/dev/null; then + systemctl start "$unit" 2>/dev/null || msg_warn "${unit} did not start after installing ${deb_pkg}" + fi + done + fi ;; prebuild | singlefile) diff --git a/tests/forge-deb-services.sh b/tests/forge-deb-services.sh new file mode 100644 index 0000000..549a68c --- /dev/null +++ b/tests/forge-deb-services.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^(fetch_and_deploy_release|_forge_deb_units|_forge_workdir|_forge_is_script_app|_forge_descriptor|_forge_strip_v|_forge_previous_version)[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" + +if [[ ! -d /run/systemd/system ]]; then + echo "SKIP: no /run/systemd/system on this machine" + exit 0 +fi + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +export HOME="$TEST_DIR/home" +mkdir -p "$HOME" +LOG="$TEST_DIR/log" +passed=0 failed=0 + +check() { + if [[ "$3" == "$2" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + sed 's/^/ /' "$LOG" >&2 + failed=$((failed + 1)) + fi +} + +# shellcheck disable=SC2034 # read by the eval'd helpers +STD="" +jq() { command jq "$@" | tr -d '\r'; } +msg_info() { :; } +msg_ok() { :; } +msg_warn() { echo "warn: $*" >>"$LOG"; } +msg_error() { echo "error: $*" >>"$LOG"; } +_forge_release_json() { printf '{"tag_name":"v2.0.0"}' >"$5"; } +_forge_asset_urls() { echo "https://example.invalid/demo_2.0.0_amd64.deb"; } +curl_download() { : >"$1"; } +apt() { echo "apt $*" >>"$LOG"; } +dpkg-deb() { echo demo; } +dpkg() { + case "$1" in + --print-architecture) echo amd64 ;; + -s) [[ "$INSTALLED" == yes ]] ;; + -L) printf '%s\n' /usr/bin/demo /lib/systemd/system/demo.service \ + /lib/systemd/system/demo-worker.service /lib/systemd/system/demo-job@.service ;; + esac +} +systemctl() { + case "$1" in + is-active) [[ " $ACTIVE " == *" $3 "* ]] ;; + is-enabled) [[ " $ENABLED " == *" $3 "* ]] ;; + restart | start) echo "$1 $2" >>"$LOG" ;; + esac +} + +deploy() { + : >"$LOG" + rm -f "$HOME"/.demo* + fetch_and_deploy_release github demo owner/demo binary latest "" "demo_*_amd64.deb" >/dev/null 2>&1 + grep -E '^(start|restart) ' "$LOG" | sort | tr '\n' ' ' | sed 's/ $//' +} + +INSTALLED=no ACTIVE="" ENABLED="demo.service" +check "fresh install starts the enabled unit only" "start demo.service" "$(deploy)" + +INSTALLED=yes ACTIVE="demo.service demo-worker.service" ENABLED="demo.service demo-worker.service" +check "upgrade restarts the units that ran" "restart demo-worker.service restart demo.service" "$(deploy)" + +INSTALLED=yes ACTIVE="" ENABLED="demo.service" +check "a unit stopped before the update stays stopped" "" "$(deploy)" + +INSTALLED=yes ACTIVE="demo.service" ENABLED="demo.service" +check "templates are never touched" "0" "$(deploy >/dev/null; grep -c 'demo-job@' "$LOG")" + +echo "${passed} passed, ${failed} failed" +[[ "$failed" -eq 0 ]]