Files
gtk-ahfail/docs/plans/2026-05-05-pam-rewrite-design.md
Asger Geel Weirsøe 3cdbc4fec9 Add design doc for PAM module + cross-platform rewrite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:09:25 +02:00

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-gtklockahfail-ui
  • ahfail-displayahfail-ui
  • ahfail-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 (pactl on Linux, osascript on 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:

  1. Register a cleanup function via pam_set_data(pamh, "ahfail", …, cleanup_fn)
  2. 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:

  1. Initialise GTK + GStreamer
  2. Register GResources (same embedded assets as gtklock module)
  3. 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
  4. Open floating, decoration-free, always-on-top GTK window
  5. Call ahfail-ui::show_animation_on_overlay(...) — places Nedry at random position respecting --deadzone
  6. Loop animation (PixbufSimpleAnim, set_loop(true)) and audio (GStreamer, seek-to-0 on end-of-stream)
  7. Concurrently: run update check in a background thread (see below)
  8. Block on GTK main loop until SIGTERM or 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, parses tag_name
  • Rate-limited: skips network call if ~/.cache/ahfail/last_update_check was 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"'
  • 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 calls ahfail-ui functions instead of implementing them inline
  • Volume: on first error_label signal (first failure), acquire volume lock and set to 100%. On on_window_destroy, restore volume.
  • Update check: called from ahfail-ui on each failure (same shared code path as display binary)
  • Multiple Nerdys on multiple failures: unchanged — each error_label signal 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)