Two commits fixing three live bugs in the always-on screen lockdown, plus a follow-up that makes the wake-alarm floor correct for any shutdown time — not just the ones the fixed schedule happened to cover.
This machine never actually shuts down at night anymore. Instead of powering off, a script called night-lockdown tears the GUI down and blanks the screen while every background server (sync jobs, media services, etc.) keeps running. Two generated scripts do the work:
night-lockdown-enter.sh — runs at the configured shutdown hour. Mutes
audio, turns RGB lighting off, masks the login prompt, stops the display manager (lightdm),
and forces the text console black.
night-lockdown-unlock.sh — runs on a morning timer. Restores audio and
RGB state, unmasks the login prompt, restarts lightdm, and — on days you've configured an
alarm — starts the morning wake-alarm routine.
Both scripts are idempotent against a single state file (LOCKED / UNLOCKED),
and the unlock side runs on a fixed staggered schedule: 05:00, 05:15, 05:30, 06:00, 07:00. The
idea was redundancy — if the 05:00 run fails for some reason, a later trigger catches it, but since the
action is idempotent, only the first one to actually run does anything.
Making the screen go black turned out to be more subtle than it sounds. This machine has exactly one
console framebuffer, nvidia-drmdrmfb, owned by the discrete NVIDIA GPU that drives both
connected monitors. The original code went to some trouble to not power the DisplayPort output
down (only FB_BLANK_NORMAL, not FB_BLANK_POWERDOWN) because powering it down had
previously dropped the DP link so hard the cable needed a physical replug:
# Force the console dark — black screen, but WITHOUT DPMS power management.
# Powering the output down ... makes DisplayPort drop its link ...
blank_console() {
local fb
for fb in /sys/class/graphics/fb*/blank; do
[[ -e "$fb" ]] || continue
echo "$CONSOLE_BLANK_MODE" >"$fb" 2>/dev/null || logg "WARN: could not blank $fb"
done
setterm --term linux --blank force --powersave off >"$CONSOLE_TTY" 2>/dev/null || ...
}
And the lockdown-entry flow called it only after the display manager had fully stopped:
# 7. Stop the GUI. This hands the VT back to fbcon, which unblanks it.
run systemctl stop lightdm.service
# 8. Now that fbcon owns the VT again, force it dark (DPMS off -> standby).
blank_console
The unlock script also re-fires a "morning routine" service (wake alarm + workout lock) on configured alarm days, but only as the very last step of the same idempotent run that restores the GUI:
maybe_start_morning_alarm() {
[[ -n "$LOCK_USER" && -n "$LOCK_UID" && -n "$MORNING_ROUTINE_UNIT" ]] || return 0
... # alarm-day check only
logg "alarm day (dow=$dow) — starting $MORNING_ROUTINE_UNIT"
run sudo -u "$LOCK_USER" ... systemctl --user start --no-block "$MORNING_ROUTINE_UNIT"
}
Nothing in this function knew or cared what time lockdown had actually started the night before.
The comment in the original code says stopping lightdm "hands the VT back to fbcon, which
unblanks it." That's the key fact: switching a virtual console active is not a passive handoff — the kernel
redraws it, showing whatever was already in that console's text buffer (old kernel/systemd log lines).
systemctl stop lightdm.service is one blocking call that can take a real one-to-a-few seconds
to fully finish (session cleanup, PAM, etc.), and the VT switch can happen at any point during that
call — not only at the very end. Blanking only after the whole call returns leaves that window
completely exposed:
The fix keeps a background loop calling blank_console every 100 ms for the whole
duration of the stop, so whichever moment the VT switch actually happens, something is re-darkening it within
a tenth of a second:
There was simply no step anywhere in the unlock flow that touched display configuration. If the
DisplayPort link didn't cleanly re-negotiate the instant lightdm restarted, nothing ever asked
it to. The fix adds a short retry loop calling autorandr --change — using a profile you save
once, in advance, while both monitors are connected correctly — right after lightdm restarts.
Lockdown entered at 23:00. The unlock schedule's first trigger, 05:00, ran the whole idempotent
unlock-and-alarm sequence in one go — 6 hours later, not the intended 8. The first fix added a floor: record
the real lockdown-entry timestamp, and don't start the alarm until entry + 8h — but the alarm
check still only ever got re-evaluated by whichever of the five fixed triggers happened to run
next.
That's fine as long as the floor lands inside 05:00–07:00. But the shutdown hour itself is
a config value (/etc/shutdown-schedule.conf) that already changes dynamically — a "sick day"
feature had pushed it to 23:00 the very night this bug was reported. Push it just a little later, say
23:30, and the floor lands at 07:30 — past every remaining trigger. Nothing left in the fixed
schedule ever checks again, so the alarm silently never fires that morning at all:
Toy example — lockdown at 23:30, floor = entry + 8h = 07:30:
Every fixed trigger says "too soon" and nothing runs again after 07:00 — unless something is
scheduled specifically for 07:30. The follow-up fix computes that exact moment when lockdown begins and
schedules a one-shot systemd-run --on-calendar timer for it, so the alarm gets a guaranteed,
correctly-timed chance no matter what the shutdown hour was that night. Play with the exact same rule
yourself below.
lightdm or calls autorandr, so it can prove the alarm-floor arithmetic works but
it structurally cannot prove bugs 1 or 2 are fixed. Both scripts now print an explicit
DRY_RUN NOTE saying exactly that, so a clean dry run is never mistaken for a real one.
Grouped by concern, not by file order.
if [[ "$DRY_RUN" != "1" ]]; then
(
while :; do
blank_console
sleep 0.1
done
) &
BLANK_POLLER_PID=$!
fi
run systemctl stop lightdm.service
if [[ -n "${BLANK_POLLER_PID:-}" ]]; then
kill "$BLANK_POLLER_PID" 2>/dev/null || true
wait "$BLANK_POLLER_PID" 2>/dev/null || true
unset BLANK_POLLER_PID
fi
# Final guaranteed blank now that the GUI is fully down.
blank_console
A background subshell keeps re-blanking every 100 ms while the (blocking, possibly slow)
systemctl stop lightdm.service runs, then gets killed and a final blank runs once more for
good measure. Cheap and idempotent — blank_console was already safe to call repeatedly.
# 2. Start the GUI — lightdm autologins back into i3/dwm.
run systemctl start lightdm.service
# 2a. Reconnect/reconfigure displays ...
if [[ -n "$LOCK_USER" && -n "$LOCK_UID" ]] && command -v autorandr >/dev/null 2>&1; then
autorandr_ok=false
for ((i = 0; i < 15; i++)); do
if sudo -u "$LOCK_USER" env "XDG_RUNTIME_DIR=/run/user/$LOCK_UID" DISPLAY=:0 \
autorandr --change --default "$AUTORANDR_PROFILE" >/dev/null 2>&1; then
autorandr_ok=true; break
fi
sleep 1
done
...
fi
Runs as the real desktop user (not root), retries for up to 15 seconds to give X time to re-enumerate
the link, and falls back to the saved default profile if nothing matches. Requires a one-time,
manual autorandr --save default while both monitors are connected correctly.
today="$(date +%F)"
if [[ "$(cat "$STATE_DIR/alarm_started_date" 2>/dev/null || echo '')" == "$today" ]]; then
logg "morning alarm already started today — skipping"
return 0
fi
locked_at="$(cat "$STATE_DIR/locked_at_epoch" 2>/dev/null || echo 0)"
now="$(date +%s)"
min_wake=$((locked_at + MIN_WAKE_AFTER_LOCKDOWN_HOURS * 3600))
if ((locked_at > 0 && now < min_wake)); then
logg "too soon since lockdown entry (...m remaining) — skipping alarm this trigger"
return 0
fi
...
echo "$today" >"$STATE_DIR/alarm_started_date"
The dedup marker matters because the alarm check is no longer gated by the same LOCKED/UNLOCKED flag that gates the GUI restore — it now runs from both branches of the unlock script, including the already-unlocked early exit:
if [[ "$(cat "$STATE_FILE" ...) " == "UNLOCKED" ]]; then
maybe_start_morning_alarm # still gets a chance, even though restore already happened
logg "already UNLOCKED — nothing else to do"
exit 0
fi
if [[ "$DRY_RUN" != "1" ]]; then
echo LOCKED >"$STATE_FILE"
lockdown_epoch="$(date +%s)"
echo "$lockdown_epoch" >"$STATE_DIR/locked_at_epoch"
floor_epoch=$((lockdown_epoch + MIN_WAKE_AFTER_LOCKDOWN_HOURS * 3600))
floor_cal="$(date -d "@$floor_epoch" '+%Y-%m-%d %H:%M:%S')"
run systemd-run --unit=night-lockdown-wake-floor --collect \
--on-calendar="$floor_cal" /usr/local/bin/night-lockdown-unlock.sh
fi
At the exact moment lockdown begins, this schedules a one-off, self-cleaning
(--collect) systemd timer for precisely entry + 8h, whatever that turns out to
be, and simply re-runs the unlock script when it fires. Because the unlock script is already idempotent and
the alarm has its own dedup, re-running it is always safe — it either restores a GUI that's already up (no-op)
and/or fires the alarm exactly once if it hasn't already.
Move the slider to set the lockdown-entry time, then step through the same schedule the real script
checks: five fixed triggers, plus the one dynamic trigger scheduled precisely at entry + floor
hours.
Score: 0 / 5