Vendor the command set the menu and shell depend on
This commit is contained in:
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user