Harden HDMI recovery and add tested user installer release tooling

This commit is contained in:
admin committed 2026-09-23 09:16:57 +02:00
1 parent 73e727ef4c
commit d963224cb7
17 files changed
+903 -646

No files matched your search

+26 -76
View File
@@ -1,81 +1,31 @@
# Architecture
## Problem
The workaround resets the display below the compositor by briefly leaving the
active graphical VT. It is intended for one local Hyprland session on `seat0`.
A monitor shared between multiple physical HDMI inputs can keep its EDID/output state visible to Linux even while the monitor is displaying another source. When switching back, Hyprland may report the output as completely healthy while the physical monitor remains black or reports no source.
1. A systemd user service runs `udevadm monitor --kernel --property
--subsystem-match=drm`. The watcher owns a per-user lock.
2. Each `HOTPLUG=1` line causes a loginctl lookup for the current UID, Wayland,
active state, seat0 and a valid VT. A known different desktop is excluded.
3. Events inside the cooldown are ignored. The clock is Bash's elapsed seconds;
timestamps are not read from shared files or affected by wall-clock changes.
4. The synchronous recovery helper locks, independently identifies the active
session, waits for HDMI settling, checks the Hyprland IPC output and refreshes
DPMS/focus. The instance signature comes from the Hyprland startup hook.
5. Before switching, the helper rechecks session identity and VT and checks sudo
permission for both directions. It rechecks session identity after those calls.
6. The helper sets an EXIT/signal trap, switches to VT2 (when graphical VT=1) or
VT1 (otherwise), and returns to the detected graphical VT.
7. Failed returns and catchable signals cause an emergency return attempt. Success
is logged only after the final DPMS/focus refresh succeeds.
8. The watcher logs the helper's actual exit status and starts the cooldown at
completion. Unexpected event-stream EOF always returns nonzero to systemd.
Typical Hyprland state in the broken condition:
Shared validation and session detection live in `lib/common.sh`. Runtime state
is restricted to a user-owned mode-0700 `XDG_RUNTIME_DIR/revive-monitor`. Root is
used only for `chvt`; user scripts are never invoked through sudo.
```text
focused: yes
dpmsStatus: 1
disabled: false
active workspace: 1
```
Commands such as `hyprctl reload`, `hyprctl dispatch dpms on`, and `hyprctl dispatch focusmonitor` may all return `ok` without restoring the image.
## Discovery
Listening to Hyprland's event socket did not provide a reliable monitor add/remove event for the physical input switch.
The kernel DRM subsystem did, however, emit:
```text
SUBSYSTEM=drm
HOTPLUG=1
```
via:
```bash
udevadm monitor --kernel --property --subsystem-match=drm
```
A manual VT switch restored the image, indicating that the required reset happens below the compositor level.
## Final design
The long-running watcher consumes kernel DRM events. For every `HOTPLUG=1` event it:
1. checks `systemd-logind` for an active Wayland session belonging to the current user;
2. ignores the event if no such session exists;
3. dynamically determines that session's VT;
4. applies debounce protection;
5. calls the recovery script with the detected VT.
The recovery script:
1. uses `flock` to prevent overlap;
2. waits for the HDMI handshake to settle;
3. asks Hyprland to enable/focus the output;
4. uses `chvt` to move to another VT;
5. returns to the dynamically detected graphical VT;
6. refreshes Hyprland DPMS/focus once more.
## Why the Wayland-session guard is critical
GPU initialization during boot can generate DRM hotplug events. If the recovery script blindly executes a hard-coded `chvt 2` followed by `chvt 1`, the machine can end up showing tty1 even though the display manager is running on tty2.
The current implementation prevents this by requiring:
```text
Name=<current user>
Type=wayland
Active=yes
VTNr>0
```
before performing any recovery.
## Privilege boundary
The watcher and recovery script run as the normal user. Only `chvt` needs elevated privileges.
The recommended sudoers rule limits passwordless sudo to:
```text
/usr/bin/chvt
```
The script always calls it through `sudo -n`, which fails instead of prompting for a password.
The session check is not atomic with the kernel VT switch. The temporary VT can
contain another console, and the workaround can briefly expose it. Multi-seat
setups, multiple concurrent desktops for one user and hardware that emits no DRM
event are outside the supported design. See README for cooldown tradeoffs.
+44
View File
@@ -0,0 +1,44 @@
# Code-Review – 23.09.2026
Geprüft wurde der ursprüngliche Stand `73e727e` (Version 0.1.0). Die folgenden
Befunde wurden für Version 0.2.0 behoben. Es gab noch keine vorhandene Git-Historie;
der gelieferte Stand wurde deshalb vor Änderungen als eigener Commit gesichert.
| Priorität | Befund im Ausgangscode | Auswirkung | Korrektur |
| --- | --- | --- | --- |
| Hoch | `revive-monitor-fix.sh`: kein EXIT-/Signal-Trap nach dem ersten `chvt` | Abbruch oder gescheiterter Rückwechsel kann den Benutzer auf einer anderen Konsole lassen. | Rückkehr-Trap, Prüfung beider sudo-Berechtigungen, Zeitlimits und ein Notfall-Rückkehrversuch. |
| Hoch | Nur der Watcher prüft eine aktive Wayland-Sitzung, danach mehrere Sekunden Wartezeit | Bei Abmeldung oder Sitzungswechsel kann die Recovery eine inzwischen fremde VT aktivieren; direkte Helper-Aufrufe umgehen den Schutz. | Eigenständige Helper-Prüfung, UID/seat0/VT-Abgleich und erneute Prüfung unmittelbar vor dem Wechsel. |
| Hoch | systemd erhält keine Hyprland-Instanz; `hyprctl`-Fehler werden ignoriert | Der Dienst kann auf die falsche/keine Instanz zugreifen und trotzdem VTs wechseln bzw. Erfolg melden. | Dokumentierter Login-Hook, Umgebungsimport im Installer, IPC-/Monitorprüfung und propagierte Fehler. |
| Mittel | `/tmp/revive-monitor` als gemeinsamer Fallback; Fehler beim Lock-Öffnen ungeprüft | Benutzerübergreifende Kollisionen, Symlink-Ziele und unwirksame Sperren möglich. | Nur privates, eigenes Runtime-Verzeichnis mit Modus 0700; kein `/tmp`-Fallback; Lockfehler führen zum Abbruch. |
| Mittel | Watcher-Pipeline ohne `pipefail`, EOF ergibt reguläres Ende | `udevadm` kann ausfallen, ohne dass `Restart=on-failure` den Dienst neu startet. | Pipeline-Status erfassen; jedes unerwartete Stream-Ende endet mit Fehlerstatus. |
| Mittel | Watcher meldet nach jedem Helper-Aufruf Erfolg; Zeit-/Monitorwerte ungeprüft | Diagnose verschleiert echte Fehler; fehlerhafte Einstellungen können Schutz- und Wartefunktionen beeinträchtigen. | Statusabhängige Logs, begrenzte Integer-Werte, erlaubte Output-Namen. |
| Mittel | Installer startet sofort; `enable --now` startet aktive Dienste bei Updates nicht neu; uneinheitliches XDG; globales `reset-failed` im Deinstaller | Aktivierung vor Konfiguration, alter Prozess trotz neuer Dateien, falscher Unit-Pfad, Löschen fremder Fehlerzustände. | Explizites `--start`, Stop vor Dateiaustausch, Restart bei Aktivierung, konsistente XDG-Pfade, Root-Verweigerung und gezieltes Zurücksetzen. |
## Validierung
- 25 bestandene Verhaltenstests mit echten Bash-Prozessen und simulierten loginctl-, hyprctl-,
udev-, sudo- und systemd-Befehlen; keine realen VT-Wechsel.
- Abgedeckt: Erfolg, Vor-Login-Schutz, fremde Benutzer/Seats/Desktops, falsche VT,
Sitzungswechsel während der Wartezeit, IPC-/Output-Fehler, fehlende Rechte,
Rückkehrfehler und Signalbehandlung, Runtime-/Symlink-Schutz, Konfiguration,
parallele Recovery, Hotplug-Bursts, Stream-Ende und Fehlerprotokollierung.
- Installation, wiederholte Installation, Update-Aktivierung und Deinstallation
unter temporärem HOME/XDG; vorhandene Konfiguration bleibt erhalten.
- Bash-Syntax und ShellCheck 0.11.0 ohne Befunde; statische systemd-Prüfung mit auf das Checkout
umgebogenem ExecStart, ohne Installation des Dienstes auf diesem Desktop.
- Release aus committed HEAD; Archivinhalt, Prüfsummen und entpackter Installer
werden separat geprüft.
## Verbleibende Grenzen
Ein Shell-Trap hilft nicht gegen SIGKILL, Stromausfall oder dauerhafte Kernel-/sudo-
Fehler. Prüfung und VT-Wechsel sind nicht atomar. Die temporäre VT kann einen
anderen Konsoleninhalt zeigen. Unterstützt ist eine lokale Hyprland-Sitzung auf
seat0; mehrere gleichzeitige Desktops desselben Benutzers sind nicht abgedeckt.
DRM-Hotplug identifiziert nicht zuverlässig den betroffenen Monitor. Der Cooldown
kann einen echten schnellen Rückwechsel unterdrücken. Das bleibt eine bewusste
Einschränkung dieses hardwareabhängigen Workarounds und ist in README beschrieben.
Ein realer Test von HDMI-Eingangswechsel, Ab-/Anmeldung und Boot-Verhalten wurde
in diesem Review nicht ausgeführt. Er bleibt nach Installation erforderlich.
+53 -103
View File
@@ -1,139 +1,89 @@
# Troubleshooting
## Check the Hyprland output name
```bash
hyprctl monitors
```
Update:
```text
~/.config/revive-monitor/config.env
```
if the output is not `HDMI-A-2`.
## Check the user service
## Status and logs
```bash
systemctl --user status revive-monitor-drm-watch.service
journalctl --user -u revive-monitor-drm-watch.service -b -f
```
## Follow logs
Both scripts write to stderr; systemd captures their messages in the unit journal.
Direct manual runs write to the terminal. There is no separate logger/syslog tag.
```bash
journalctl --user -u revive-monitor-drm-watch.service -f
```
## Missing Hyprland instance / IPC failure
For the recovery script tag:
From a Hyprland terminal, run `./scripts/install.sh --start` and add the startup
hook from README so each new login updates `HYPRLAND_INSTANCE_SIGNATURE`.
`hyprctl monitors` must list the output configured in `config.env`. A stale
signature, failed IPC call or absent output aborts recovery without switching VTs.
```bash
journalctl --user -t revive-monitor-fix -f
```
## No recovery after switching inputs
## Verify DRM hotplug events manually
Observe events without starting another watcher:
```bash
udevadm monitor --kernel --property --subsystem-match=drm
```
Switch the physical monitor input away and back. Look for:
```text
HOTPLUG=1
```
If no DRM hotplug event is emitted, this project has no reliable event to trigger from.
## Verify the active Wayland session
Look for `HOTPLUG=1`. Then inspect the session:
```bash
loginctl list-sessions
loginctl show-session SESSION_ID -p User -p Type -p Active -p Seat -p VTNr -p Desktop
```
Then:
Expected: your numeric UID, `Type=wayland`, `Active=yes`, `Seat=seat0`, a VT from
1 to 63, and either a Hyprland desktop or an empty Desktop property. The helper
also checks these properties when invoked manually.
## Invalid runtime directory
`XDG_RUNTIME_DIR` must be your login-managed, user-owned directory with mode 0700
(normally `/run/user/UID`). Do not set it to `/tmp` or run the service with sudo.
The systemd unit sets it from `%t`.
## sudo / VT return failures
Use `visudo` to verify the explicit VT permissions. Check authorization without
switching the display (replace the VTs to match your session):
```bash
for s in $(loginctl list-sessions --no-legend | awk '{print $1}'); do
echo "=== Session $s ==="
loginctl show-session "$s" -p Name -p Type -p Active -p VTNr
done
sudo -n -l /usr/bin/chvt 2
sudo -n -l /usr/bin/chvt 1
```
A normal Hyprland session should look similar to:
A denied return permission aborts before the outward switch. Actual chvt errors
are still possible; a return failure is retried once and logged. A failed emergency
return requires manually returning to the graphical VT, e.g. Ctrl+Alt+F1 for VT1.
```text
Name=your-user
VTNr=1
Type=wayland
Active=yes
```
## Debounce hides a rapid return / repeated recoveries
## Display manager appears only after manually switching TTY
`DEBOUNCE_SECONDS` is a cooldown after each attempt, not an event queue. A genuine
rapid return can be ignored along with duplicate and self-generated events.
Reduce the interval if needed; increasing it can help noisy hardware. Setting it
to zero disables cooldown and can cause a recovery loop on some GPUs.
Check the boot journal:
## Manual recovery
From a terminal inside the active Hyprland session, pass its actual VT number:
```bash
journalctl -b --no-pager | grep -E 'revive-monitor|COMMAND=/usr/bin/chvt|chvt'
~/.local/bin/revive-monitor-fix 1
```
If `chvt` runs before login, verify that you installed the current watcher script. It must ignore hotplug events when there is no active Wayland session.
This will visibly switch VTs. If the monitor needs more handshake time, increase
`HDMI_SETTLE_SECONDS` (default 5). Manual recovery still needs valid IPC, the
configured output, a private runtime directory and sudo permission.
Expected pre-login behavior:
## Updating / uninstalling
```text
No active Wayland session - DRM hotplug ignored
```
Updates preserve `config.env`. Run `./scripts/install.sh --start` inside Hyprland
to restart with the new scripts. Plain `install.sh` leaves the service stopped.
For uninstall, the systemd user manager must be reachable. Remove the startup
hook manually as documented in README.
## sudo / chvt fails
## Hardware acceptance
Test non-interactively:
```bash
sudo -n /usr/bin/chvt 2
```
If sudo asks for a password or exits with an error, verify:
```text
/etc/sudoers.d/revive-monitor-chvt
```
Always edit sudoers files with `visudo`.
## Recovery runs multiple times
Increase:
```bash
DEBOUNCE_SECONDS=30
```
in:
```text
~/.config/revive-monitor/config.env
```
For noisy hardware, `60` may be more appropriate.
## Recovery runs but the monitor still stays black
Increase:
```bash
HDMI_SETTLE_SECONDS=5
```
to `7` or `10` and test again.
Also manually verify that a VT switch actually restores the image:
```bash
sudo chvt 2
sleep 1
sudo chvt 1
```
Adjust the numbers to match the machine's graphical VT.
After installation, check a normal input change, a rapid away/back change and a
logout/login cycle. Before Hyprland login there must be no chvt invocation from
this service. Automated tests use mocks and do not replace this hardware check.