181 lines
7.7 KiB
Markdown
181 lines
7.7 KiB
Markdown
# 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-ui`
|
|
- `ahfail-display` → `ahfail-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:
|
|
```bash
|
|
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)
|