From 36ec27a6699d9db1c1226f3df2f95db6a9cf8ef0 Mon Sep 17 00:00:00 2001 From: Aalman Sadath <96546470+AalmanSadath@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:28:49 +1000 Subject: [PATCH 1/2] fix(linux): stop the agent when the package is removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `postremove.sh` only reloaded udev rules, so removing the .deb or .rpm deleted the binaries and left the agent running. The unit stayed enabled too, which means it also came back on the next login. A stranded agent is not merely untidy. It keeps HID++ sessions open on the same /dev/hidraw* nodes as whatever OpenLogi the user is running now, and those devices admit one owner at a time — the same contention the issue template's pre-flight checklist warns about for Logi Options+. Devices then go missing or come back misdetected, with nothing pointing at a package that is no longer installed. When the survivor belongs to the same install as the new client it also keeps the singleton lock and the IPC socket. uninstall.sh has done this correctly all along; only the packaged path was missing it. The awkward part is that the agent is a per-user service while this script runs as root under the package manager, so there is no single session to address. Walk /run/user instead and disable the unit for each logged-in user. `disable --now` rather than `stop`, because the unit is Restart=on-failure and the agent may have written its own copy to $XDG_CONFIG_HOME/systemd/user/ for launch-at-login; both share a unit name, so one disable covers whichever is active and drops the symlink that would restart it. An agent the desktop app spawned is not under systemd at all, so that pass cannot reach it. Match those on their executable path, which keeps a Flatpak, a source build, or a /usr/local install untouched. Both managers also run this on upgrade, spelled differently: dpkg passes "remove"/"purge"/"upgrade", rpm passes the number of instances left behind. Act only on the removal forms, so an update no longer silently turns autostart off. --- packaging/linux/nfpm-scripts/postremove.sh | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packaging/linux/nfpm-scripts/postremove.sh b/packaging/linux/nfpm-scripts/postremove.sh index e0fb1f2e8..e9a8e74f0 100755 --- a/packaging/linux/nfpm-scripts/postremove.sh +++ b/packaging/linux/nfpm-scripts/postremove.sh @@ -1,6 +1,66 @@ #!/bin/sh set -eu +# Both package managers call this on upgrade as well as removal, and they spell +# the distinction differently: dpkg passes "remove"/"purge"/"upgrade", rpm passes +# the number of instances left behind (0 on the final removal, >=1 on upgrade). +# Stopping the agent on an upgrade would turn autostart off on every update, so +# only act when the package is actually going away. +removing=no +case "${1:-}" in + 0 | remove | purge) removing=yes ;; +esac + +if [ "$removing" = yes ]; then + # The agent is a *user* service, but this script runs as root under the package + # manager, so there is no single session to target. Walk the runtime directories + # of logged-in users instead. + # + # `disable --now` rather than `stop`: the unit is Restart=on-failure, and the + # agent may also have written its own copy to $XDG_CONFIG_HOME/systemd/user/ for + # launch-at-login. Both carry the same unit name, so one disable stops whichever + # is active and drops the .wants symlink that would otherwise start it again. + if command -v systemctl >/dev/null 2>&1; then + if command -v runuser >/dev/null 2>&1; then + as_user="runuser -u" + elif command -v sudo >/dev/null 2>&1; then + as_user="sudo -u" + else + as_user="" + fi + + if [ -n "$as_user" ]; then + for runtime in /run/user/*; do + [ -d "$runtime" ] || continue + uid=${runtime#/run/user/} + case $uid in + '' | *[!0-9]*) continue ;; + esac + user=$(id -nu "$uid" 2>/dev/null) || continue + # shellcheck disable=SC2086 # as_user is a command plus its flag, split on purpose + $as_user "$user" -- env XDG_RUNTIME_DIR="$runtime" \ + systemctl --user disable --now openlogi-agent.service >/dev/null 2>&1 || true + done + fi + fi + + # An agent the desktop app launched is not under systemd at all — the GUI spawns + # it from beside its own executable — so the disable above cannot reach it, and + # it would keep serving IPC to whatever connects next. Match on the executable + # path so a Flatpak or source-built agent, which the user may still want, is left + # alone. Our binary is already gone by this point, hence the "(deleted)" form. + if command -v pgrep >/dev/null 2>&1; then + for pid in $(pgrep -x openlogi-agent 2>/dev/null || true); do + exe=$(readlink "/proc/${pid}/exe" 2>/dev/null) || continue + case "$exe" in + /usr/bin/openlogi-agent | "/usr/bin/openlogi-agent (deleted)") + kill "$pid" 2>/dev/null || true + ;; + esac + done + fi +fi + # Reload udev rules and wait for the uaccess revocation to take effect. if command -v udevadm >/dev/null 2>&1; then udevadm control --reload-rules From 1a29efaa8afee4518876ea1403a8191eae9a1740 Mon Sep 17 00:00:00 2001 From: Aalman Sadath <96546470+AalmanSadath@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:27:26 +1000 Subject: [PATCH 2/2] docs(linux): record what postremove deliberately does not reach Comment-only. Two points a reviewer raised, kept next to the code so the next reader does not have to rediscover them. Removing the unit file before this script runs does not defeat the disable: systemd matches .wants symlinks by unit name rather than by reading [Install], so a dangling symlink is still cleaned up. A logged-out user without linger keeps a dangling symlink until the package is reinstalled, and that is deliberate. Root has no route into their systemd instance, and the standard tooling does not attempt one either, so the residue is inert rather than worth chasing through home directories. --- packaging/linux/nfpm-scripts/postremove.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packaging/linux/nfpm-scripts/postremove.sh b/packaging/linux/nfpm-scripts/postremove.sh index e9a8e74f0..7ba2803b9 100755 --- a/packaging/linux/nfpm-scripts/postremove.sh +++ b/packaging/linux/nfpm-scripts/postremove.sh @@ -20,6 +20,17 @@ if [ "$removing" = yes ]; then # agent may also have written its own copy to $XDG_CONFIG_HOME/systemd/user/ for # launch-at-login. Both carry the same unit name, so one disable stops whichever # is active and drops the .wants symlink that would otherwise start it again. + # Removing the unit file first does not defeat this: systemd matches .wants + # symlinks by unit name, so a dangling one is still cleaned up here. + # + # /run/user covers logged-in and lingering users. A logged-out user without + # linger keeps a dangling symlink that starts nothing (the unit file went with + # the package) until the package is reinstalled. That is deliberate, not an + # oversight: root has no route into a logged-out user's systemd instance, and + # the standard tooling does not try either — Fedora's %systemd_user_* macros + # and Debian's deb-systemd-helper both manage global enablement under + # /etc/systemd/user and never touch per-user config. Do not "fix" this by + # walking /home and deleting files inside user config directories. if command -v systemctl >/dev/null 2>&1; then if command -v runuser >/dev/null 2>&1; then as_user="runuser -u"