From 250f3b47f61d982ddbebb9639626fa6e548704d0 Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:44:42 +0300 Subject: [PATCH] rfct: Split up session loading for readability Now there are distinct steps to session loading. Regex was replaced with a desktop file parsing library. This makes the code easier to comprehend and allows for better flexibility in the future (rharish101/ReGreet#67). --- Cargo.lock | 206 +++++---------- Cargo.toml | 7 +- src/constants.rs | 6 - src/gui/component/auth_ui.rs | 13 +- src/gui/component/greetd_controls.rs | 1 - src/gui/component/mod.rs | 4 +- src/main.rs | 43 ++- src/sysutil.rs | 382 ++++++++++++++------------- 8 files changed, 305 insertions(+), 357 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f756123..013b82b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,27 +23,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -114,15 +93,26 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -191,9 +181,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.15" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "shlex", ] @@ -228,9 +218,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.16" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -238,9 +228,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -257,7 +247,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -397,6 +387,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "freedesktop_entry_parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" +dependencies = [ + "nom", + "thiserror", +] + [[package]] name = "futures" version = "0.3.30" @@ -453,7 +453,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -644,12 +644,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "gobject-sys" version = "0.16.3" @@ -791,15 +785,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -916,15 +901,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "lru" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" -dependencies = [ - "hashbrown 0.13.2", -] - [[package]] name = "memchr" version = "2.7.4" @@ -940,6 +916,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -979,6 +961,16 @@ dependencies = [ "getrandom", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom8" version = "0.2.0" @@ -1086,7 +1078,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1175,50 +1167,19 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - [[package]] name = "regreet" version = "0.1.1" dependencies = [ - "chrono", + "async-recursion", "clap", "const_format", "derivative", "file-rotate", - "glob", + "freedesktop_entry_parser", "greetd_ipc", "gtk4", - "lru", "pwd", - "regex", "relm4", "serde", "serde-tuple-vec-map", @@ -1230,7 +1191,6 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", - "tracker", ] [[package]] @@ -1320,14 +1280,14 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", "memchr", @@ -1412,9 +1372,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1458,7 +1418,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1469,7 +1429,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "test-case-core", ] @@ -1490,7 +1450,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1560,7 +1520,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1673,7 +1633,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1712,26 +1672,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tracker" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5c98457ff700aaeefcd4a4a492096e78a2af1dd8523c66e94a3adb0fdbd415" -dependencies = [ - "tracker-macros", -] - -[[package]] -name = "tracker-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1796,7 +1736,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -1818,7 +1758,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1950,23 +1890,3 @@ checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] diff --git a/Cargo.toml b/Cargo.toml index 94048f5..22516b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,17 +14,15 @@ license = "GPL-3.0-or-later" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { version = "0.4.22", default-features = false } +async-recursion = "1.1.1" clap = { version = "4.1.4", features = ["derive"] } const_format = "0.2.26" derivative = "2.2.0" file-rotate = "0.7.2" -glob = "0.3.0" +freedesktop_entry_parser = "1.3.0" greetd_ipc = { version = "0.9.0", features = ["tokio-codec"] } gtk4 = "0.5" -lru = "0.9.0" pwd = "1.4.0" -regex = "1.7.1" relm4 = "0.5.0" serde = { version = "1.0.142", features = ["derive"] } serde-tuple-vec-map = "1.0.1" @@ -35,7 +33,6 @@ toml = "0.6.0" tracing = "0.1.37" tracing-appender = "0.2.2" tracing-subscriber = { version = "0.3.16", features = ["local-time"] } -tracker = "0.2.0" [features] gtk4_8 = ["gtk4/v4_8"] diff --git a/src/constants.rs b/src/constants.rs index ef442d6..34ee2aa 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -48,9 +48,3 @@ pub const POWEROFF_CMD: &str = env_or!("POWEROFF_CMD", "poweroff"); /// Default greeting message pub const GREETING_MSG: &str = "Welcome back!"; - -/// Directories separated by `:`, containing desktop files for X11/Wayland sessions -pub const SESSION_DIRS: &str = env_or!( - "SESSION_DIRS", - "/usr/share/xsessions:/usr/share/wayland-sessions" -); diff --git a/src/gui/component/auth_ui.rs b/src/gui/component/auth_ui.rs index 21950aa..9244d62 100644 --- a/src/gui/component/auth_ui.rs +++ b/src/gui/component/auth_ui.rs @@ -9,6 +9,7 @@ use tracing::error; use crate::greetd::Greetd; use crate::gui::component::greetd_controls::GreetdControlsInit; use crate::gui::component::{GreetdControlsOutput, SelectorInit, SelectorMsg, SelectorOutput}; +use crate::sysutil::SessionInfo; use super::greetd_controls::{GreetdControls, GreetdState}; use super::{EntryOrDropDown, GreetdControlsMsg, Selector, SelectorOption}; @@ -23,7 +24,7 @@ where { pub initial_user: String, pub users: HashMap, - pub sessions: HashMap>, + pub sessions: HashMap, pub last_user_session_cache: HashMap, @@ -157,9 +158,9 @@ where .launch(SelectorInit { entry_placeholder: "Session command".to_string(), options: sessions - .keys() - .map(|name| SelectorOption { - id: name.clone(), + .iter() + .map(|(xdg_id, SessionInfo { name, .. })| SelectorOption { + id: xdg_id.clone(), text: name.clone(), }) .collect(), @@ -172,7 +173,9 @@ where let SelectorOutput::CurrentSelection(entry) = output; let cmdline = match entry { EntryOrDropDown::Entry(cmdline) => shlex::split(&cmdline), - EntryOrDropDown::DropDown(id) => sessions.get(&id).cloned(), + EntryOrDropDown::DropDown(id) => sessions + .get(&id) + .map(|SessionInfo { command, .. }| command.clone()), }; Self::Input::SessionChanged(cmdline) diff --git a/src/gui/component/greetd_controls.rs b/src/gui/component/greetd_controls.rs index ac289b6..b4ff7a8 100644 --- a/src/gui/component/greetd_controls.rs +++ b/src/gui/component/greetd_controls.rs @@ -319,7 +319,6 @@ where GreetdState::NotCreated(_) => gtk::Box { set_halign: gtk::Align::End, #[template] LoginButton { - #[watch] grab_focus: (), connect_clicked => GreetdControlsMsg::AdvanceAuthentication, diff --git a/src/gui/component/mod.rs b/src/gui/component/mod.rs index 85b5091..06521dc 100644 --- a/src/gui/component/mod.rs +++ b/src/gui/component/mod.rs @@ -10,7 +10,7 @@ use relm4::prelude::*; #[cfg(feature = "gtk4_8")] use crate::config::BgFit; -use crate::greetd::Greetd; +use crate::{greetd::Greetd, sysutil::SessionInfo}; mod selector; pub use selector::*; @@ -30,7 +30,7 @@ where Client: Greetd, { pub users: HashMap, - pub sessions: HashMap>, + pub sessions: HashMap, pub initial_user: String, pub last_user_session_cache: HashMap, diff --git a/src/main.rs b/src/main.rs index 630c17b..c663289 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,17 +11,18 @@ mod gui; mod sysutil; pub mod tomlutils; +use std::collections::HashMap; use std::fs::{create_dir_all, OpenOptions}; use std::io::{Result as IoResult, Write}; use std::path::{Path, PathBuf}; -use cache::Cache; +use cache::{Cache, SessionIdOrCmdline}; use clap::{Parser, ValueEnum}; use constants::CACHE_PATH; use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate}; use greetd::MockGreetd; use gui::component::{App, AppInit, EntryOrDropDown, GreetdState}; -use sysutil::{SystemUsersAndSessions, User}; +use sysutil::SystemUsersAndSessions; use tracing::subscriber::set_global_default; use tracing::warn; use tracing_appender::{non_blocking, non_blocking::WorkerGuard}; @@ -31,6 +32,9 @@ use tracing_subscriber::{ use crate::constants::{APP_ID, CONFIG_PATH, CSS_PATH, LOG_PATH}; +#[macro_use] +extern crate async_recursion; + #[cfg(test)] #[macro_use] extern crate test_case; @@ -77,20 +81,29 @@ struct Args { } fn main() { - let args = Args::parse(); + let Args { + logs, + log_level, + verbose, + config, + style, + demo, + } = Args::parse(); // Keep the guard alive till the end of the function, since logging depends on this. - let _guard = init_logging(&args.logs, &args.log_level, args.verbose); + let _guard = init_logging(&logs, &log_level, verbose); // TODO: Is there a better way? we have to not start tokio until OffsetTime is initialized. // TODO: What on earth is this let binding? - let (cache, (SystemUsersAndSessions { users, sessions }, non_fatal_errors)) = + let (cache, SystemUsersAndSessions { users, sessions }) = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(async { - let (cache, users) = - tokio::join!(Cache::load(CACHE_PATH), SystemUsersAndSessions::load()); + let (cache, users) = tokio::join!( + Cache::load(CACHE_PATH), + SystemUsersAndSessions::load(Vec::new()) + ); ( cache.unwrap_or_else(|err| { @@ -106,17 +119,15 @@ fn main() { .and_then(|user| users.contains_key(user).then_some(user.to_string())) .unwrap_or_else(|| users.keys().next().cloned().unwrap_or_default()); // TODO: Make Init accept an option - let last_user_session_cache = cache + let mut last_user_session_cache: HashMap<_, _> = cache .user_to_last_sess .into_iter() .filter_map(|(username, session)| match session { - cache::SessionIdOrCmdline::ID(id) => sessions + SessionIdOrCmdline::ID(id) => sessions .contains_key(&id) .then_some((username, EntryOrDropDown::DropDown(id))), - cache::SessionIdOrCmdline::Command(cmd) => { - Some((username, EntryOrDropDown::Entry(cmd))) - } + SessionIdOrCmdline::Command(cmd) => Some((username, EntryOrDropDown::Entry(cmd))), }) .collect(); @@ -124,7 +135,13 @@ fn main() { let users = users .into_iter() - .map(|(sys, User { full_name, .. })| (sys, full_name)) + .map(|(sys, user)| { + if sessions.is_empty() { + last_user_session_cache + .insert(sys.clone(), EntryOrDropDown::Entry(user.shell().to_owned())); + } + (sys, user.full_name) + }) .collect(); app.run::>(AppInit { diff --git a/src/sysutil.rs b/src/sysutil.rs index dd5aa16..6b48f9c 100644 --- a/src/sysutil.rs +++ b/src/sysutil.rs @@ -4,40 +4,30 @@ //! Helper for system utilities like users and sessions -use std::collections::HashMap; -use std::collections::HashSet; -use std::env; -use std::fs::read; +use std::collections::hash_map; +use std::ffi::OsStr; use std::io; -use std::io::Result as IOResult; -use std::path::Path; -use std::str::from_utf8; +use std::path::{Path, PathBuf}; +use std::{collections::HashMap, env}; -use glob::glob; +use freedesktop_entry_parser::Entry; use pwd::Passwd; -use regex::Regex; -use relm4::spawn_blocking; use thiserror::Error; -use tokio::fs::read_to_string; -use tracing::{debug, info, warn}; - -use crate::constants::SESSION_DIRS; - -const XDG_DIR_ENV_VAR: &'static str = "XDG_DATA_DIRS"; - -type SessionMap = HashMap>; +use tokio::fs::{read, read_dir, read_to_string}; +use tokio::task::spawn_blocking; +use tracing::{debug, warn}; /// Stores info of all regular users and sessions pub struct SystemUsersAndSessions { /// Maps from system usename to [`User`]. pub users: HashMap, - /// Maps a session's full name to its command - pub sessions: SessionMap, + /// Maps a session's xdg desktop file id to [`SessionInfo`]. + pub sessions: HashMap, } pub struct User { pub full_name: String, - pub login_shell: Option, + login_shell: Option, } impl User { @@ -49,34 +39,33 @@ impl User { } impl SystemUsersAndSessions { - pub async fn load() -> IOResult<(Self, Vec)> { - let mut non_fatal_errors = Vec::new(); + const SESSION_DIRS_ENV: &'static str = "XDG_DATA_DIRS"; + const SESSION_DIRS_DEFAULT: &'static str = "/usr/local/share/:/usr/share/"; + pub async fn load(x11_prefix: Vec) -> io::Result { let uid_limit = match read_to_string(NormalUser::PATH).await { Ok(text) => spawn_blocking(move || NormalUser::parse_login_defs(&text)) .await .unwrap(), Err(e) => { - let e = NonFatalError::UidLimitRead(e); warn!("{e}"); - non_fatal_errors.push(e); NormalUser::default() } }; - let users = Self::init_users(uid_limit)?; + let (users, sessions) = tokio::join!( + spawn_blocking(move || Self::init_users(uid_limit)), + Self::init_sessions(x11_prefix) + ); - Ok(( - Self { - users, - sessions: Self::init_sessions()?, - }, - non_fatal_errors, - )) + let users = users.unwrap().unwrap_or_default(); + let sessions = sessions.unwrap_or_default(); + + Ok(Self { users, sessions }) } - fn init_users(uid_limit: NormalUser) -> IOResult> { + fn init_users(uid_limit: NormalUser) -> io::Result> { debug!("{uid_limit:?}"); let mut users = HashMap::new(); @@ -122,169 +111,156 @@ impl SystemUsersAndSessions { Ok(users) } - /// Get available X11 and Wayland sessions. + /// Get the avaliable graphical X11 and Wayland sessions. These are retrieved from [`Self::SESSION_DIRS_ENV`] + /// (defaults to [`Self::SESSION_DIRS_DEFAULT`]). For each directory from the env, scans `/xsessions` and + /// `/wayland-sessions` (Wayland takes priority if an x11 desktop file has the same ID). The resulting hashmap maps + /// the desktop file ID to the information about that session file. /// - /// These are defined as either X11 or Wayland session desktop files stored in specific - /// directories. - fn init_sessions() -> IOResult { - let mut found_session_names = HashSet::new(); - let mut sessions = HashMap::new(); - - // Use the XDG spec if available, else use the one that's compiled. - // The XDG env var can change after compilation in some distros like NixOS. - let session_dirs = if let Ok(sess_parent_dirs) = env::var(XDG_DIR_ENV_VAR) { - debug!("Found XDG env var {XDG_DIR_ENV_VAR}: {sess_parent_dirs}"); - match sess_parent_dirs - .split(':') - .map(|parent_dir| format!("{parent_dir}/xsessions:{parent_dir}/wayland-sessions")) - .reduce(|a, b| a + ":" + &b) - { - None => SESSION_DIRS.to_string(), - Some(dirs) => dirs, - } - } else { - SESSION_DIRS.to_string() - }; + /// For each X11 session, `x11_prefix` is added. + async fn init_sessions(x11_prefix: Vec) -> io::Result> { + let session_dirs = env::var(Self::SESSION_DIRS_ENV) + .into_iter() + .find(|s| !s.is_empty()) + .unwrap_or_else(|| Self::SESSION_DIRS_DEFAULT.to_string()); + + let (x11_dirs, wayland_dirs): (Vec<_>, Vec<_>) = session_dirs + .split(':') + .map(|dir| { + ( + PathBuf::from(dir).join("xsessions"), + PathBuf::from(dir).join("wayland-sessions"), + ) + }) + .unzip(); + + let (x11_entries, wayland_entries) = tokio::join!( + Self::get_desktop_entries_in_dirs(x11_dirs), + Self::get_desktop_entries_in_dirs(wayland_dirs), + ); + + let mut x11_entries = x11_entries.unwrap_or_default(); + let wayland_entries = wayland_entries.unwrap_or_default(); + + x11_entries.iter_mut().for_each(|(_, v)| { + let mut command = x11_prefix.clone(); + command.append(&mut v.command); + + v.command = command; + }); + + x11_entries.extend(wayland_entries); + Ok(x11_entries) + } + + /// Given a list of directories (in order as they appear in the env var) scan those dirs recursively. + /// For each `*.desktop` file, process it and place into the hash map. + /// However if a desktop file's id ([as defined by the XDG spec](https://specifications.freedesktop.org/desktop-entry-spec/latest/file-naming.html#desktop-file-id)) + /// is already processed, skip the identical id. + async fn get_desktop_entries_in_dirs

( + dirs: Vec

, + ) -> Result, DesktopFileError> + where + P: AsRef + std::marker::Send + 'static + std::marker::Sync, + { + let mut dirs_of_files = Vec::new(); - for sess_dir in session_dirs.split(':') { - let sess_parent_dir = if let Some(sess_parent_dir) = Path::new(sess_dir).parent() { - sess_parent_dir - } else { - warn!("Session directory does not have a parent: {sess_dir}"); + for dir in dirs { + let Ok(files) = Self::recursively_find_desktop_files(&dir).await else { + // Try to collect as many entries that yield Ok without early return continue; }; - debug!("Checking session directory: {sess_dir}"); - // Iterate over all '.desktop' files. - for glob_path in glob(&format!("{sess_dir}/*.desktop")) - .expect("Invalid glob pattern for session desktop files") - { - let path = match glob_path { - Ok(path) => path, - Err(err) => { - warn!("Error when globbing: {err}"); - continue; - } - }; - info!("Now scanning session file: {}", path.display()); - let contents = read(&path)?; - let text = from_utf8(contents.as_slice()).unwrap_or_else(|err| { - panic!("Session file '{}' is not UTF-8: {}", path.display(), err) - }); + dirs_of_files.push((dir, files)); + } - let fname_and_type = match path.strip_prefix(sess_parent_dir) { - Ok(fname_and_type) => fname_and_type.to_owned(), - Err(err) => { - warn!("Error with file name: {err}"); - continue; - } - }; + let mut map = HashMap::new(); + for (id, file) in dirs_of_files.into_iter().flat_map(|(base, files)| { + files + .into_iter() + .map(move |file| (Self::desktop_file_id(&base, &file), file)) + }) { + let map_entry = map.entry(id); - if found_session_names.contains(&fname_and_type) { - debug!( - "{fname_and_type:?} was already found elsewhere, skipping {}", - path.display() - ); + if matches!(map_entry, hash_map::Entry::Vacant(_)) { + let Ok(Some(entry)) = SessionInfo::load(file).await else { continue; }; - // The session launch command is specified as: Exec=command arg1 arg2... - let cmd_regex = - Regex::new(r"Exec=(.*)").expect("Invalid regex for session command"); - // The session name is specified as: Name=My Session - let name_regex = Regex::new(r"Name=(.*)").expect("Invalid regex for session name"); - - // Hiding could be either as Hidden=true or NoDisplay=true - let hidden_regex = Regex::new(r"Hidden=(.*)").expect("Invalid regex for hidden"); - let no_display_regex = - Regex::new(r"NoDisplay=(.*)").expect("Invalid regex for no display"); - - let hidden: bool = if let Some(hidden_str) = hidden_regex - .captures(text) - .and_then(|capture| capture.get(1)) - { - hidden_str.as_str().parse().unwrap_or(false) - } else { - false - }; + // Cannot use or_insert_with because of async. + map_entry.or_insert(entry); + } + } - let no_display: bool = if let Some(no_display_str) = no_display_regex - .captures(text) - .and_then(|capture| capture.get(1)) - { - no_display_str.as_str().parse().unwrap_or(false) - } else { - false - }; + Ok(map) + } - if hidden | no_display { - found_session_names.insert(fname_and_type); - continue; - }; + /// Iterates over the directory and yields everything that has a `.desktop` extension. + /// If the entry is a directory, recurses into it and appends all the files there to the list. + #[async_recursion] + async fn recursively_find_desktop_files

(dir: P) -> io::Result> + where + P: AsRef + std::marker::Send, + { + // You will see a lot of `let Ok else continue` in this function. + // This is because we try to ignore as many errors as possible. + // Otherwise even a single permission error can cause the session list to be empty. + let mut ls = read_dir(dir).await?; + + let mut files = Vec::new(); + while let Some(entry) = ls.next_entry().await? { + let Ok(file_type) = entry.file_type().await else { + continue; + }; - // Parse the desktop file to get the session command. - let cmd = if let Some(cmd_str) = - cmd_regex.captures(text).and_then(|capture| capture.get(1)) - { - if let Some(cmd) = shlex::split(cmd_str.as_str()) { - cmd - } else { - warn!( - "Couldn't split command of '{}' into arguments: {}", - path.display(), - cmd_str.as_str() - ); - // Skip the desktop file, since a missing command means that we can't - // use it. - continue; - } - } else { - warn!("No command found for session: {}", path.display()); - // Skip the desktop file, since a missing command means that we can't use it. + if file_type.is_dir() { + let Ok(mut recursed) = Self::recursively_find_desktop_files(entry.path()).await + else { continue; }; - // Get the full name of this session. - let name = if let Some(name) = - name_regex.captures(text).and_then(|capture| capture.get(1)) - { - debug!( - "Found name '{}' for session '{}' with command '{:?}'", - name.as_str(), - path.display(), - cmd - ); - name.as_str() - } else if let Some(stem) = path.file_stem() { - // Get the stem of the filename of this desktop file. - // This is used as backup, in case the file name doesn't exist. - if let Some(stem) = stem.to_str() { - debug!( - "Using file stem '{stem}', since no name was found for session: {}", - path.display() - ); - stem - } else { - warn!("Non-UTF-8 file stem in session file: {}", path.display()); - // No way to display this session name, so just skip it. - continue; - } - } else { - warn!("No file stem found for session: {}", path.display()); - // No file stem implies no file name, which shouldn't happen. - // Since there's no full name nor file stem, just skip this anomalous - // session. - continue; - }; - found_session_names.insert(fname_and_type); - sessions.insert(name.to_string(), cmd); + files.append(&mut recursed); + + continue; } + + if !entry + .path() + .extension() + .map(|e| e.to_string_lossy() == "desktop") + .unwrap_or(false) + { + continue; + }; + + files.push(entry.path()); } - Ok(sessions) + Ok(files) + } + + /// Returns a dektop file id given a base directory and a path to the desktop file. The algorithm is described in + /// the [XDG spec: Desktop File ID](https://specifications.freedesktop.org/desktop-entry-spec/latest/file-naming.html#desktop-file-id). + /// + /// # Panics + /// + /// This function requires that `base` is prefix of `file`. + fn desktop_file_id(base: P1, file: P2) -> String + where + P1: AsRef, + P2: AsRef, + { + let base = base.as_ref(); + let file = file.as_ref(); + + let path = file.strip_prefix(base).unwrap().with_extension(""); + + path.iter() + .map(OsStr::to_string_lossy) + .fold(String::new(), |acc, item| acc + &item) } } +/// Returs input, but the first character is capitalized. fn capitalize(s: &str) -> String { let mut chars = s.chars(); match chars.next() { @@ -385,14 +361,56 @@ impl NormalUser { } } -/// Represents an error that is not considered fatal for loading the user and session information, however the -/// assumptions that the loading process makes may cause unexpected behavior. -/// -/// This should be shown to the user in the UI. +#[derive(Debug)] +pub struct SessionInfo { + /// The displayed name of the session + pub name: String, + /// The command to run when the session starts. + pub command: Vec, +} + +impl SessionInfo { + async fn load

(path: P) -> Result, DesktopFileError> + where + P: AsRef, + { + let skip = Ok(None); + + let contents = read(path).await?; + let desktop_file = Entry::parse(contents)?; + let entry = desktop_file.section("Desktop Entry"); + + if let Some("true") = entry.attr("Hidden") { + return skip; + } + + if let Some("true") = entry.attr("NoDisplay") { + return skip; + } + + let Some(name) = entry.attr("Name") else { + return skip; + }; + + let Some(exec) = entry.attr("Exec") else { + return skip; + }; + + Ok(shlex::split(exec).map(|command| Self { + name: name.to_string(), + command, + })) + } +} + +/// Represents errors from loading the xdg desktop files. #[derive(Error, Debug)] -pub enum NonFatalError { - #[error("Failed to read UID limits defined in '{}': {0}", NormalUser::PATH)] - UidLimitRead(#[from] io::Error), +pub enum DesktopFileError { + #[error("I/O error occured while reading a desktop file: {0}")] + IO(#[from] io::Error), + + #[error("XDG desktop file parsing error: {0}")] + Xdg(#[from] freedesktop_entry_parser::ParseError), } #[cfg(test)]