125 lines
4.6 KiB
Bash
Executable File
125 lines
4.6 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# blob:summary=Lock before suspend and wait for the session lock to become secure
|
|
# blob:group=system
|
|
# blob:hidden=true
|
|
|
|
# Overrunning the budget is the failure this whole path exists to prevent:
|
|
# logind stops honouring the inhibitor and suspends mid-lock. Every call below
|
|
# is bounded by what is left of the budget, so the deadline enforces itself
|
|
# rather than depending on an estimate of how long a step ought to take.
|
|
budget_cap_ms=12000
|
|
lock_timeout_ms=1000
|
|
status_timeout_ms=500
|
|
poll_interval=0.1
|
|
|
|
# logind decides how long a delay inhibitor may hold the machine, and the
|
|
# shipped drop-in only counts once logind has reloaded it, so ask rather than
|
|
# assume. Leaving logind a fifth of its own window to deliver PrepareForSleep
|
|
# and act on the release gives 4s at the 5s default and 12s at the shipped 15s.
|
|
# The cap keeps a hand-raised window from stranding a closed laptop in a bag.
|
|
derive_budget_ms() {
|
|
local window
|
|
window=$(timeout --kill-after=0.1s 1s busctl get-property \
|
|
org.freedesktop.login1 /org/freedesktop/login1 \
|
|
org.freedesktop.login1.Manager InhibitDelayMaxUSec 2>/dev/null)
|
|
window=${window##* }
|
|
|
|
# An unreadable window means we cannot know, so assume logind's own default.
|
|
[[ $window =~ ^[0-9]+$ ]] && (( window > 0 )) || window=5000000
|
|
window=$((window / 1000))
|
|
|
|
# Never leave logind less than a second, however small its window is.
|
|
window=$((window - (window / 5 > 1000 ? window / 5 : 1000)))
|
|
|
|
(( window < budget_cap_ms )) && echo "$window" || echo "$budget_cap_ms"
|
|
}
|
|
|
|
budget_ms=${1:-$(derive_budget_ms)}
|
|
if [[ ! $budget_ms =~ ^[0-9]+$ ]] || (( budget_ms < 1 || budget_ms > budget_cap_ms )); then
|
|
budget_ms=$(derive_budget_ms)
|
|
fi
|
|
|
|
# EPOCHREALTIME renders with the locale's decimal separator, so drop every
|
|
# non-digit rather than assuming a period. A comma would otherwise read as
|
|
# bash's comma operator and silently void the deadline.
|
|
deadline_ms=$((10#${EPOCHREALTIME//[!0-9]/} / 1000 + budget_ms))
|
|
|
|
remaining_ms() {
|
|
echo $((deadline_ms - 10#${EPOCHREALTIME//[!0-9]/} / 1000))
|
|
}
|
|
|
|
# Clamping to what is left as well as to the call's own limit is what lets the
|
|
# loop below stay a plain "while there is time" without predicting step costs.
|
|
lock_ipc() {
|
|
local limit=$1 remaining seconds
|
|
shift
|
|
|
|
remaining=$(remaining_ms)
|
|
(( remaining > 0 )) || return 1
|
|
(( limit < remaining )) || limit=$remaining
|
|
printf -v seconds '%d.%03d' $((limit / 1000)) $((limit % 1000))
|
|
|
|
BLOB_SHELL_IPC_TIMEOUT="$seconds" \
|
|
timeout --kill-after=0.1s "$seconds" blob-shell "$@"
|
|
}
|
|
|
|
# The shell answers refusals on stdout with a zero exit, so the reply is the
|
|
# only way to spot a lock it can never perform. Nothing else needs inspecting:
|
|
# the status poll is what confirms success, and re-requesting is idempotent, so
|
|
# a request that may not have landed costs nothing to repeat.
|
|
request_lock() {
|
|
case $(lock_ipc "$lock_timeout_ms" lock lock 2>/dev/null) in
|
|
missing-pam) report_unsecured "no lock screen is configured" ;;
|
|
esac
|
|
}
|
|
|
|
# secure: done. locking: the shell has the request and is working on it, so
|
|
# leave it alone. Anything else, unreadable replies included, means ask again.
|
|
lock_state() {
|
|
jq -r 'if .secure == true then "secure"
|
|
elif .requested == true then "locking"
|
|
else "idle" end' \
|
|
<<<"$(lock_ipc "$status_timeout_ms" lock status 2>/dev/null)" 2>/dev/null
|
|
}
|
|
|
|
sync_clamshell() {
|
|
# Lid transitions can temporarily stall Hyprland IPC. This is best-effort:
|
|
# the lid binding and monitor watcher also reconcile clamshell state.
|
|
timeout --kill-after=0.1s 0.4s \
|
|
blob-hypr-monitor-clamshell >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# logind suspends whether or not this wait succeeded, so a failure here means
|
|
# the machine slept with the session exposed. The notification is the only way
|
|
# anyone finds out, and it lands on the screen they unlock into.
|
|
report_unsecured() {
|
|
printf 'blob-system-sleep: suspending without a secure lock (%s)\n' \
|
|
"$1" >&2
|
|
|
|
blob-notify-send -u critical -g \
|
|
"Screen did not lock before suspend" \
|
|
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
|
|
|
|
exit 1
|
|
}
|
|
|
|
# Request the lock before touching monitor state, so a stuck Hyprland IPC call
|
|
# cannot consume the window before Quickshell has begun securing the session.
|
|
request_lock
|
|
sync_clamshell
|
|
|
|
# The trailing sleep can overshoot the deadline by one interval, which is well
|
|
# inside the reserve derive_budget_ms already held back for logind.
|
|
while (( $(remaining_ms) > 0 )); do
|
|
case $(lock_state) in
|
|
secure) exit 0 ;;
|
|
locking) ;;
|
|
*) request_lock ;;
|
|
esac
|
|
|
|
sleep "$poll_interval"
|
|
done
|
|
|
|
report_unsecured "the shell did not secure the session within ${budget_ms}ms"
|