Harden HDMI recovery and add tested user installer release tooling
This commit is contained in:
1 parent
73e727ef4c
commit
d963224cb7
17 files changed
+903
-646
No files matched your search
+26
-76
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
Reference in new issue
Block a user