Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de97d6658e | |||
| 9c1e00d0ef | |||
| 874888391e | |||
| 51157ecb23 | |||
| 183e10c1cc | |||
| 094878fc2e | |||
| cf18105887 | |||
| f6f33a13ab | |||
| 60d294fa37 |
Generated
+20
-1
@@ -59,6 +59,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -569,7 +575,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.7.0"
|
version = "0.7.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
@@ -580,6 +586,7 @@ dependencies = [
|
|||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
"log",
|
"log",
|
||||||
|
"nix",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"systemd-journal-logger",
|
"systemd-journal-logger",
|
||||||
@@ -588,6 +595,18 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.7.0"
|
version = "0.7.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -16,6 +16,7 @@ toml = "0.8"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
graphene-rs = { version = "0.22", package = "graphene-rs" }
|
graphene-rs = { version = "0.22", package = "graphene-rs" }
|
||||||
|
nix = { version = "0.29", features = ["signal"] }
|
||||||
zeroize = { version = "1", features = ["std"] }
|
zeroize = { version = "1", features = ["std"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
systemd-journal-logger = "2.2"
|
systemd-journal-logger = "2.2"
|
||||||
@@ -23,5 +24,10 @@ systemd-journal-logger = "2.2"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
glib-build-tools = "0.22"
|
glib-build-tools = "0.22"
|
||||||
|
|||||||
+24
-3
@@ -1,15 +1,36 @@
|
|||||||
# Decisions
|
# Decisions
|
||||||
|
|
||||||
|
## 2026-04-06 – Restore explicit gtk-theme in moongreet config
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: GTK4 under greetd does not reliably read `/etc/xdg/gtk-4.0/settings.ini` — likely requires a settings daemon that doesn't run in the greeter session. moongreet fell back to Adwaita/Colloid-default (blue accent) instead of Colloid-Grey-Dark-Catppuccin.
|
||||||
|
- **Tradeoffs**: Reverts `094878f` ("Remove gtk-theme from app config, use system-wide GTK settings instead"). Duplicates the theme name between settings.ini and moongreet.toml, but the explicit set via `set_gtk_theme_name()` is the only reliable path in a greetd context.
|
||||||
|
- **How**: Added `gtk-theme = "Colloid-Grey-Dark-Catppuccin"` to example config and deployed `/etc/moongreet/moongreet.toml`.
|
||||||
|
|
||||||
|
## 2026-04-02 – Replace hardcoded CSS colors with GTK theme variables
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: moongreet used hardcoded colors (#1a1a2e, white, #ff6b6b) while moonset already used @theme_bg_color, @theme_fg_color, @error_color etc. Inconsistent across the ecosystem and broke theme flexibility.
|
||||||
|
- **Tradeoffs**: Depends on the active GTK theme defining standard color variables. Catppuccin Colloid provides all needed vars (@theme_bg_color, @theme_fg_color, @error_color, @success_color, @theme_selected_bg_color). Fallback behavior if a theme lacks vars is GTK's default colors — acceptable.
|
||||||
|
- **How**: Replaced all hardcoded hex/named colors with GTK theme variables. Coordinated change across moongreet, moonlock, and moonset (all three now use identical pattern).
|
||||||
|
|
||||||
|
## 2026-03-31 – Fourth audit: power timeout, timing mitigation, release profile, GREETD_SOCK caching
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: Fourth triple audit found moongreet power.rs had no timeout on loginctl (greeter could freeze), username enumeration via timing differential, GREETD_SOCK re-read on every login, missing release profile, and missing GResource compression.
|
||||||
|
- **Tradeoffs**: 500ms minimum login response time adds slight delay on fast auth but prevents timing-based username enumeration. Power timeout (30s + SIGKILL) matches moonset pattern — aggressive but prevents greeter freeze.
|
||||||
|
- **How**: (1) power.rs adapted from moonset with 30s timeout + SIGKILL (nix dependency added). (2) 500ms min response floor in attempt_login via Instant + glib::timeout_future. (3) GREETD_SOCK cached in GreeterState at startup. (4) `[profile.release]` with LTO, codegen-units=1, strip. (5) `compressed="true"` on GResource entries. (6) SYNC comments on duplicated blur/background functions.
|
||||||
|
|
||||||
## 2026-03-30 – Full audit fix: security, quality, performance (v0.6.2)
|
## 2026-03-30 – Full audit fix: security, quality, performance (v0.6.2)
|
||||||
|
|
||||||
- **Who**: Ragnar, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Three parallel audits (security, code quality, performance) identified 10 actionable findings across the codebase — from world-readable cache dirs to a GPU blur geometry bug to a race condition in fingerprint probing.
|
- **Why**: Three parallel audits (security, code quality, performance) identified 10 actionable findings across the codebase — from world-readable cache dirs to a GPU blur geometry bug to a race condition in fingerprint probing.
|
||||||
- **Tradeoffs**: `too_many_arguments` Clippy warnings suppressed with `#[allow]` rather than introducing a `UiWidgets` struct — GTK's `clone!` macro with `#[weak]` refs requires individual widget parameters, a struct would fight the idiom. Async avatar loading skipped because `Pixbuf` is `!Send`; cache already prevents repeat loads. TOCTOU socket pre-check removed entirely — `connect()` in login_worker already handles errors, the `metadata()` check gave false security guarantees.
|
- **Tradeoffs**: `too_many_arguments` Clippy warnings suppressed with `#[allow]` rather than introducing a `UiWidgets` struct — GTK's `clone!` macro with `#[weak]` refs requires individual widget parameters, a struct would fight the idiom. Async avatar loading skipped because `Pixbuf` is `!Send`; cache already prevents repeat loads. TOCTOU socket pre-check removed entirely — `connect()` in login_worker already handles errors, the `metadata()` check gave false security guarantees.
|
||||||
- **How**: Cache dirs use `DirBuilder::mode(0o700)` instead of `create_dir_all`. Blur config clamped to `0.0..=200.0` with `is_finite()` guard. Blur texture cached in `Rc<RefCell<Option<gdk::Texture>>>` across monitors. FingerprintProbe device proxy cached in `GreeterState` with generation counter to prevent stale async writes. GPU blur geometry fixed (`-pad` origin shift instead of texture stretching). `is_valid_gtk_theme` extracted as testable function. 9 new tests.
|
- **How**: Cache dirs use `DirBuilder::mode(0o700)` instead of `create_dir_all`. Blur config clamped to `0.0..=200.0` with `is_finite()` guard. Blur texture cached in `Rc<RefCell<Option<gdk::Texture>>>` across monitors. FingerprintProbe device proxy cached in `GreeterState` with generation counter to prevent stale async writes. GPU blur geometry fixed (`-pad` origin shift instead of texture stretching). `is_valid_gtk_theme` extracted as testable function. 9 new tests.
|
||||||
|
|
||||||
## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM
|
## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM
|
||||||
|
|
||||||
- **Who**: Ragnar, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen.
|
- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen.
|
||||||
- **Tradeoffs**: Direct fprintd D-Bus verification (like moonlock) can't start a greetd session — greetd controls session creation via PAM. Using greetd multi-stage means PAM decides the auth order (fingerprint first, then password fallback), not truly parallel. Acceptable — matches standard pam_fprintd behavior.
|
- **Tradeoffs**: Direct fprintd D-Bus verification (like moonlock) can't start a greetd session — greetd controls session creation via PAM. Using greetd multi-stage means PAM decides the auth order (fingerprint first, then password fallback), not truly parallel. Acceptable — matches standard pam_fprintd behavior.
|
||||||
- **How**: Replace single-pass auth with a loop over auth_message rounds. Secret prompts get the password, non-secret prompts (fprintd) get None and block until PAM resolves. fprintd D-Bus probe (gio::DBusProxy) only for UI — detecting device availability and enrolled fingers. 60s socket timeout when fingerprint available. Config option `fingerprint-enabled` (default true).
|
- **How**: Replace single-pass auth with a loop over auth_message rounds. Secret prompts get the password, non-secret prompts (fprintd) get None and block until PAM resolves. fprintd D-Bus probe (gio::DBusProxy) only for UI — detecting device availability and enrolled fingers. 60s socket timeout when fingerprint available. Config option `fingerprint-enabled` (default true).
|
||||||
@@ -23,7 +44,7 @@
|
|||||||
|
|
||||||
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||||
|
|
||||||
- **Who**: Ragnar, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity.
|
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity.
|
||||||
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes `image` crate dependency entirely (~15 transitive crates eliminated). No disk cache needed.
|
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes `image` crate dependency entirely (~15 transitive crates eliminated). No disk cache needed.
|
||||||
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency. Symmetric with moonlock and moonset.
|
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency. Symmetric with moonlock and moonset.
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ Part of the Moonarch ecosystem.
|
|||||||
- **Power actions** — Reboot / Shutdown via `loginctl`
|
- **Power actions** — Reboot / Shutdown via `loginctl`
|
||||||
- **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer)
|
- **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer)
|
||||||
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
||||||
|
- **GPU blur** — Background blur via GskBlurNode (shared cache across monitors)
|
||||||
- **i18n** — German and English (auto-detected from system locale)
|
- **i18n** — German and English (auto-detected from system locale)
|
||||||
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
||||||
- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable)
|
- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable)
|
||||||
|
- **Journal logging** — `journalctl -t moongreet`, debug level via `MOONGREET_DEBUG` env var
|
||||||
|
- **Password wiping** — Zeroize on drop
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -66,8 +69,8 @@ cargo test
|
|||||||
# Build release
|
# Build release
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Run locally (without greetd, needs LD_PRELOAD for layer-shell)
|
# Run locally (without greetd, disables layer-shell)
|
||||||
LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moongreet
|
MOONGREET_NO_LAYER_SHELL=1 ./target/release/moongreet
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ABOUTME: Build script for compiling GResource bundle.
|
// ABOUTME: Build script for compiling GResource bundle.
|
||||||
// ABOUTME: Bundles style.css, wallpaper.jpg, and default-avatar.svg into the binary.
|
// ABOUTME: Bundles style.css and default-avatar.svg into the binary.
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
glib_build_tools::compile_resources(
|
glib_build_tools::compile_resources(
|
||||||
|
|||||||
@@ -4,5 +4,7 @@
|
|||||||
[appearance]
|
[appearance]
|
||||||
# Absolute path to wallpaper image
|
# Absolute path to wallpaper image
|
||||||
background = "/usr/share/backgrounds/wallpaper.jpg"
|
background = "/usr/share/backgrounds/wallpaper.jpg"
|
||||||
# GTK theme for the greeter UI
|
|
||||||
gtk-theme = "Colloid-Catppuccin"
|
# GTK theme name — must match a directory in /usr/share/themes/
|
||||||
|
# Required because GTK4 under greetd does not reliably read settings.ini
|
||||||
|
gtk-theme = "Colloid-Grey-Dark-Catppuccin"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/dev/moonarch/moongreet">
|
<gresource prefix="/dev/moonarch/moongreet">
|
||||||
<file>style.css</file>
|
<file compressed="true">style.css</file>
|
||||||
<file>default-avatar.svg</file>
|
<file compressed="true">default-avatar.svg</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|||||||
+13
-13
@@ -1,16 +1,16 @@
|
|||||||
/* ABOUTME: GTK4 CSS stylesheet for the Moongreet greeter. */
|
/* ABOUTME: GTK4 CSS stylesheet for the Moongreet greeter. */
|
||||||
/* ABOUTME: Defines styling for the login screen layout. */
|
/* ABOUTME: Uses GTK theme colors for consistency with the active desktop theme. */
|
||||||
|
|
||||||
/* Main window background */
|
/* Main window background */
|
||||||
window.greeter {
|
window.greeter {
|
||||||
background-color: #1a1a2e;
|
background-color: @theme_bg_color;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wallpaper-only window for secondary monitors */
|
/* Wallpaper-only window for secondary monitors */
|
||||||
window.wallpaper {
|
window.wallpaper {
|
||||||
background-color: #1a1a2e;
|
background-color: @theme_bg_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Central login area */
|
/* Central login area */
|
||||||
@@ -26,14 +26,14 @@ window.wallpaper {
|
|||||||
min-width: 128px;
|
min-width: 128px;
|
||||||
min-height: 128px;
|
min-height: 128px;
|
||||||
background-color: @theme_selected_bg_color;
|
background-color: @theme_selected_bg_color;
|
||||||
border: 3px solid alpha(white, 0.3);
|
border: 3px solid alpha(@theme_fg_color, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Username label */
|
/* Username label */
|
||||||
.username-label {
|
.username-label {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: @theme_fg_color;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
@@ -50,13 +50,13 @@ window.wallpaper {
|
|||||||
|
|
||||||
/* Error message label */
|
/* Error message label */
|
||||||
.error-label {
|
.error-label {
|
||||||
color: #ff6b6b;
|
color: @error_color;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fingerprint prompt label */
|
/* Fingerprint prompt label */
|
||||||
.fingerprint-label {
|
.fingerprint-label {
|
||||||
color: alpha(white, 0.6);
|
color: alpha(@theme_fg_color, 0.6);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -70,16 +70,16 @@ window.wallpaper {
|
|||||||
.user-list-item {
|
.user-list-item {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: @theme_fg_color;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-list-item:hover {
|
.user-list-item:hover {
|
||||||
background-color: alpha(white, 0.15);
|
background-color: alpha(@theme_fg_color, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-list-item:selected {
|
.user-list-item:selected {
|
||||||
background-color: alpha(white, 0.2);
|
background-color: alpha(@theme_fg_color, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Power buttons on the bottom right */
|
/* Power buttons on the bottom right */
|
||||||
@@ -88,12 +88,12 @@ window.wallpaper {
|
|||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background-color: alpha(white, 0.1);
|
background-color: alpha(@theme_fg_color, 0.1);
|
||||||
color: white;
|
color: @theme_fg_color;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-button:hover {
|
.power-button:hover {
|
||||||
background-color: alpha(white, 0.25);
|
background-color: alpha(@theme_fg_color, 0.25);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-3
@@ -133,6 +133,10 @@ pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
|||||||
|
|
||||||
// -- GPU blur via GskBlurNode -------------------------------------------------
|
// -- GPU blur via GskBlurNode -------------------------------------------------
|
||||||
|
|
||||||
|
// SYNC: MAX_BLUR_DIMENSION, render_blurred_texture, and create_background_picture
|
||||||
|
// are duplicated in moonlock/src/lockscreen.rs and moonset/src/panel.rs.
|
||||||
|
// Changes here must be mirrored to the other two projects.
|
||||||
|
|
||||||
/// Maximum texture dimension before downscaling for blur.
|
/// Maximum texture dimension before downscaling for blur.
|
||||||
/// Keeps GPU work reasonable on 4K+ displays.
|
/// Keeps GPU work reasonable on 4K+ displays.
|
||||||
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
const MAX_BLUR_DIMENSION: f32 = 1920.0;
|
||||||
@@ -240,6 +244,7 @@ struct GreeterState {
|
|||||||
default_avatar_texture: Option<gdk::Texture>,
|
default_avatar_texture: Option<gdk::Texture>,
|
||||||
failed_attempts: HashMap<String, u32>,
|
failed_attempts: HashMap<String, u32>,
|
||||||
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
||||||
|
greetd_sock_path: Option<String>,
|
||||||
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
||||||
fingerprint_available: bool,
|
fingerprint_available: bool,
|
||||||
/// Incremented on each user switch to discard stale async results.
|
/// Incremented on each user switch to discard stale async results.
|
||||||
@@ -281,12 +286,16 @@ pub fn create_greeter_window(
|
|||||||
log::debug!("GTK theme: {theme}");
|
log::debug!("GTK theme: {theme}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache GREETD_SOCK at startup — it never changes during runtime
|
||||||
|
let greetd_sock_path = std::env::var("GREETD_SOCK").ok().filter(|p| !p.is_empty());
|
||||||
|
|
||||||
let state = Rc::new(RefCell::new(GreeterState {
|
let state = Rc::new(RefCell::new(GreeterState {
|
||||||
selected_user: None,
|
selected_user: None,
|
||||||
avatar_cache: HashMap::new(),
|
avatar_cache: HashMap::new(),
|
||||||
default_avatar_texture: None,
|
default_avatar_texture: None,
|
||||||
failed_attempts: HashMap::new(),
|
failed_attempts: HashMap::new(),
|
||||||
greetd_sock: Arc::new(Mutex::new(None)),
|
greetd_sock: Arc::new(Mutex::new(None)),
|
||||||
|
greetd_sock_path,
|
||||||
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
fingerprint_available: false,
|
fingerprint_available: false,
|
||||||
user_switch_generation: 0,
|
user_switch_generation: 0,
|
||||||
@@ -544,6 +553,18 @@ pub fn create_greeter_window(
|
|||||||
));
|
));
|
||||||
window.add_controller(key_controller);
|
window.add_controller(key_controller);
|
||||||
|
|
||||||
|
// Grab keyboard focus after map — layer-shell keyboard grab is only
|
||||||
|
// confirmed by the compositor at map time, not at realize time.
|
||||||
|
window.connect_map(clone!(
|
||||||
|
#[weak]
|
||||||
|
password_entry,
|
||||||
|
move |_| {
|
||||||
|
glib::idle_add_local_once(move || {
|
||||||
|
password_entry.grab_focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
// Defer initial user selection until realized (for correct theme colors)
|
// Defer initial user selection until realized (for correct theme colors)
|
||||||
window.connect_realize(clone!(
|
window.connect_realize(clone!(
|
||||||
#[strong]
|
#[strong]
|
||||||
@@ -960,9 +981,9 @@ fn attempt_login(
|
|||||||
session_dropdown: >k::DropDown,
|
session_dropdown: >k::DropDown,
|
||||||
) {
|
) {
|
||||||
log::debug!("Login attempt for user: {}", user.username);
|
log::debug!("Login attempt for user: {}", user.username);
|
||||||
let sock_path = match std::env::var("GREETD_SOCK") {
|
let sock_path = match state.borrow().greetd_sock_path.clone() {
|
||||||
Ok(p) if !p.is_empty() => p,
|
Some(p) => p,
|
||||||
_ => {
|
None => {
|
||||||
show_error(error_label, password_entry, strings.greetd_sock_not_set);
|
show_error(error_label, password_entry, strings.greetd_sock_not_set);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1009,6 +1030,8 @@ fn attempt_login(
|
|||||||
state,
|
state,
|
||||||
async move {
|
async move {
|
||||||
let session_name_clone = session_name.clone();
|
let session_name_clone = session_name.clone();
|
||||||
|
// Minimum response time to prevent username enumeration via timing
|
||||||
|
let login_start = std::time::Instant::now();
|
||||||
let result = gio::spawn_blocking(move || {
|
let result = gio::spawn_blocking(move || {
|
||||||
login_worker(
|
login_worker(
|
||||||
&username,
|
&username,
|
||||||
@@ -1022,6 +1045,11 @@ fn attempt_login(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
let elapsed = login_start.elapsed();
|
||||||
|
let min_response = std::time::Duration::from_millis(500);
|
||||||
|
if elapsed < min_response {
|
||||||
|
glib::timeout_future(min_response - elapsed).await;
|
||||||
|
}
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(LoginResult::Success { username })) => {
|
Ok(Ok(LoginResult::Success { username })) => {
|
||||||
|
|||||||
+70
-19
@@ -2,11 +2,18 @@
|
|||||||
// ABOUTME: Wrappers around system commands for the greeter UI.
|
// ABOUTME: Wrappers around system commands for the greeter UI.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::process::Command;
|
use std::io::Read;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const POWER_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PowerError {
|
pub enum PowerError {
|
||||||
CommandFailed { action: &'static str, message: String },
|
CommandFailed { action: &'static str, message: String },
|
||||||
|
Timeout { action: &'static str },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PowerError {
|
impl fmt::Display for PowerError {
|
||||||
@@ -15,41 +22,79 @@ impl fmt::Display for PowerError {
|
|||||||
PowerError::CommandFailed { action, message } => {
|
PowerError::CommandFailed { action, message } => {
|
||||||
write!(f, "{action} failed: {message}")
|
write!(f, "{action} failed: {message}")
|
||||||
}
|
}
|
||||||
|
PowerError::Timeout { action } => {
|
||||||
|
write!(f, "{action} timed out")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for PowerError {}
|
impl std::error::Error for PowerError {}
|
||||||
|
|
||||||
/// Run a command and return a PowerError on failure.
|
/// Run a command with timeout and return a PowerError on failure.
|
||||||
|
///
|
||||||
|
/// Uses blocking `child.wait()` with a separate timeout thread that sends
|
||||||
|
/// SIGKILL after POWER_TIMEOUT. This runs inside `gio::spawn_blocking`,
|
||||||
|
/// so blocking is expected.
|
||||||
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
|
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
|
||||||
log::debug!("Power action: {action} ({program} {args:?})");
|
log::debug!("Power action: {action} ({program} {args:?})");
|
||||||
let child = Command::new(program)
|
let mut child = Command::new(program)
|
||||||
.args(args)
|
.args(args)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| PowerError::CommandFailed {
|
.map_err(|e| PowerError::CommandFailed {
|
||||||
action,
|
action,
|
||||||
message: e.to_string(),
|
message: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let output = child
|
let child_pid = nix::unistd::Pid::from_raw(child.id() as i32);
|
||||||
.wait_with_output()
|
let done = Arc::new(AtomicBool::new(false));
|
||||||
.map_err(|e| PowerError::CommandFailed {
|
let done_clone = done.clone();
|
||||||
action,
|
|
||||||
message: e.to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
let timeout_thread = std::thread::spawn(move || {
|
||||||
log::debug!("Power action {action} completed successfully");
|
let interval = Duration::from_millis(100);
|
||||||
|
let mut elapsed = Duration::ZERO;
|
||||||
|
while elapsed < POWER_TIMEOUT {
|
||||||
|
std::thread::sleep(interval);
|
||||||
|
if done_clone.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elapsed += interval;
|
||||||
|
}
|
||||||
|
// ESRCH if the process already exited — harmless
|
||||||
|
let _ = nix::sys::signal::kill(child_pid, nix::sys::signal::Signal::SIGKILL);
|
||||||
|
});
|
||||||
|
|
||||||
|
let status = child.wait().map_err(|e| PowerError::CommandFailed {
|
||||||
|
action,
|
||||||
|
message: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
done.store(true, Ordering::Relaxed);
|
||||||
|
let _ = timeout_thread.join();
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
log::debug!("Power action {action} completed");
|
||||||
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
#[cfg(unix)]
|
||||||
return Err(PowerError::CommandFailed {
|
{
|
||||||
action,
|
use std::os::unix::process::ExitStatusExt;
|
||||||
message: format!("exit code {}: {}", output.status, stderr.trim()),
|
if status.signal() == Some(9) {
|
||||||
});
|
return Err(PowerError::Timeout { action });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
let mut stderr_buf = String::new();
|
||||||
|
if let Some(mut stderr) = child.stderr.take() {
|
||||||
|
let _ = stderr.read_to_string(&mut stderr_buf);
|
||||||
|
}
|
||||||
|
Err(PowerError::CommandFailed {
|
||||||
|
action,
|
||||||
|
message: format!("exit code {}: {}", status, stderr_buf.trim()),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reboot the system via loginctl.
|
/// Reboot the system via loginctl.
|
||||||
@@ -75,6 +120,12 @@ mod tests {
|
|||||||
assert_eq!(err.to_string(), "reboot failed: No such file or directory");
|
assert_eq!(err.to_string(), "reboot failed: No such file or directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn power_error_timeout_display() {
|
||||||
|
let err = PowerError::Timeout { action: "shutdown" };
|
||||||
|
assert_eq!(err.to_string(), "shutdown timed out");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_command_returns_error_for_missing_binary() {
|
fn run_command_returns_error_for_missing_binary() {
|
||||||
let result = run_command("test", "nonexistent-binary-xyz", &[]);
|
let result = run_command("test", "nonexistent-binary-xyz", &[]);
|
||||||
@@ -99,7 +150,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_command_passes_args() {
|
fn run_command_passes_args() {
|
||||||
let result = run_command("test", "true", &["--ignored-arg"]);
|
let result = run_command("test", "echo", &["hello", "world"]);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user