feat: switch to systemd-journal-logger, add debug logging (v0.4.0)

Replace env_logger file-based logging with systemd-journal-logger for
consistency with moonlock and native journalctl integration. Add debug-level
logging at all decision points: config loading, user/session detection,
avatar resolution, locale detection, IPC messages, login flow, and
persistence. No credentials are ever logged.
This commit is contained in:
2026-03-28 01:23:18 +01:00
parent b91e8d47d1
commit 96c94f030a
11 changed files with 137 additions and 230 deletions
+29 -13
View File
@@ -40,27 +40,39 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
let mut merged = Config::default();
for path in paths {
if let Ok(content) = fs::read_to_string(path) {
if let Ok(parsed) = toml::from_str::<TomlConfig>(&content) {
if let Some(appearance) = parsed.appearance {
if let Some(bg) = appearance.background {
// Resolve relative paths against config file directory
let bg_path = PathBuf::from(&bg);
if bg_path.is_absolute() {
merged.background_path = Some(bg);
} else if let Some(parent) = path.parent() {
merged.background_path =
Some(parent.join(&bg).to_string_lossy().to_string());
match fs::read_to_string(path) {
Ok(content) => {
match toml::from_str::<TomlConfig>(&content) {
Ok(parsed) => {
log::debug!("Config loaded: {}", path.display());
if let Some(appearance) = parsed.appearance {
if let Some(bg) = appearance.background {
// Resolve relative paths against config file directory
let bg_path = PathBuf::from(&bg);
if bg_path.is_absolute() {
merged.background_path = Some(bg);
} else if let Some(parent) = path.parent() {
merged.background_path =
Some(parent.join(&bg).to_string_lossy().to_string());
}
}
if appearance.gtk_theme.is_some() {
merged.gtk_theme = appearance.gtk_theme;
}
}
}
if appearance.gtk_theme.is_some() {
merged.gtk_theme = appearance.gtk_theme;
Err(e) => {
log::warn!("Config parse error in {}: {e}", path.display());
}
}
}
Err(_) => {
log::debug!("Config not found: {}", path.display());
}
}
}
log::debug!("Config result: background={:?}, gtk_theme={:?}", merged.background_path, merged.gtk_theme);
merged
}
@@ -77,16 +89,20 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
if let Some(ref bg) = config.background_path {
let path = PathBuf::from(bg);
if path.is_file() {
log::debug!("Wallpaper: using config path {}", path.display());
return path;
}
log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display());
}
// Moonarch ecosystem default
if moonarch_wallpaper.is_file() {
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
return moonarch_wallpaper.to_path_buf();
}
// GResource fallback path (loaded from compiled resources at runtime)
log::debug!("Wallpaper: using GResource fallback");
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
}
+29 -1
View File
@@ -163,6 +163,10 @@ pub fn create_greeter_window(
let strings = load_strings(None);
let all_users = users::get_users(None);
let all_sessions = sessions::get_sessions(None, None);
log::debug!("Greeter window: {} user(s), {} session(s)", all_users.len(), all_sessions.len());
if let Some(ref theme) = config.gtk_theme {
log::debug!("GTK theme: {theme}");
}
let state = Rc::new(RefCell::new(GreeterState {
selected_user: None,
@@ -489,6 +493,7 @@ fn select_initial_user(
.as_ref()
.and_then(|name| users.iter().find(|u| &u.username == name))
.unwrap_or(&users[0]);
log::debug!("Initial user: {} (last_user={:?})", target.username, last_username);
switch_to_user(
target,
@@ -515,6 +520,7 @@ fn switch_to_user(
sessions: &[Session],
window: &gtk::ApplicationWindow,
) {
log::debug!("Switching to user: {}", user.username);
{
let mut s = state.borrow_mut();
s.selected_user = Some(user.clone());
@@ -531,8 +537,10 @@ fn switch_to_user(
};
if let Some(texture) = cached {
log::debug!("Avatar cache hit for {}", user.username);
avatar_image.set_paintable(Some(&texture));
} else {
log::debug!("Avatar cache miss for {}", user.username);
let avatar_path = users::get_avatar_path(&user.username, &user.home);
if let Some(path) = avatar_path {
// get_avatar_path already checks existence — go straight to loading
@@ -558,6 +566,7 @@ fn set_avatar_from_file(
// Reject oversized files
if let Ok(meta) = std::fs::metadata(path) {
if meta.len() > MAX_AVATAR_FILE_SIZE {
log::debug!("Avatar file too large ({} bytes): {}", meta.len(), path.display());
image.set_icon_name(Some("avatar-default-symbolic"));
return;
}
@@ -574,7 +583,8 @@ fn set_avatar_from_file(
}
image.set_paintable(Some(&texture));
}
Err(_) => {
Err(e) => {
log::debug!("Failed to load avatar {}: {e}", path.display());
image.set_icon_name(Some("avatar-default-symbolic"));
}
}
@@ -590,10 +600,12 @@ fn set_default_avatar(
{
let s = state.borrow();
if let Some(ref texture) = s.default_avatar_texture {
log::debug!("Default avatar: using cached texture");
image.set_paintable(Some(texture));
return;
}
}
log::debug!("Default avatar: tinting SVG from GResource");
let resource_path = users::get_default_avatar_path();
if let Ok(bytes) =
@@ -698,6 +710,7 @@ fn show_greetd_error(
/// Cancel any in-progress greetd session.
fn cancel_pending_session(state: &Rc<RefCell<GreeterState>>) {
log::debug!("Cancelling pending greetd session");
let s = state.borrow();
s.login_cancelled
.store(true, std::sync::atomic::Ordering::SeqCst);
@@ -731,6 +744,7 @@ fn attempt_login(
password_entry: &gtk::PasswordEntry,
session_dropdown: &gtk::DropDown,
) {
log::debug!("Login attempt for user: {}", user.username);
let sock_path = match std::env::var("GREETD_SOCK") {
Ok(p) if !p.is_empty() => p,
_ => {
@@ -740,6 +754,7 @@ fn attempt_login(
};
// Validate socket path
log::debug!("GREETD_SOCK: {sock_path}");
let sock_pathbuf = PathBuf::from(&sock_path);
if !sock_pathbuf.is_absolute() {
show_error(
@@ -886,9 +901,11 @@ fn login_worker(
strings: &Strings,
) -> Result<LoginResult, String> {
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
log::debug!("Login cancelled before connect");
return Ok(LoginResult::Cancelled);
}
log::debug!("Connecting to greetd socket: {sock_path}");
let mut sock = UnixStream::connect(sock_path).map_err(|e| e.to_string())?;
if let Err(e) = sock.set_read_timeout(Some(std::time::Duration::from_secs(10))) {
log::warn!("Failed to set read timeout: {e}");
@@ -902,6 +919,7 @@ fn login_worker(
}
// Step 1: Create session — if a stale session exists, cancel it and retry
log::debug!("Creating greetd session for {username}");
let mut response = ipc::create_session(&mut sock, username).map_err(|e| e.to_string())?;
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
@@ -909,6 +927,7 @@ fn login_worker(
}
if response.get("type").and_then(|v| v.as_str()) == Some("error") {
log::debug!("Stale session detected, cancelling and retrying");
let _ = ipc::cancel_session(&mut sock);
response = ipc::create_session(&mut sock, username).map_err(|e| e.to_string())?;
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
@@ -927,6 +946,7 @@ fn login_worker(
// Step 2: Send password if auth message received
if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
log::debug!("Sending auth response for {username}");
response =
ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?;
@@ -953,6 +973,7 @@ fn login_worker(
// Step 3: Start session
if response.get("type").and_then(|v| v.as_str()) == Some("success") {
log::debug!("Auth successful, starting session: {exec_cmd}");
let cmd = match split_shell_words(exec_cmd) {
Some(words) if !words.is_empty() => words,
_ => {
@@ -981,6 +1002,7 @@ fn login_worker(
}
if response.get("type").and_then(|v| v.as_str()) == Some("success") {
log::info!("Login successful for {username}");
return Ok(LoginResult::Success {
username: username.to_string(),
});
@@ -1039,8 +1061,10 @@ fn load_last_user_from(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let username = content.trim();
if is_valid_username(username) {
log::debug!("Loaded last user: {username}");
Some(username.to_string())
} else {
log::debug!("Invalid last user in {}", path.display());
None
}
}
@@ -1050,6 +1074,7 @@ fn save_last_user(username: &str) {
}
fn save_last_user_to(path: &Path, username: &str) {
log::debug!("Saving last user: {username}");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
@@ -1072,8 +1097,10 @@ fn load_last_session_from(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let name = content.trim();
if is_valid_session_name(name) {
log::debug!("Loaded last session: {name}");
Some(name.to_string())
} else {
log::debug!("Invalid last session in {}", path.display());
None
}
}
@@ -1101,6 +1128,7 @@ fn save_last_session(username: &str, session_name: &str) {
}
fn save_last_session_to(path: &Path, session_name: &str) {
log::debug!("Saving last session: {session_name}");
use std::os::unix::fs::OpenOptionsExt;
use std::io::Write;
let _ = std::fs::OpenOptions::new()
+8 -3
View File
@@ -127,10 +127,15 @@ pub fn detect_locale() -> String {
.filter(|s| !s.is_empty())
.or_else(|| read_lang_from_conf(Path::new(DEFAULT_LOCALE_CONF)));
match lang {
Some(l) => parse_lang_prefix(&l),
let result = match lang {
Some(ref l) => parse_lang_prefix(l),
None => "en".to_string(),
}
};
log::debug!("Detected locale: {result} (source: {})", match lang {
Some(_) => "LANG env or locale.conf",
None => "default",
});
result
}
/// Return the string table for the given locale, defaulting to English.
+5
View File
@@ -84,6 +84,9 @@ pub fn send_message(
return Err(IpcError::PayloadTooLarge(payload.len()));
}
let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("unknown");
log::debug!("IPC send: type={msg_type}, size={} bytes", payload.len());
let header = (payload.len() as u32).to_le_bytes();
stream.write_all(&header)?;
stream.write_all(&payload)?;
@@ -101,6 +104,8 @@ pub fn recv_message(stream: &mut UnixStream) -> Result<serde_json::Value, IpcErr
let payload = recv_payload(stream, length)?;
let value: serde_json::Value = serde_json::from_slice(&payload)?;
let msg_type = value.get("type").and_then(|v| v.as_str()).unwrap_or("unknown");
log::debug!("IPC recv: type={msg_type}, size={length} bytes");
Ok(value)
}
+9 -21
View File
@@ -13,8 +13,6 @@ use gdk4 as gdk;
use gtk4::prelude::*;
use gtk4::{self as gtk, gio};
use gtk4_layer_shell::LayerShell;
use std::path::PathBuf;
fn load_css(display: &gdk::Display) {
let css_provider = gtk::CssProvider::new();
css_provider.load_from_resource("/dev/moonarch/moongreet/style.css");
@@ -48,13 +46,16 @@ fn activate(app: &gtk::Application) {
}
};
log::debug!("Display: {:?}", display);
load_css(&display);
// Load config and resolve wallpaper
let config = config::load_config(None);
let bg_path = config::resolve_background_path(&config);
log::debug!("Background path: {}", bg_path.display());
let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err();
log::debug!("Layer shell: {use_layer_shell}");
// Main greeter window (login UI) — compositor picks focused monitor
let greeter_window = greeter::create_greeter_window(&bg_path, &config, app);
@@ -66,6 +67,7 @@ fn activate(app: &gtk::Application) {
// Wallpaper-only windows on all monitors (only with layer shell)
if use_layer_shell {
let monitors = display.monitors();
log::debug!("Monitor count: {}", monitors.n_items());
for i in 0..monitors.n_items() {
if let Some(monitor) = monitors
.item(i)
@@ -81,25 +83,11 @@ fn activate(app: &gtk::Application) {
}
fn setup_logging() {
let mut builder = env_logger::Builder::from_default_env();
builder.filter_level(log::LevelFilter::Info);
// Try file logging to /var/cache/moongreet/ — fall back to stderr
let log_dir = PathBuf::from("/var/cache/moongreet");
if log_dir.is_dir() {
let log_file = log_dir.join("moongreet.log");
use std::os::unix::fs::OpenOptionsExt;
if let Ok(file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.mode(0o640)
.open(&log_file)
{
builder.target(env_logger::Target::Pipe(Box::new(file)));
}
}
builder.init();
systemd_journal_logger::JournalLog::new()
.unwrap()
.install()
.unwrap();
log::set_max_level(log::LevelFilter::Debug);
}
fn main() {
+4
View File
@@ -23,6 +23,7 @@ impl std::error::Error for PowerError {}
/// Run a command and return a PowerError on failure.
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
log::debug!("Power action: {action} ({program} {args:?})");
let child = Command::new(program)
.args(args)
.spawn()
@@ -38,6 +39,9 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
message: e.to_string(),
})?;
if output.status.success() {
log::debug!("Power action {action} completed successfully");
}
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PowerError::CommandFailed {
+16 -3
View File
@@ -46,8 +46,17 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
}
}
let name = name.filter(|s| !s.is_empty())?;
let exec_cmd = exec_cmd.filter(|s| !s.is_empty())?;
let name = name.filter(|s| !s.is_empty());
let exec_cmd = exec_cmd.filter(|s| !s.is_empty());
if name.is_none() || exec_cmd.is_none() {
log::debug!("Skipping {}: missing Name={} Exec={}", path.display(),
name.is_some(), exec_cmd.is_some());
return None;
}
let name = name?;
let exec_cmd = exec_cmd?;
Some(Session {
name,
@@ -74,7 +83,10 @@ pub fn get_sessions(
for (dirs, session_type) in [(wayland, "wayland"), (xsession, "x11")] {
for directory in dirs {
let entries = match fs::read_dir(directory) {
Ok(e) => e,
Ok(e) => {
log::debug!("Scanning session directory: {}", directory.display());
e
}
Err(_) => continue,
};
@@ -93,6 +105,7 @@ pub fn get_sessions(
}
}
log::debug!("Found {} session(s)", sessions.len());
sessions
}
+20 -9
View File
@@ -46,7 +46,10 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return Vec::new(),
Err(e) => {
log::warn!("Failed to read passwd file {}: {e}", path.display());
return Vec::new();
}
};
let mut users = Vec::new();
@@ -88,6 +91,7 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
});
}
log::debug!("Found {} login user(s)", users.len());
users
}
@@ -106,21 +110,28 @@ pub fn get_avatar_path_with(
// AccountsService icon takes priority
if accountsservice_dir.exists() {
let icon = accountsservice_dir.join(username);
if let Ok(meta) = icon.symlink_metadata()
&& !meta.file_type().is_symlink()
{
return Some(icon);
if let Ok(meta) = icon.symlink_metadata() {
if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar for {username}: {}", icon.display());
} else {
log::debug!("Avatar for {username}: AccountsService {}", icon.display());
return Some(icon);
}
}
}
// ~/.face fallback
let face = home.join(".face");
if let Ok(meta) = face.symlink_metadata()
&& !meta.file_type().is_symlink()
{
return Some(face);
if let Ok(meta) = face.symlink_metadata() {
if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar for {username}: {}", face.display());
} else {
log::debug!("Avatar for {username}: ~/.face {}", face.display());
return Some(face);
}
}
log::debug!("No avatar found for {username}");
None
}