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

+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Build only committed sources; generated artifacts never include local secrets.
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
if ! git diff --quiet || ! git diff --cached --quiet; then
printf 'Commit tracked changes before building a release.\n' >&2
exit 1
fi
VERSION="$(git show HEAD:VERSION)"
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { printf 'Invalid VERSION\n' >&2; exit 1; }
NAME="hyprland-hdmi-revive-$VERSION"
mkdir -p dist
git archive --format=tar --prefix="$NAME/" HEAD | gzip -n > "dist/$NAME.tar.gz"
git archive --format=zip --prefix="$NAME/" HEAD > "dist/$NAME.zip"
base64 -w 64 "dist/$NAME.zip" > "dist/$NAME.zip.b64"
(
cd dist
sha256sum "$NAME.tar.gz" "$NAME.zip" "$NAME.zip.b64" > SHA256SUMS
sha256sum -c SHA256SUMS
)
printf 'Built %s from commit %s\n' "$NAME" "$(git rev-parse HEAD)"
+54 -17
View File
@@ -1,35 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
START_SERVICE=0
case "${1:-}" in
'') ;;
--start) START_SERVICE=1 ;;
--help) printf 'Usage: %s [--start]\nInstall for the current user; --start also enables and restarts the service.\n' "$0"; exit 0 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 2 ;;
esac
(( $# <= 1 )) || { printf 'Too many arguments\n' >&2; exit 2; }
if (( EUID == 0 )); then
printf 'Run this installer as the desktop user, without sudo.\n' >&2
exit 1
fi
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
BIN_DIR="$HOME/.local/bin"
SYSTEMD_DIR="$HOME/.config/systemd/user"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor"
mkdir -p "$BIN_DIR" "$SYSTEMD_DIR" "$CONFIG_DIR"
LIB_DIR="$HOME/.local/lib/hyprland-hdmi-revive"
CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
[[ "$CONFIG_HOME" == /* ]] || { printf 'XDG_CONFIG_HOME must be absolute\n' >&2; exit 1; }
SYSTEMD_DIR="$CONFIG_HOME/systemd/user"
CONFIG_DIR="$CONFIG_HOME/revive-monitor"
LOGTAG=revive-monitor-install
# shellcheck source=lib/common.sh
source "$REPO_ROOT/lib/common.sh"
require_commands systemctl udevadm stdbuf timeout loginctl hyprctl jq flock sudo stat install || exit 1
[[ -x /usr/bin/chvt ]] || { printf 'Missing /usr/bin/chvt (Arch package: kbd)\n' >&2; exit 1; }
# Validate existing config before overwriting any installed files.
load_config || exit 1
if (( START_SERVICE )); then
[[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]] || {
printf 'Use --start from a terminal inside Hyprland.\n' >&2; exit 1;
}
fi
# Avoid replacing Bash files while a running watcher/recovery is reading them.
if systemctl --user is-active --quiet revive-monitor-drm-watch.service; then
systemctl --user stop revive-monitor-drm-watch.service
fi
mkdir -p -- "$BIN_DIR" "$LIB_DIR" "$SYSTEMD_DIR" "$CONFIG_DIR"
install -m 0755 "$REPO_ROOT/scripts/revive-monitor-fix.sh" "$BIN_DIR/revive-monitor-fix"
install -m 0755 "$REPO_ROOT/scripts/revive-monitor-drm-watch.sh" "$BIN_DIR/revive-monitor-drm-watch"
install -m 0644 "$REPO_ROOT/lib/common.sh" "$LIB_DIR/common.sh"
install -m 0644 "$REPO_ROOT/systemd/user/revive-monitor-drm-watch.service" "$SYSTEMD_DIR/revive-monitor-drm-watch.service"
if [[ ! -e "$CONFIG_DIR/config.env" ]]; then
install -m 0644 "$REPO_ROOT/config/revive-monitor.env.example" "$CONFIG_DIR/config.env"
install -m 0600 "$REPO_ROOT/config/revive-monitor.env.example" "$CONFIG_DIR/config.env"
printf 'Created %s\n' "$CONFIG_DIR/config.env"
printf 'Edit MONITOR before using the service if your output is not HDMI-A-2.\n'
else
printf 'Keeping existing config: %s\n' "$CONFIG_DIR/config.env"
fi
systemctl --user daemon-reload
systemctl --user enable --now revive-monitor-drm-watch.service
if (( START_SERVICE )); then
systemctl --user import-environment HYPRLAND_INSTANCE_SIGNATURE
# Keep the service and installer on the same configuration root.
export XDG_CONFIG_HOME="$CONFIG_HOME"
systemctl --user import-environment XDG_CONFIG_HOME
systemctl --user daemon-reload
systemctl --user enable revive-monitor-drm-watch.service
systemctl --user restart revive-monitor-drm-watch.service
systemctl --user is-active --quiet revive-monitor-drm-watch.service
else
printf 'Files installed. Activation is explicit; follow README.md before starting.\n'
fi
cat <<'MSG'
Installed successfully.
Next steps:
1. Verify ~/.config/revive-monitor/config.env
2. Configure /etc/sudoers.d/revive-monitor-chvt using one of the templates in sudoers/
3. Test with: systemctl --user status revive-monitor-drm-watch.service
4. Watch logs: journalctl --user -u revive-monitor-drm-watch.service -f
1. Set MONITOR in ${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env
2. Configure chvt permission with visudo using the sudoers/ templates.
3. Add the Hyprland environment/start hook documented in README.md.
4. Run ./scripts/install.sh --start from Hyprland to activate or update.
5. Check: journalctl --user -u revive-monitor-drm-watch.service -b
MSG
+50 -101
View File
@@ -1,115 +1,64 @@
#!/usr/bin/env bash
set -u
set -uo pipefail
LOGTAG="revive-monitor-drm-watch"
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env"
FIX_SCRIPT="${REVIVE_MONITOR_FIX_SCRIPT:-$HOME/.local/bin/revive-monitor-fix}"
# Defaults. Override in ~/.config/revive-monitor/config.env
DEBOUNCE_SECONDS="30"
if [[ -r "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE"
LOGTAG=revive-monitor-drm-watch
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
COMMON="$SCRIPT_DIR/../lib/hyprland-hdmi-revive/common.sh"
[[ -f "$COMMON" ]] || COMMON="$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=lib/common.sh
source "$COMMON" || exit 1
load_config || exit 1
require_commands udevadm stdbuf timeout loginctl flock stat || exit 1
prepare_runtime || exit 1
if [[ -n "${REVIVE_MONITOR_FIX_SCRIPT:-}" ]]; then
FIX_SCRIPT="$REVIVE_MONITOR_FIX_SCRIPT"
else
FIX_SCRIPT="$SCRIPT_DIR/revive-monitor-fix"
[[ -e "$FIX_SCRIPT" ]] || FIX_SCRIPT="$SCRIPT_DIR/revive-monitor-fix.sh"
fi
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/revive-monitor"
LAST_RUN_FILE="$STATE_DIR/last-drm-hotplug-run"
mkdir -p "$STATE_DIR"
log() {
logger -t "$LOGTAG" -- "$*"
}
get_active_wayland_session() {
local sid
local name
local type
local active
local vt
while read -r sid; do
[[ -n "$sid" ]] || continue
name="$(loginctl show-session "$sid" -p Name --value 2>/dev/null)"
[[ "$name" == "$USER" ]] || continue
type="$(loginctl show-session "$sid" -p Type --value 2>/dev/null)"
[[ "$type" == "wayland" ]] || continue
active="$(loginctl show-session "$sid" -p Active --value 2>/dev/null)"
[[ "$active" == "yes" ]] || continue
vt="$(loginctl show-session "$sid" -p VTNr --value 2>/dev/null)"
if [[ "$vt" =~ ^[1-9][0-9]*$ ]]; then
printf '%s:%s\n' "$sid" "$vt"
return 0
fi
done < <(
loginctl list-sessions --no-legend --no-pager 2>/dev/null | awk '{print $1}'
)
return 1
}
run_recovery() {
local session_info
local session_id
local graphical_vt
local now
local last="0"
local diff
if ! session_info="$(get_active_wayland_session)"; then
log "No active Wayland session - DRM hotplug ignored"
return 0
fi
session_id="${session_info%%:*}"
graphical_vt="${session_info##*:}"
now="$(date +%s)"
if [[ -r "$LAST_RUN_FILE" ]]; then
last="$(cat "$LAST_RUN_FILE" 2>/dev/null || printf '0')"
fi
[[ "$last" =~ ^[0-9]+$ ]] || last="0"
diff=$((now - last))
if (( diff < DEBOUNCE_SECONDS )); then
log "DRM hotplug ignored because of debounce (${diff}s)"
return 0
fi
printf '%s\n' "$now" > "$LAST_RUN_FILE"
log "DRM hotplug detected - Wayland session $session_id on VT $graphical_vt"
"$FIX_SCRIPT" "$graphical_vt"
log "Recovery after DRM hotplug finished"
}
if ! command -v udevadm >/dev/null 2>&1; then
log "udevadm not found"
exit 1
fi
if [[ ! -x "$FIX_SCRIPT" ]]; then
log "Recovery script is not executable: $FIX_SCRIPT"
exit 1
fi
exec 8>"$STATE_DIR/watcher.lock" || exit 1
if ! flock -n 8; then
log 'Another watcher is running or the watcher lock is unavailable'
exit 1
fi
log "Listening for DRM hotplug events"
run_recovery() {
local session_info now status
session_info="$(get_active_wayland_session)" || {
log 'No active Wayland session - DRM hotplug ignored'
return 0
}
now=$SECONDS
if (( last_run >= 0 && now - last_run < DEBOUNCE_SECONDS )); then
log 'DRM hotplug ignored because of debounce'
return 0
fi
log "DRM hotplug detected - Wayland session ${session_info%%:*} on VT ${session_info##*:}"
"$FIX_SCRIPT" "${session_info##*:}"
status=$?
# Measure from completion, so events queued during our own VT refresh are suppressed.
last_run=$SECONDS
if (( status != 0 )); then
log "Recovery after DRM hotplug failed (exit $status)"
else
log 'Recovery helper finished; see its log for completion or skip reason'
fi
}
log 'Listening for DRM hotplug events'
# pipefail propagates a failed producer. EOF (even exit 0) is unexpected for
# this long-running service and must also trigger Restart=on-failure.
stdbuf -oL udevadm monitor --kernel --property --subsystem-match=drm |
while IFS= read -r line; do
last_run=${last_run:--1}
case "$line" in
HOTPLUG=1)
run_recovery
;;
HOTPLUG=1) run_recovery ;;
esac
done
status=$?
log "DRM event stream ended (exit $status) - requesting service restart"
exit 1
+96 -55
View File
@@ -1,77 +1,118 @@
#!/usr/bin/env bash
set -u
set -uo pipefail
LOGTAG="revive-monitor-fix"
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env"
# Defaults. Override in ~/.config/revive-monitor/config.env
MONITOR="HDMI-A-2"
HDMI_SETTLE_SECONDS="5"
VT_SWITCH_DELAY_SECONDS="1"
VT_RETURN_SETTLE_SECONDS="2"
if [[ -r "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE"
fi
LOGTAG=revive-monitor-fix
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# Source-tree and installed layouts.
COMMON="$SCRIPT_DIR/../lib/hyprland-hdmi-revive/common.sh"
[[ -f "$COMMON" ]] || COMMON="$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=lib/common.sh
source "$COMMON" || exit 1
load_config || exit 1
require_commands timeout loginctl hyprctl jq sudo flock stat sleep || exit 1
prepare_runtime || exit 1
GRAPHICAL_VT="${1:-}"
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/revive-monitor"
LOCK_FILE="$STATE_DIR/fix.lock"
if [[ ! "$GRAPHICAL_VT" =~ ^[1-9][0-9]?$ ]] || (( GRAPHICAL_VT > 63 )); then
log 'No valid graphical VT supplied (expected 1..63)'
exit 1
fi
mkdir -p "$STATE_DIR"
exec 9>"$STATE_DIR/fix.lock" || exit 1
flock -n -E 75 9
lock_status=$?
if (( lock_status == 75 )); then
log 'Recovery already running - skipping'
exit 0
elif (( lock_status != 0 )); then
log 'Cannot acquire recovery lock'
exit 1
fi
log() {
logger -t "$LOGTAG" -- "$*"
SESSION="$(get_active_wayland_session)" || { log 'No active Wayland session - recovery skipped'; exit 0; }
if [[ "${SESSION##*:}" != "$GRAPHICAL_VT" ]]; then
log 'Requested VT does not belong to the active Wayland session'
exit 1
fi
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
log 'Missing HYPRLAND_INSTANCE_SIGNATURE; import the Hyprland environment into systemd --user'
exit 1
fi
monitor_present() {
timeout 5 hyprctl -j monitors |
jq -e --arg name "$MONITOR" 'any(.[]; .name == $name)' >/dev/null 2>&1
}
run_quiet() {
"$@" >/dev/null 2>&1
refresh_monitor() {
timeout 5 hyprctl dispatch dpms on "$MONITOR" >/dev/null 2>&1 &&
timeout 5 hyprctl dispatch focusmonitor "$MONITOR" >/dev/null 2>&1
}
if ! [[ "$GRAPHICAL_VT" =~ ^[1-9][0-9]*$ ]]; then
log "No valid graphical VT supplied - recovery aborted"
exit 0
fi
if [[ "$GRAPHICAL_VT" -eq 1 ]]; then
TEMP_VT=2
else
TEMP_VT=1
fi
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "Recovery already running - skipping"
exit 0
fi
log "Recovery started for $MONITOR on VT $GRAPHICAL_VT"
sleep "$HDMI_SETTLE_SECONDS" || exit 1
if ! monitor_present; then
log "Hyprland IPC unavailable or configured output $MONITOR absent - recovery aborted"
exit 1
fi
if ! refresh_monitor; then
log 'Hyprland refresh failed - recovery aborted'
exit 1
fi
sleep "$HDMI_SETTLE_SECONDS"
# Recheck after settling/IPC, immediately before the privileged switch.
if [[ "$(get_active_wayland_session)" != "$SESSION" ]]; then
log 'Active session changed while settling - recovery skipped'
exit 0
fi
TEMP_VT=1
[[ "$GRAPHICAL_VT" == 1 ]] && TEMP_VT=2
# Check both directions before leaving the desktop (actual sudo can still fail).
for vt in "$TEMP_VT" "$GRAPHICAL_VT"; do
if ! timeout 5 sudo -n -l /usr/bin/chvt "$vt" >/dev/null 2>&1; then
log "sudo permission check failed for VT $vt - recovery aborted"
exit 1
fi
done
run_quiet hyprctl dispatch dpms on "$MONITOR"
run_quiet hyprctl dispatch focusmonitor "$MONITOR"
sleep 1
if [[ "$(get_active_wayland_session)" != "$SESSION" ]]; then
log 'Active session changed during permission checks - recovery skipped'
exit 0
fi
NEEDS_RETURN=0
cleanup() {
local status=$?
trap - EXIT HUP INT TERM
if (( NEEDS_RETURN )); then
log "Attempting emergency return to graphical VT $GRAPHICAL_VT"
if ! timeout 5 sudo -n /usr/bin/chvt "$GRAPHICAL_VT"; then
log "Emergency return failed; manually switch to VT $GRAPHICAL_VT"
status=1
fi
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
log "VT refresh: $TEMP_VT -> $GRAPHICAL_VT"
if ! sudo -n /usr/bin/chvt "$TEMP_VT"; then
# Set before chvt to cover a signal arriving as the command completes.
NEEDS_RETURN=1
if ! timeout 5 sudo -n /usr/bin/chvt "$TEMP_VT"; then
log "Failed to switch to temporary VT $TEMP_VT"
exit 1
fi
sleep "$VT_SWITCH_DELAY_SECONDS"
if ! sudo -n /usr/bin/chvt "$GRAPHICAL_VT"; then
sleep "$VT_SWITCH_DELAY_SECONDS" || exit 1
if ! timeout 5 sudo -n /usr/bin/chvt "$GRAPHICAL_VT"; then
log "Failed to return to graphical VT $GRAPHICAL_VT"
exit 1
fi
sleep "$VT_RETURN_SETTLE_SECONDS"
run_quiet hyprctl dispatch dpms on "$MONITOR"
run_quiet hyprctl dispatch focusmonitor "$MONITOR"
NEEDS_RETURN=0
sleep "$VT_RETURN_SETTLE_SECONDS" || exit 1
if ! refresh_monitor; then
log 'Final Hyprland refresh failed'
exit 1
fi
log "Recovery completed for $MONITOR"
+19 -15
View File
@@ -1,24 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
if (( EUID == 0 )); then
printf 'Run as the desktop user, without sudo.\n' >&2
exit 1
fi
BIN_DIR="$HOME/.local/bin"
SYSTEMD_DIR="$HOME/.config/systemd/user"
systemctl --user disable --now revive-monitor-drm-watch.service 2>/dev/null || true
rm -f "$SYSTEMD_DIR/revive-monitor-drm-watch.service"
rm -f "$BIN_DIR/revive-monitor-fix"
rm -f "$BIN_DIR/revive-monitor-drm-watch"
SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
LIB_DIR="$HOME/.local/lib/hyprland-hdmi-revive"
SERVICE=revive-monitor-drm-watch.service
[[ "$SYSTEMD_DIR" == /* ]] || { printf 'XDG_CONFIG_HOME must be absolute\n' >&2; exit 1; }
systemctl --user show-environment >/dev/null
# Stop must succeed before removing a running service's files.
if systemctl --user cat "$SERVICE" >/dev/null 2>&1; then
systemctl --user disable --now "$SERVICE"
fi
rm -f -- "$SYSTEMD_DIR/$SERVICE" "$BIN_DIR/revive-monitor-fix" "$BIN_DIR/revive-monitor-drm-watch" "$LIB_DIR/common.sh"
rmdir -- "$LIB_DIR" 2>/dev/null || true
systemctl --user daemon-reload
systemctl --user reset-failed
# Never reset failure information for unrelated user services.
systemctl --user reset-failed "$SERVICE" 2>/dev/null || true
cat <<'MSG'
Removed the user service and installed scripts.
Not removed automatically:
~/.config/revive-monitor/config.env
/etc/sudoers.d/revive-monitor-chvt
Remove those manually if you no longer need them.
Preserved config.env and /etc/sudoers.d/revive-monitor-chvt.
Remove the Hyprland startup hook and sudoers entry manually if no longer needed.
MSG