Compare commits
6 Commits
14d6476e5a
..
v0.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
| d5e431d37e | |||
| 7c10516473 | |||
| 09371b5fd2 | |||
| 3c39467508 | |||
| 64470f99c3 | |||
| 293bba32a6 |
@@ -56,12 +56,13 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
|||||||
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
|
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
|
||||||
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
||||||
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
||||||
- **Symmetrie mit moonset**: Gleiche Patterns (i18n, config, users, power, GResource)
|
- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate
|
||||||
|
- **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur)
|
||||||
- **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt
|
- **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt
|
||||||
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
|
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
|
||||||
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
|
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
|
||||||
- **File Permissions**: Cache-Dateien 0o600
|
- **File Permissions**: Cache-Dateien 0o600
|
||||||
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests
|
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests
|
||||||
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster (Greeter + Wallpaper-Windows) geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
|
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
|
||||||
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
|
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
|
||||||
- **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal
|
- **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal
|
||||||
|
|||||||
Generated
+2
-1
@@ -569,13 +569,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.4.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"glib-build-tools",
|
"glib-build-tools",
|
||||||
|
"graphene-rs",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.4.1"
|
version = "0.5.1"
|
||||||
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"
|
||||||
@@ -15,6 +15,7 @@ gio = "0.22"
|
|||||||
toml = "0.8"
|
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" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
systemd-journal-logger = "2.2"
|
systemd-journal-logger = "2.2"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# Decisions
|
# Decisions
|
||||||
|
|
||||||
|
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||||
|
|
||||||
|
- **Who**: Ragnar, 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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
||||||
|
|
||||||
|
- **Who**: Selene, Dom
|
||||||
|
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
|
||||||
|
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
|
||||||
|
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns blurred `gdk::Texture`. Config option `background-blur: Option<f32>` in `[appearance]` TOML section.
|
||||||
|
|
||||||
## 2026-03-28 – Audit fixes for shared wallpaper texture (v0.4.1)
|
## 2026-03-28 – Audit fixes for shared wallpaper texture (v0.4.1)
|
||||||
- **Who**: Selene, Dominik
|
- **Who**: Selene, Dominik
|
||||||
- **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling
|
- **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling
|
||||||
|
|||||||
+22
-4
@@ -22,6 +22,8 @@ struct TomlConfig {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
struct Appearance {
|
struct Appearance {
|
||||||
background: Option<String>,
|
background: Option<String>,
|
||||||
|
#[serde(rename = "background-blur")]
|
||||||
|
background_blur: Option<f32>,
|
||||||
#[serde(rename = "gtk-theme")]
|
#[serde(rename = "gtk-theme")]
|
||||||
gtk_theme: Option<String>,
|
gtk_theme: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ struct Appearance {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub background_path: Option<String>,
|
pub background_path: Option<String>,
|
||||||
|
pub background_blur: Option<f32>,
|
||||||
pub gtk_theme: Option<String>,
|
pub gtk_theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +59,9 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
Some(parent.join(&bg).to_string_lossy().to_string());
|
Some(parent.join(&bg).to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if appearance.background_blur.is_some() {
|
||||||
|
merged.background_blur = appearance.background_blur;
|
||||||
|
}
|
||||||
if appearance.gtk_theme.is_some() {
|
if appearance.gtk_theme.is_some() {
|
||||||
merged.gtk_theme = appearance.gtk_theme;
|
merged.gtk_theme = appearance.gtk_theme;
|
||||||
}
|
}
|
||||||
@@ -72,7 +78,7 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Config result: background={:?}, gtk_theme={:?}", merged.background_path, merged.gtk_theme);
|
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme);
|
||||||
merged
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +120,7 @@ mod tests {
|
|||||||
fn default_config_has_none_fields() {
|
fn default_config_has_none_fields() {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
assert!(config.background_path.is_none());
|
assert!(config.background_path.is_none());
|
||||||
|
assert!(config.background_blur.is_none());
|
||||||
assert!(config.gtk_theme.is_none());
|
assert!(config.gtk_theme.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +138,7 @@ mod tests {
|
|||||||
let conf = dir.path().join("moongreet.toml");
|
let conf = dir.path().join("moongreet.toml");
|
||||||
fs::write(
|
fs::write(
|
||||||
&conf,
|
&conf,
|
||||||
"[appearance]\nbackground = \"/custom/wallpaper.jpg\"\ngtk-theme = \"catppuccin\"\n",
|
"[appearance]\nbackground = \"/custom/wallpaper.jpg\"\nbackground-blur = 20.0\ngtk-theme = \"catppuccin\"\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let paths = vec![conf];
|
let paths = vec![conf];
|
||||||
@@ -140,9 +147,20 @@ mod tests {
|
|||||||
config.background_path.as_deref(),
|
config.background_path.as_deref(),
|
||||||
Some("/custom/wallpaper.jpg")
|
Some("/custom/wallpaper.jpg")
|
||||||
);
|
);
|
||||||
|
assert_eq!(config.background_blur, Some(20.0));
|
||||||
assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin"));
|
assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_blur_optional() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let conf = dir.path().join("moongreet.toml");
|
||||||
|
fs::write(&conf, "[appearance]\nbackground = \"/bg.jpg\"\n").unwrap();
|
||||||
|
let paths = vec![conf];
|
||||||
|
let config = load_config(Some(&paths));
|
||||||
|
assert!(config.background_blur.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_config_resolves_relative_background() {
|
fn load_config_resolves_relative_background() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -196,7 +214,7 @@ mod tests {
|
|||||||
fs::write(&wallpaper, "fake").unwrap();
|
fs::write(&wallpaper, "fake").unwrap();
|
||||||
let config = Config {
|
let config = Config {
|
||||||
background_path: Some(wallpaper.to_str().unwrap().to_string()),
|
background_path: Some(wallpaper.to_str().unwrap().to_string()),
|
||||||
gtk_theme: None,
|
..Config::default()
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
||||||
@@ -208,7 +226,7 @@ mod tests {
|
|||||||
fn resolve_ignores_config_path_when_file_missing() {
|
fn resolve_ignores_config_path_when_file_missing() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
background_path: Some("/nonexistent/wallpaper.jpg".to_string()),
|
background_path: Some("/nonexistent/wallpaper.jpg".to_string()),
|
||||||
gtk_theme: None,
|
..Config::default()
|
||||||
};
|
};
|
||||||
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
||||||
assert!(result.to_str().unwrap().contains("moongreet"));
|
assert!(result.to_str().unwrap().contains("moongreet"));
|
||||||
|
|||||||
+134
-40
@@ -92,10 +92,10 @@ fn is_valid_username(name: &str) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
name.chars()
|
name.chars()
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' || c == '@')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the background image as a shared texture (decode once, reuse everywhere).
|
/// Load background texture from GResource or filesystem.
|
||||||
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
||||||
let path_str = bg_path.to_str()?;
|
let path_str = bg_path.to_str()?;
|
||||||
if bg_path.starts_with("/dev/moonarch/moongreet") {
|
if bg_path.starts_with("/dev/moonarch/moongreet") {
|
||||||
@@ -135,9 +135,31 @@ pub fn load_background_texture(bg_path: &Path) -> Option<gdk::Texture> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- GPU blur via GskBlurNode -------------------------------------------------
|
||||||
|
|
||||||
|
/// Render a blurred texture using the GPU via GskBlurNode.
|
||||||
|
fn render_blurred_texture(
|
||||||
|
widget: &impl IsA<gtk::Widget>,
|
||||||
|
texture: &gdk::Texture,
|
||||||
|
sigma: f32,
|
||||||
|
) -> Option<gdk::Texture> {
|
||||||
|
let native = widget.native()?;
|
||||||
|
let renderer = native.renderer()?;
|
||||||
|
let snapshot = gtk::Snapshot::new();
|
||||||
|
let bounds = graphene_rs::Rect::new(
|
||||||
|
0.0, 0.0, texture.width() as f32, texture.height() as f32,
|
||||||
|
);
|
||||||
|
snapshot.push_blur(sigma as f64);
|
||||||
|
snapshot.append_texture(texture, &bounds);
|
||||||
|
snapshot.pop();
|
||||||
|
let node = snapshot.to_node()?;
|
||||||
|
Some(renderer.render_texture(&node, None))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a wallpaper-only window for secondary monitors.
|
/// Create a wallpaper-only window for secondary monitors.
|
||||||
pub fn create_wallpaper_window(
|
pub fn create_wallpaper_window(
|
||||||
texture: &gdk::Texture,
|
texture: &gdk::Texture,
|
||||||
|
blur_radius: Option<f32>,
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
) -> gtk::ApplicationWindow {
|
) -> gtk::ApplicationWindow {
|
||||||
let window = gtk::ApplicationWindow::builder()
|
let window = gtk::ApplicationWindow::builder()
|
||||||
@@ -145,18 +167,28 @@ pub fn create_wallpaper_window(
|
|||||||
.build();
|
.build();
|
||||||
window.add_css_class("wallpaper");
|
window.add_css_class("wallpaper");
|
||||||
|
|
||||||
let background = create_background_picture(texture);
|
let background = create_background_picture(texture, blur_radius);
|
||||||
window.set_child(Some(&background));
|
window.set_child(Some(&background));
|
||||||
|
|
||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a Picture widget for the wallpaper background from a pre-loaded texture.
|
/// Create a Picture widget for the wallpaper background, optionally with GPU blur.
|
||||||
fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
|
fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -> gtk::Picture {
|
||||||
let background = gtk::Picture::for_paintable(texture);
|
let background = gtk::Picture::for_paintable(texture);
|
||||||
background.set_content_fit(gtk::ContentFit::Cover);
|
background.set_content_fit(gtk::ContentFit::Cover);
|
||||||
background.set_hexpand(true);
|
background.set_hexpand(true);
|
||||||
background.set_vexpand(true);
|
background.set_vexpand(true);
|
||||||
|
|
||||||
|
if let Some(sigma) = blur_radius.filter(|s| *s > 0.0) {
|
||||||
|
let texture = texture.clone();
|
||||||
|
background.connect_realize(move |picture| {
|
||||||
|
if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) {
|
||||||
|
picture.set_paintable(Some(&blurred));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
background
|
background
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +252,7 @@ pub fn create_greeter_window(
|
|||||||
|
|
||||||
// Background wallpaper
|
// Background wallpaper
|
||||||
if let Some(texture) = texture {
|
if let Some(texture) = texture {
|
||||||
overlay.set_child(Some(&create_background_picture(texture)));
|
overlay.set_child(Some(&create_background_picture(texture, config.background_blur)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
// Main layout: 3 rows (top spacer, center login, bottom bar)
|
||||||
@@ -368,7 +400,7 @@ pub fn create_greeter_window(
|
|||||||
error_label,
|
error_label,
|
||||||
move |btn| {
|
move |btn| {
|
||||||
btn.set_sensitive(false);
|
btn.set_sensitive(false);
|
||||||
execute_power_action(power::reboot, strings.reboot_failed, &error_label);
|
execute_power_action(power::reboot, strings.reboot_failed, &error_label, btn);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
power_box.append(&reboot_btn);
|
power_box.append(&reboot_btn);
|
||||||
@@ -382,7 +414,7 @@ pub fn create_greeter_window(
|
|||||||
error_label,
|
error_label,
|
||||||
move |btn| {
|
move |btn| {
|
||||||
btn.set_sensitive(false);
|
btn.set_sensitive(false);
|
||||||
execute_power_action(power::shutdown, strings.shutdown_failed, &error_label);
|
execute_power_action(power::shutdown, strings.shutdown_failed, &error_label, btn);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
power_box.append(&shutdown_btn);
|
power_box.append(&shutdown_btn);
|
||||||
@@ -602,16 +634,33 @@ fn set_avatar_from_file(
|
|||||||
username: Option<&str>,
|
username: Option<&str>,
|
||||||
state: &Rc<RefCell<GreeterState>>,
|
state: &Rc<RefCell<GreeterState>>,
|
||||||
) {
|
) {
|
||||||
// Reject oversized files
|
// Re-check symlink status to narrow TOCTOU window from get_avatar_path_with()
|
||||||
if let Ok(meta) = std::fs::metadata(path) {
|
match std::fs::symlink_metadata(path) {
|
||||||
if meta.len() > MAX_AVATAR_FILE_SIZE {
|
Ok(meta) if meta.file_type().is_symlink() => {
|
||||||
|
log::warn!("Rejecting symlink avatar at load time: {}", path.display());
|
||||||
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(meta) if meta.len() > MAX_AVATAR_FILE_SIZE => {
|
||||||
log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display());
|
log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display());
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("Cannot stat avatar {}: {e}", path.display());
|
||||||
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
|
let Some(path_str) = path.to_str() else {
|
||||||
|
log::debug!("Non-UTF-8 avatar path, skipping: {}", path.display());
|
||||||
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
|
||||||
Ok(pixbuf) => {
|
Ok(pixbuf) => {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
if let Some(name) = username {
|
if let Some(name) = username {
|
||||||
@@ -729,6 +778,15 @@ fn show_error(
|
|||||||
password_entry.grab_focus();
|
password_entry.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract and length-check a greetd error description from a JSON response.
|
||||||
|
fn extract_greetd_description<'a>(response: &'a serde_json::Value, fallback: &'a str) -> &'a str {
|
||||||
|
response
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|d| !d.is_empty() && d.len() <= MAX_GREETD_ERROR_LENGTH)
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
/// Display a greetd error, using a fallback for missing or oversized descriptions.
|
/// Display a greetd error, using a fallback for missing or oversized descriptions.
|
||||||
fn show_greetd_error(
|
fn show_greetd_error(
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
@@ -736,15 +794,8 @@ fn show_greetd_error(
|
|||||||
response: &serde_json::Value,
|
response: &serde_json::Value,
|
||||||
fallback: &str,
|
fallback: &str,
|
||||||
) {
|
) {
|
||||||
let description = response
|
let message = extract_greetd_description(response, fallback);
|
||||||
.get("description")
|
show_error(error_label, password_entry, message);
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH {
|
|
||||||
show_error(error_label, password_entry, description);
|
|
||||||
} else {
|
|
||||||
show_error(error_label, password_entry, fallback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel any in-progress greetd session.
|
/// Cancel any in-progress greetd session.
|
||||||
@@ -973,15 +1024,7 @@ fn login_worker(
|
|||||||
return Ok(LoginResult::Cancelled);
|
return Ok(LoginResult::Cancelled);
|
||||||
}
|
}
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
|
||||||
let description = response
|
let message = extract_greetd_description(&response, strings.auth_failed).to_string();
|
||||||
.get("description")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let message = if !description.is_empty() && description.len() <= MAX_GREETD_ERROR_LENGTH {
|
|
||||||
description.to_string()
|
|
||||||
} else {
|
|
||||||
strings.auth_failed.to_string()
|
|
||||||
};
|
|
||||||
return Ok(LoginResult::Error { message });
|
return Ok(LoginResult::Error { message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1050,10 +1093,7 @@ fn login_worker(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Ok(LoginResult::Error {
|
return Ok(LoginResult::Error {
|
||||||
message: response
|
message: extract_greetd_description(&response, strings.session_start_failed)
|
||||||
.get("description")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or(strings.session_start_failed)
|
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1069,10 +1109,13 @@ fn execute_power_action(
|
|||||||
action_fn: fn() -> Result<(), PowerError>,
|
action_fn: fn() -> Result<(), PowerError>,
|
||||||
error_message: &'static str,
|
error_message: &'static str,
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
|
button: >k::Button,
|
||||||
) {
|
) {
|
||||||
glib::spawn_future_local(clone!(
|
glib::spawn_future_local(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
|
#[weak]
|
||||||
|
button,
|
||||||
async move {
|
async move {
|
||||||
let result = gio::spawn_blocking(move || action_fn()).await;
|
let result = gio::spawn_blocking(move || action_fn()).await;
|
||||||
|
|
||||||
@@ -1082,11 +1125,13 @@ fn execute_power_action(
|
|||||||
log::error!("Power action failed: {e}");
|
log::error!("Power action failed: {e}");
|
||||||
error_label.set_text(error_message);
|
error_label.set_text(error_message);
|
||||||
error_label.set_visible(true);
|
error_label.set_visible(true);
|
||||||
|
button.set_sensitive(true);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::error!("Power action panicked");
|
log::error!("Power action panicked");
|
||||||
error_label.set_text(error_message);
|
error_label.set_text(error_message);
|
||||||
error_label.set_visible(true);
|
error_label.set_visible(true);
|
||||||
|
button.set_sensitive(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1117,18 +1162,24 @@ fn save_last_user(username: &str) {
|
|||||||
|
|
||||||
fn save_last_user_to(path: &Path, username: &str) {
|
fn save_last_user_to(path: &Path, username: &str) {
|
||||||
log::debug!("Saving last user: {username}");
|
log::debug!("Saving last user: {username}");
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent()
|
||||||
let _ = std::fs::create_dir_all(parent);
|
&& let Err(e) = std::fs::create_dir_all(parent)
|
||||||
|
{
|
||||||
|
log::warn!("Failed to create cache dir {}: {e}", parent.display());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let _ = std::fs::OpenOptions::new()
|
if let Err(e) = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.mode(0o600)
|
.mode(0o600)
|
||||||
.open(path)
|
.open(path)
|
||||||
.and_then(|mut f| f.write_all(username.as_bytes()));
|
.and_then(|mut f| f.write_all(username.as_bytes()))
|
||||||
|
{
|
||||||
|
log::warn!("Failed to save last user to {}: {e}", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_last_session(username: &str) -> Option<String> {
|
fn load_last_session(username: &str) -> Option<String> {
|
||||||
@@ -1173,13 +1224,16 @@ fn save_last_session_to(path: &Path, session_name: &str) {
|
|||||||
log::debug!("Saving last session: {session_name}");
|
log::debug!("Saving last session: {session_name}");
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let _ = std::fs::OpenOptions::new()
|
if let Err(e) = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.mode(0o600)
|
.mode(0o600)
|
||||||
.open(path)
|
.open(path)
|
||||||
.and_then(|mut f| f.write_all(session_name.as_bytes()));
|
.and_then(|mut f| f.write_all(session_name.as_bytes()))
|
||||||
|
{
|
||||||
|
log::warn!("Failed to save last session to {}: {e}", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1193,6 +1247,8 @@ mod tests {
|
|||||||
assert!(is_valid_username("test-user"));
|
assert!(is_valid_username("test-user"));
|
||||||
assert!(is_valid_username("test.user"));
|
assert!(is_valid_username("test.user"));
|
||||||
assert!(is_valid_username("_admin"));
|
assert!(is_valid_username("_admin"));
|
||||||
|
assert!(is_valid_username("user@domain"));
|
||||||
|
assert!(is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1200,6 +1256,7 @@ mod tests {
|
|||||||
assert!(!is_valid_username(""));
|
assert!(!is_valid_username(""));
|
||||||
assert!(!is_valid_username(".hidden"));
|
assert!(!is_valid_username(".hidden"));
|
||||||
assert!(!is_valid_username("-dash"));
|
assert!(!is_valid_username("-dash"));
|
||||||
|
assert!(!is_valid_username("@domain"));
|
||||||
assert!(!is_valid_username("user/name"));
|
assert!(!is_valid_username("user/name"));
|
||||||
assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1)));
|
assert!(!is_valid_username(&"a".repeat(MAX_USERNAME_LENGTH + 1)));
|
||||||
}
|
}
|
||||||
@@ -1560,6 +1617,18 @@ mod tests {
|
|||||||
assert!(matches!(result, LoginResult::Cancelled));
|
assert!(matches!(result, LoginResult::Cancelled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn login_worker_connect_failure() {
|
||||||
|
let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let result = login_worker(
|
||||||
|
"alice", "pass", "/usr/bin/niri",
|
||||||
|
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
||||||
|
load_strings(Some("en")),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn login_worker_invalid_exec_cmd() {
|
fn login_worker_invalid_exec_cmd() {
|
||||||
let (sock_path, handle) = fake_greetd(|stream| {
|
let (sock_path, handle) = fake_greetd(|stream| {
|
||||||
@@ -1653,4 +1722,29 @@ mod tests {
|
|||||||
let result = load_background_texture(path);
|
let result = load_background_texture(path);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_normal() {
|
||||||
|
let resp = serde_json::json!({"type": "error", "description": "bad password"});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "bad password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_oversized() {
|
||||||
|
let long = "x".repeat(MAX_GREETD_ERROR_LENGTH + 1);
|
||||||
|
let resp = serde_json::json!({"type": "error", "description": long});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_empty() {
|
||||||
|
let resp = serde_json::json!({"type": "error", "description": ""});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_greetd_description_missing() {
|
||||||
|
let resp = serde_json::json!({"type": "error"});
|
||||||
|
assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ pub struct Strings {
|
|||||||
pub session_start_failed: &'static str,
|
pub session_start_failed: &'static str,
|
||||||
pub reboot_failed: &'static str,
|
pub reboot_failed: &'static str,
|
||||||
pub shutdown_failed: &'static str,
|
pub shutdown_failed: &'static str,
|
||||||
pub connection_error: &'static str,
|
|
||||||
pub socket_error: &'static str,
|
pub socket_error: &'static str,
|
||||||
pub unexpected_greetd_response: &'static str,
|
pub unexpected_greetd_response: &'static str,
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ const STRINGS_DE: Strings = Strings {
|
|||||||
session_start_failed: "Session konnte nicht gestartet werden",
|
session_start_failed: "Session konnte nicht gestartet werden",
|
||||||
reboot_failed: "Neustart fehlgeschlagen",
|
reboot_failed: "Neustart fehlgeschlagen",
|
||||||
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
||||||
connection_error: "Verbindungsfehler",
|
|
||||||
socket_error: "Socket-Fehler",
|
socket_error: "Socket-Fehler",
|
||||||
unexpected_greetd_response: "Unerwartete Antwort von greetd",
|
unexpected_greetd_response: "Unerwartete Antwort von greetd",
|
||||||
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
|
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
|
||||||
@@ -76,7 +74,6 @@ const STRINGS_EN: Strings = Strings {
|
|||||||
session_start_failed: "Failed to start session",
|
session_start_failed: "Failed to start session",
|
||||||
reboot_failed: "Reboot failed",
|
reboot_failed: "Reboot failed",
|
||||||
shutdown_failed: "Shutdown failed",
|
shutdown_failed: "Shutdown failed",
|
||||||
connection_error: "Connection error",
|
|
||||||
socket_error: "Socket error",
|
socket_error: "Socket error",
|
||||||
unexpected_greetd_response: "Unexpected response from greetd",
|
unexpected_greetd_response: "Unexpected response from greetd",
|
||||||
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",
|
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",
|
||||||
|
|||||||
+12
-5
@@ -55,6 +55,7 @@ fn activate(app: >k::Application) {
|
|||||||
log::debug!("Background path: {}", bg_path.display());
|
log::debug!("Background path: {}", bg_path.display());
|
||||||
|
|
||||||
// Load background texture once — shared across all windows
|
// Load background texture once — shared across all windows
|
||||||
|
// Blur is applied on the GPU via GskBlurNode at widget realization time.
|
||||||
let bg_texture = greeter::load_background_texture(&bg_path);
|
let bg_texture = greeter::load_background_texture(&bg_path);
|
||||||
if bg_texture.is_none() {
|
if bg_texture.is_none() {
|
||||||
log::error!("Failed to load background texture — greeter will start without wallpaper");
|
log::error!("Failed to load background texture — greeter will start without wallpaper");
|
||||||
@@ -81,7 +82,7 @@ fn activate(app: >k::Application) {
|
|||||||
.item(i)
|
.item(i)
|
||||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||||
{
|
{
|
||||||
let wallpaper = greeter::create_wallpaper_window(texture, app);
|
let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, app);
|
||||||
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
|
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
|
||||||
wallpaper.set_monitor(Some(&monitor));
|
wallpaper.set_monitor(Some(&monitor));
|
||||||
wallpaper.present();
|
wallpaper.present();
|
||||||
@@ -91,10 +92,16 @@ fn activate(app: >k::Application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup_logging() {
|
fn setup_logging() {
|
||||||
systemd_journal_logger::JournalLog::new()
|
match systemd_journal_logger::JournalLog::new() {
|
||||||
.unwrap()
|
Ok(logger) => {
|
||||||
.install()
|
if let Err(e) = logger.install() {
|
||||||
.unwrap();
|
eprintln!("Failed to install journal logger: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create journal logger: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
|
||||||
log::LevelFilter::Debug
|
log::LevelFilter::Debug
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+2
-3
@@ -41,8 +41,7 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
log::debug!("Power action {action} completed successfully");
|
log::debug!("Power action {action} completed successfully");
|
||||||
}
|
} else {
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
return Err(PowerError::CommandFailed {
|
return Err(PowerError::CommandFailed {
|
||||||
action,
|
action,
|
||||||
@@ -100,7 +99,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_command_passes_args() {
|
fn run_command_passes_args() {
|
||||||
let result = run_command("test", "echo", &["hello", "world"]);
|
let result = run_command("test", "true", &["--ignored-arg"]);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"];
|
|||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub exec_cmd: String,
|
pub exec_cmd: String,
|
||||||
|
#[allow(dead_code)] // Retained for future Wayland-only filtering
|
||||||
pub session_type: String,
|
pub session_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-9
@@ -23,9 +23,11 @@ const NOLOGIN_SHELLS: &[&str] = &[
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
#[allow(dead_code)] // Retained for debugging and future UID-based features
|
||||||
pub uid: u32,
|
pub uid: u32,
|
||||||
pub gecos: String,
|
pub gecos: String,
|
||||||
pub home: PathBuf,
|
pub home: PathBuf,
|
||||||
|
#[allow(dead_code)] // Retained for debugging and future shell-based filtering
|
||||||
pub shell: String,
|
pub shell: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,16 +57,13 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
|
|||||||
let mut users = Vec::new();
|
let mut users = Vec::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let parts: Vec<&str> = line.split(':').collect();
|
let mut fields = line.splitn(7, ':');
|
||||||
if parts.len() < 7 {
|
let (Some(username), Some(_pw), Some(uid_str), Some(_gid), Some(gecos), Some(home), Some(shell)) =
|
||||||
|
(fields.next(), fields.next(), fields.next(), fields.next(),
|
||||||
|
fields.next(), fields.next(), fields.next())
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
let username = parts[0];
|
|
||||||
let uid_str = parts[2];
|
|
||||||
let gecos = parts[4];
|
|
||||||
let home = parts[5];
|
|
||||||
let shell = parts[6];
|
|
||||||
|
|
||||||
let uid = match uid_str.parse::<u32>() {
|
let uid = match uid_str.parse::<u32>() {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
|
|||||||
Reference in New Issue
Block a user