#!/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