127 lines
5.8 KiB
Bash
Executable File
127 lines
5.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# FIDO key presence detection daemon.
|
|
# Checks whether a FIDO2/U2F security key is plugged in, at an adaptive
|
|
# interval (20s under light load, 120s under moderate load, 600s under heavy
|
|
# load), and shares caffeine's systemd-inhibit idle lock while a key is
|
|
# present, so hypridle never fires during an active session.
|
|
#
|
|
# Detection: `fido2-token -L` (libfido2) enumerates connected FIDO CTAP
|
|
# devices by USB HID usage page — no touch/tap required, so this is a pure
|
|
# presence check, not an authentication. Non-empty output = a key is present.
|
|
#
|
|
# This replaced an earlier webcam motion-detection design: simpler, no camera
|
|
# contention with video calls/howdy, and no OpenCV dependency.
|
|
|
|
# Shared with caffeine.sh: both the manual toggle and this daemon drive the
|
|
# same systemd-inhibit lock, so caffeine-status.sh reflects either source.
|
|
PID_FILE="/tmp/caffeine-inhibit.pid"
|
|
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
|
# the lock, so a "key removed" tick never releases a manually-started session.
|
|
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
|
# Records whether a FIDO key is present on the last tick. presence-status.sh
|
|
# reads this so the Eww widget can show presence as a distinct signal from
|
|
# the idle lock. During a MANUAL caffeine session the daemon still checks (the
|
|
# check is cheap and doesn't touch any device other apps might want), so this
|
|
# stays live even then — only the inhibit ownership differs.
|
|
PRESENCE_FLAG="/tmp/presence-detected"
|
|
|
|
INTERVAL_LIGHT=20 # seconds between checks when CPU-or-RAM usage < LOAD_LIGHT
|
|
INTERVAL_MID=120 # seconds between checks when usage is between the two thresholds
|
|
INTERVAL_HEAVY=600 # seconds between checks when usage >= LOAD_HEAVY
|
|
LOAD_LIGHT=0.20 # CPU-or-RAM fraction below which we poll at INTERVAL_LIGHT
|
|
LOAD_HEAVY=0.50 # CPU-or-RAM fraction at/above which we back off to INTERVAL_HEAVY
|
|
NPROC="$(nproc)"
|
|
|
|
# True if any FIDO2/U2F authenticator is currently enumerable over USB.
|
|
# fido2-token -L lists one line per connected device and needs no PIN, touch,
|
|
# or tap — it's a pure USB HID enumeration, so this never prompts the key.
|
|
_fido_key_present() {
|
|
[[ -n "$(fido2-token -L 2>/dev/null)" ]]
|
|
}
|
|
|
|
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
|
# core-count-normalized 1-minute load average and the used-RAM fraction. Load
|
|
# average is a cheap, sampling-free proxy for "CPU busy" — no extra measurement
|
|
# delay per tick.
|
|
_resource_usage() {
|
|
local load1 cpu_frac mem_frac
|
|
load1="$(awk '{print $1}' /proc/loadavg)"
|
|
cpu_frac="$(awk -v l="$load1" -v n="$NPROC" 'BEGIN{print l/n}')"
|
|
mem_frac="$(free | awk '/^Mem:/ {print $3/$2}')"
|
|
awk -v c="$cpu_frac" -v m="$mem_frac" 'BEGIN{print (c>m)?c:m}'
|
|
}
|
|
|
|
# Three-tier poll interval based on current resource usage:
|
|
# >= LOAD_HEAVY -> INTERVAL_HEAVY (heavy load: back off, stay out of the way)
|
|
# < LOAD_LIGHT -> INTERVAL_LIGHT (near-idle: poll fast, cheap and responsive)
|
|
# otherwise -> INTERVAL_MID (moderate load)
|
|
_next_interval() {
|
|
local u; u="$(_resource_usage)"
|
|
awk -v u="$u" -v heavy="$LOAD_HEAVY" -v light="$LOAD_LIGHT" \
|
|
-v fh="$INTERVAL_HEAVY" -v fl="$INTERVAL_LIGHT" -v fm="$INTERVAL_MID" \
|
|
'BEGIN{ if (u >= heavy) print fh; else if (u < light) print fl; else print fm }'
|
|
}
|
|
|
|
# Returns true if the inhibitor sentinel process is still alive.
|
|
_inhibit_running() {
|
|
[[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
|
|
}
|
|
|
|
_start_inhibit() {
|
|
# Guard: don't start a second inhibitor if one is already active — whether
|
|
# it's ours from a previous tick or a manually-started caffeine session.
|
|
_inhibit_running && return
|
|
# --what=idle: target the logind idle-inhibit lock that hypridle polls.
|
|
# "sleep infinity" is the sentinel; its PID is saved so we can kill it later.
|
|
systemd-inhibit --what=idle --who="presence-detect" \
|
|
--why="FIDO key present" --mode=block \
|
|
sleep infinity &
|
|
echo $! > "$PID_FILE"
|
|
touch "$OWNED_FLAG"
|
|
# logger writes to the system journal — visible via `journalctl -t presence-detect`.
|
|
logger -t presence-detect "FIDO key present — idle inhibited"
|
|
}
|
|
|
|
_stop_inhibit() {
|
|
_inhibit_running || return
|
|
# Never release a lock we didn't start — that would be a manual caffeine
|
|
# session, which must persist regardless of presence.
|
|
[[ -f "$OWNED_FLAG" ]] || return
|
|
# Killing the sleep process releases the systemd-inhibit lock automatically.
|
|
kill "$(cat "$PID_FILE")" 2>/dev/null
|
|
rm -f "$PID_FILE" "$OWNED_FLAG"
|
|
logger -t presence-detect "FIDO key removed — idle inhibit released"
|
|
}
|
|
|
|
_cleanup() {
|
|
# On daemon stop (systemd unit stop, user logout, etc.), release the idle
|
|
# lock (only if we're the one holding it) and clear the presence flag —
|
|
# with the daemon gone, nothing is watching.
|
|
_stop_inhibit
|
|
rm -f "$PRESENCE_FLAG"
|
|
exit 0
|
|
}
|
|
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
|
trap _cleanup SIGTERM SIGINT SIGHUP
|
|
|
|
while true; do
|
|
# When idle is inhibited MANUALLY (a caffeine toggle, not this daemon), the
|
|
# lock is already held regardless of presence. Detected the same way
|
|
# caffeine-manual-status.sh does it: the shared lock is alive but we don't
|
|
# own it. We still update PRESENCE_FLAG below since checking is cheap and
|
|
# doesn't touch a device other apps care about — only inhibit ownership
|
|
# differs during a manual session.
|
|
if _fido_key_present; then
|
|
touch "$PRESENCE_FLAG"
|
|
# No-op if a manual caffeine session already holds the lock — only
|
|
# _stop_inhibit needs the ownership check; starting is always safe
|
|
# since _start_inhibit itself no-ops when a lock is already held.
|
|
_start_inhibit
|
|
else
|
|
rm -f "$PRESENCE_FLAG"
|
|
_stop_inhibit
|
|
fi
|
|
|
|
sleep "$(_next_interval)"
|
|
done
|