From f57d68a7891b93d1d9cde25aa917b5ac51dd270d Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 12 Nov 2026 17:25:30 +0100 Subject: [PATCH 1/2] Start and restart a .deb's services after binary mode installs it Binary mode installs with SYSTEMD_OFFLINE=1 so systemd-tmpfiles does not fail in unprivileged containers. That also makes the package scripts skip their own service start and restart (systemctl prints "ignoring command"). An upgrade left the old process running on replaced files unless the script restarted it itself, and a fresh install left enabled services stopped (seen with Euro-Office). After the install core now does what the package would have done: restart the package's units that were running before, and on a fresh install start the ones the package enabled. A unit the script stopped before the update stays stopped, so scripts that stop, update, migrate and then start keep their order. Template units are left alone. --- .github/workflows/verify-split-files.yml | 4 ++ lib/API.txt | 1 + lib/forge.func | 34 ++++++++++ tests/forge-deb-services.sh | 81 ++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 tests/forge-deb-services.sh 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..533f2ad 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -2267,6 +2267,14 @@ _forge_is_script_app() { [[ "$fetched" == *"$script"* || "$script" == *"$fetched"* ]] } +# The service units an installed package ships, by name. Templates (foo@) are +# left out: they have no instance to start. Prints nothing 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 +2468,23 @@ 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 also makes the package scripts skip their own service + # start and restart. Do what they would have done afterwards: restart what + # ran before (an upgrade otherwise leaves the old process on replaced + # files) and start what a fresh install enabled. A unit the caller stopped + # before the update stays stopped, so its own order is kept. + 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 +2492,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..7bc72cf --- /dev/null +++ b/tests/forge-deb-services.sh @@ -0,0 +1,81 @@ +#!/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")" + +# _forge_deb_units only lists units on a systemd host. +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; } +# INSTALLED=yes: the package is there before this run (an upgrade). +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 +} +# ACTIVE / ENABLED: the units running or enabled before the install. +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 ]] From 62f39bb67750b402a1129c6de1733ce253fb4c71 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:42:48 +0100 Subject: [PATCH 2/2] Trim the comments to what a reader would get wrong --- lib/forge.func | 9 ++------- tests/forge-deb-services.sh | 3 --- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/forge.func b/lib/forge.func index 533f2ad..8ccceb3 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -2267,8 +2267,7 @@ _forge_is_script_app() { [[ "$fetched" == *"$script"* || "$script" == *"$fetched"* ]] } -# The service units an installed package ships, by name. Templates (foo@) are -# left out: they have no instance to start. Prints nothing without systemd. +# 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 @@ -2468,11 +2467,7 @@ 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 also makes the package scripts skip their own service - # start and restart. Do what they would have done afterwards: restart what - # ran before (an upgrade otherwise leaves the old process on replaced - # files) and start what a fresh install enabled. A unit the caller stopped - # before the update stays stopped, so its own order is kept. + # 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)" diff --git a/tests/forge-deb-services.sh b/tests/forge-deb-services.sh index 7bc72cf..549a68c 100644 --- a/tests/forge-deb-services.sh +++ b/tests/forge-deb-services.sh @@ -4,7 +4,6 @@ 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")" -# _forge_deb_units only lists units on a systemd host. if [[ ! -d /run/systemd/system ]]; then echo "SKIP: no /run/systemd/system on this machine" exit 0 @@ -40,7 +39,6 @@ _forge_asset_urls() { echo "https://example.invalid/demo_2.0.0_amd64.deb"; } curl_download() { : >"$1"; } apt() { echo "apt $*" >>"$LOG"; } dpkg-deb() { echo demo; } -# INSTALLED=yes: the package is there before this run (an upgrade). dpkg() { case "$1" in --print-architecture) echo amd64 ;; @@ -49,7 +47,6 @@ dpkg() { /lib/systemd/system/demo-worker.service /lib/systemd/system/demo-job@.service ;; esac } -# ACTIVE / ENABLED: the units running or enabled before the install. systemctl() { case "$1" in is-active) [[ " $ACTIVE " == *" $3 "* ]] ;;