7.7 KiB
Design: PAM module + cross-platform support
Date: 2026-05-05 Status: Approved
Problem
ahfail-module.so is a gtklock-specific plugin. It works only on Wayland via gtklock. The goal is to also support X11 Linux lock screens and macOS, while keeping the existing Wayland/gtklock integration intact.
Decision: Approach A — Two integration points, shared core
Keep the gtklock module for Wayland. Add a PAM module for X11 Linux and macOS. Share all display/audio/update logic through a common ahfail-ui crate.
| Platform | Lock screen | Integration | Display |
|---|---|---|---|
| Wayland | gtklock | ahfail-module.so (gtklock module, kept) |
inline GTK overlay via ahfail-ui |
| X11 Linux | any PAM locker (i3lock, xscreensaver…) | ahfail-pam.so |
ahfail-display binary via ahfail-ui |
| macOS | loginwindow / screensaver | ahfail-pam.so |
ahfail-display binary via ahfail-ui |
macOS CI is out of scope — users build from source via Homebrew. Linux x86_64 release binaries are produced by Gitea Actions.
Cargo workspace layout
ahfail/
├── Cargo.toml ← workspace root
├── meson.build ← updated for new crate layout
├── crates/
│ ├── ahfail-gtklock/ → ahfail-module.so (existing module, moved)
│ ├── ahfail-pam/ → ahfail-pam.so (new)
│ ├── ahfail-display/ → ahfail-display (new binary)
│ └── ahfail-ui/ → lib (shared display/audio/update logic)
├── assets/ ← unchanged
├── include/ ← unchanged
└── .gitea/workflows/
├── test.yml
└── release.yml
Dependency graph:
ahfail-gtklock→ahfail-uiahfail-display→ahfail-uiahfail-pam→ nothing (no GTK/GStreamer; minimal by design)
Section 1: ahfail-ui (shared crate)
Contains everything that ahfail-gtklock and ahfail-display share:
- Animation loading (GResources →
PixbufSimpleAnim,set_loop(true)) - GTK display:
show_animation_on_overlay(overlay, monitor, deadzone)— works for both an in-process gtklock overlay and a standalone window - GStreamer audio playback (player pool, seek-to-0 on end-of-stream loop)
- Deadzone logic (random placement with retry,
--deadzone=x,y,w,h) - Update check (see below)
- System volume save/restore (
pactlon Linux,osascripton macOS)
Section 2: PAM module (ahfail-pam)
A minimal C-ABI shared library. No GTK. No GStreamer. No credential handling.
Security principle: we never touch PAM_AUTHTOK or any credential. All authentication is performed exclusively by the native host (pam_unix, pam_opendirectory, etc.). We are a pure side-effect observer.
pam_sm_authenticate:
- Register a cleanup function via
pam_set_data(pamh, "ahfail", …, cleanup_fn) - Return
PAM_IGNORE
All other pam_sm_* functions return PAM_IGNORE.
Cleanup function fires in two cases:
error_status |
Meaning | Action |
|---|---|---|
PAM_DATA_REPLACE set |
pam_sm_authenticate called again (previous attempt failed, same handle) |
spawn ahfail-display |
PAM_AUTH_ERR (no replace) |
final failure from pam_end() |
spawn ahfail-display |
PAM_SUCCESS (no replace) |
auth succeeded from pam_end() |
pkill -SIGTERM ahfail-display; restore volume via state file |
This handles both lock screens that loop pam_authenticate() on one handle and those that create a fresh PAM transaction per attempt.
Finding ahfail-display: compile-time default path (/usr/lib/ahfail/ahfail-display on Linux, /usr/local/lib/ahfail/ahfail-display on macOS), overridable via PAM argument display_path=/custom/path.
PAM config lines:
# Linux — /etc/pam.d/gtklock (or i3lock, xscreensaver, etc.)
auth optional ahfail-pam.so
# macOS — /etc/pam.d/screensaver
auth optional ahfail-pam.so display_path=/usr/local/lib/ahfail/ahfail-display
Section 3: Display binary (ahfail-display)
Spawned by ahfail-pam.so via double-fork (fully detached). Uses ahfail-ui for all rendering.
Startup sequence:
- Initialise GTK + GStreamer
- Register GResources (same embedded assets as gtklock module)
- Acquire volume lock: atomically create
$XDG_RUNTIME_DIR/ahfail.lock- If created (first instance): save current volume/mute state into the file, set system volume to 100% unmuted
- If already exists: skip volume management
- Open floating, decoration-free, always-on-top GTK window
- Call
ahfail-ui::show_animation_on_overlay(...)— places Nedry at random position respecting--deadzone - Loop animation (
PixbufSimpleAnim,set_loop(true)) and audio (GStreamer, seek-to-0 on end-of-stream) - Concurrently: run update check in a background thread (see below)
- Block on GTK main loop until
SIGTERMor 15-minute failsafe timeout
On SIGTERM (clean exit):
- If holding volume lock: restore volume/mute state from lock file, delete lock file
- Exit
Multiple instances: each failed attempt spawns one additional ahfail-display. Each shows one Nedry sprite independently. Only the first instance (lock file holder) manages volume. On success, pkill -SIGTERM ahfail-display terminates all instances simultaneously.
Volume control:
- Linux:
pactl set-sink-mute @DEFAULT_SINK@ 0 && pactl set-sink-volume @DEFAULT_SINK@ 100% - macOS:
osascript -e 'set volume output volume 100 without output muted' - Restore: reverse using saved state from lock file
Update check (background thread):
- Calls
https://gitea.weircon.dk/api/v1/repos/agw/gtk-ahfail/releases/latest, parsestag_name - Rate-limited: skips network call if
~/.cache/ahfail/last_update_checkwas written within 24 hours - If newer version found: fires desktop notification
- Linux:
notify-send "ahfail" "Update available — visit https://gitea.weircon.dk/agw/gtk-ahfail/releases" - macOS:
osascript -e 'display notification "..." with title "ahfail"'
- Linux:
- Fails silently on any error (no network, bad response, parse failure)
Section 4: gtklock module (ahfail-gtklock)
The existing module, moved to crates/ahfail-gtklock/. Integration mechanism is unchanged.
Changes from current src/:
- Display/audio logic extracted into
ahfail-ui; module callsahfail-uifunctions instead of implementing them inline - Volume: on first
error_labelsignal (first failure), acquire volume lock and set to 100%. Onon_window_destroy, restore volume. - Update check: called from
ahfail-uion each failure (same shared code path as display binary) - Multiple Nerdys on multiple failures: unchanged — each
error_labelsignal adds a new sprite to the overlay
Section 5: Gitea CI/CD
.gitea/workflows/test.yml — on push and pull request:
deps: libgtk-3-dev libgstreamer1.0-dev gstreamer-plugins-base libpam0g-dev
→ cargo test
→ meson setup builddir && meson compile -C builddir
→ meson test -C builddir
.gitea/workflows/release.yml — on v* tag push:
→ same build with --buildtype=release
→ bundle into ahfail-linux-x86_64.tar.gz:
ahfail-module.so (gtklock/Wayland)
ahfail-pam.so (PAM/X11+macOS)
ahfail-display (display binary)
→ create Gitea release + upload asset via curl to Gitea API
using GITEA_TOKEN secret
macOS: no CI. README documents:
brew install gtk+3 gstreamer gst-plugins-good gst-plugins-base
cargo build --release
meson setup builddir && meson compile -C builddir
Out of scope
- macOS CI / cross-compilation (SDK licensing + native dep complexity)
- Supporting Wayland compositors other than gtklock (protocol blocks external surfaces during lock)
- Auto-installing updates (notification only)