From f3e71f8a13d8c1fe1991eca0ab9075ff1edc80e4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 Oct 2026 11:43:56 +0200 Subject: [PATCH] Restart active OWE user services after upgrades (#888) --- .github/workflows/test.yml | 1 + pkgbuilds/owe/90-owe-restart.hook | 9 +++ pkgbuilds/owe/PKGBUILD | 12 +++- pkgbuilds/owe/package-restart | 21 +++++++ tests/owe-package-restart.py | 94 +++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 pkgbuilds/owe/90-owe-restart.hook create mode 100755 pkgbuilds/owe/package-restart create mode 100644 tests/owe-package-restart.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92af7c9d..041740bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,7 @@ jobs: archlinux:base-devel bash -lc ' set -euo pipefail pacman -Syu --noconfirm git jq python libarchive neovim tmux + python tests/owe-package-restart.py python tests/oma-service-removal.py python tests/upstream-watch.py bash tests/ipu7-install.sh diff --git a/pkgbuilds/owe/90-owe-restart.hook b/pkgbuilds/owe/90-owe-restart.hook new file mode 100644 index 00000000..a36feb17 --- /dev/null +++ b/pkgbuilds/owe/90-owe-restart.hook @@ -0,0 +1,9 @@ +[Trigger] +Operation = Upgrade +Type = Package +Target = owe + +[Action] +Description = Restarting active OWE user services... +When = PostTransaction +Exec = /usr/lib/owe/package-restart diff --git a/pkgbuilds/owe/PKGBUILD b/pkgbuilds/owe/PKGBUILD index 35391632..395fb444 100644 --- a/pkgbuilds/owe/PKGBUILD +++ b/pkgbuilds/owe/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: owe contributors pkgname=owe pkgver=0.2.10 -pkgrel=3 +pkgrel=4 pkgdesc="High-performance wallpaper engine for Omarchy (mp4, gif, stills)" arch=('x86_64' 'aarch64') url="https://github.com/omacom/owe" @@ -12,9 +12,13 @@ checkdepends=('python') optdepends=('intel-media-driver: VAAPI hardware decode on Intel GPUs' 'libva-mesa-driver: VAAPI hardware decode on AMD GPUs') source=("$pkgname-$pkgver.tar.gz::https://github.com/omacom/owe/archive/refs/tags/v$pkgver.tar.gz" - 'lock-policy-timestamp.patch') + 'lock-policy-timestamp.patch' + '90-owe-restart.hook' + 'package-restart') sha256sums=('b171397c24c0ffd6096781b3f744f674055a57f660a74db07a3b71a111a6980b' - '3ebf413093922b0917bca60c42720c6424c77b1695317364bf73f9fd745376d8') + '3ebf413093922b0917bca60c42720c6424c77b1695317364bf73f9fd745376d8' + 'aee4a08132f0528575d3b7364defc070b8af1648cc48559238ba089c09504b62' + 'ef1fc780425343623c4a4ddd36cc58cd89ba8a71c528641f4b421db834a349cd') prepare() { cd "$srcdir/owe-$pkgver" @@ -43,6 +47,8 @@ check() { package() { DESTDIR="$pkgdir" ninja -C build install + install -Dm644 "$srcdir/90-owe-restart.hook" "$pkgdir/usr/share/libalpm/hooks/90-owe-restart.hook" + install -Dm755 "$srcdir/package-restart" "$pkgdir/usr/lib/owe/package-restart" install -d "$pkgdir/usr/lib/systemd/user" sed 's|%h/.local/bin/owed|/usr/bin/owed|' "$srcdir/owe-$pkgver/systemd/owed.service" \ >"$pkgdir/usr/lib/systemd/user/owed.service" diff --git a/pkgbuilds/owe/package-restart b/pkgbuilds/owe/package-restart new file mode 100755 index 00000000..77a3f025 --- /dev/null +++ b/pkgbuilds/owe/package-restart @@ -0,0 +1,21 @@ +#!/bin/bash + +# Run after systemd's 30-systemd-daemon-reload-user.hook. Image builds and +# offline installations have no running managers and must remain untouched. +if systemd-detect-virt --chroot || ! systemd-notify --booted; then + exit 0 +fi + +managers=$(systemctl list-units 'user@*.service' --state=running --no-legend --plain) || exit 1 +result=0 +while read -r unit _; do + [[ $unit =~ ^user@([0-9]+)\.service$ ]] || continue + user_id=${BASH_REMATCH[1]} + # Use the user's manager rather than pacman's root session. try-restart + # never starts a stopped service or changes its enablement. + if ! systemctl --user --machine="$user_id@.host" try-restart owed.service; then + echo ":: Could not restart OWE for user $user_id; restart owed.service in that user session." >&2 + result=1 + fi +done <<< "$managers" +exit "$result" diff --git a/tests/owe-package-restart.py b/tests/owe-package-restart.py new file mode 100644 index 00000000..f3fee3a3 --- /dev/null +++ b/tests/owe-package-restart.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Package restart fixtures; no real systemd managers are contacted.""" +import hashlib +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE = ROOT / "pkgbuilds/owe" + + +class Restart(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="owe-restart-") + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.log = self.root / "calls" + self.env = dict(os.environ, PATH=f"{self.root}:{os.environ['PATH']}", CALLS=str(self.log)) + self.command("systemd-detect-virt", 'exit "${CHROOT:-1}"') + self.command("systemd-notify", 'exit "${BOOTED:-0}"') + self.command("systemctl", ''' +printf '%s\\n' "$*" >> "$CALLS" +case "$1" in + list-units) + printf '%s\\n' "${MANAGERS:-}" + exit "${LIST_FAILED:-0}" + ;; +esac +[[ $* != *"--machine=${FAIL_USER:-none}@.host"* ]] +''') + + def command(self, name, body): + path = self.root / name + path.write_text("#!/bin/bash\n" + body + "\n") + path.chmod(0o755) + + def run_hook(self, **values): + return subprocess.run(["bash", str(PACKAGE / "package-restart")], + env=self.env | values, capture_output=True, text=True) + + def calls(self): + return self.log.read_text().splitlines() if self.log.exists() else [] + + def test_offline_and_chroot_skip_managers(self): + for values in ({"CHROOT": "0"}, {"BOOTED": "1"}): + with self.subTest(values=values): + self.assertEqual(self.run_hook(**values).returncode, 0) + self.assertEqual(self.calls(), []) + + def test_no_running_managers(self): + self.assertEqual(self.run_hook().returncode, 0) + self.assertEqual(len(self.calls()), 1) + + def test_all_running_managers_use_conditional_restart(self): + result = self.run_hook(MANAGERS="user@1000.service loaded active running User Manager\nuser@1001.service loaded active running User Manager") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.calls(), [ + "list-units user@*.service --state=running --no-legend --plain", + "--user --machine=1000@.host try-restart owed.service", + "--user --machine=1001@.host try-restart owed.service", + ]) + + def test_failure_reports_user_and_continues(self): + result = self.run_hook(MANAGERS="user@1000.service loaded active running\nuser@1001.service loaded active running", FAIL_USER="1000") + self.assertNotEqual(result.returncode, 0) + self.assertIn("Could not restart OWE for user 1000", result.stderr) + self.assertIn("--user --machine=1001@.host try-restart owed.service", self.calls()) + + def test_list_failure_is_not_silently_successful(self): + self.assertNotEqual(self.run_hook(LIST_FAILED="1").returncode, 0) + self.assertEqual(len(self.calls()), 1) + + def test_only_user_manager_units_are_accepted(self): + self.assertEqual(self.run_hook(MANAGERS="invalid.service loaded active running\nuser@oops.service loaded active running").returncode, 0) + self.assertEqual(len(self.calls()), 1) + + def test_packaged_hook_and_sources(self): + hook = (PACKAGE / "90-owe-restart.hook").read_text() + self.assertIn("Operation = Upgrade\n", hook) + self.assertNotIn("Operation = Install", hook) + self.assertNotIn("Operation = Remove", hook) + self.assertIn("When = PostTransaction\n", hook) + self.assertIn("Exec = /usr/lib/owe/package-restart\n", hook) + recipe = (PACKAGE / "PKGBUILD").read_text() + for source in ("90-owe-restart.hook", "package-restart"): + self.assertIn(hashlib.sha256((PACKAGE / source).read_bytes()).hexdigest(), recipe) + self.assertIn('"$pkgdir/usr/share/libalpm/hooks/90-owe-restart.hook"', recipe) + self.assertIn('"$pkgdir/usr/lib/owe/package-restart"', recipe) + + +if __name__ == "__main__": + unittest.main()