diff --git a/src/dbus.rs b/src/dbus.rs index 7cc6ee62..d1b618ef 100644 --- a/src/dbus.rs +++ b/src/dbus.rs @@ -255,3 +255,31 @@ pub mod user { fn xsession(&self) -> zbus::Result; } } + +pub mod home1_manager { + //! # D-Bus interface proxy for: `org.freedesktop.home1.Manager` + use zbus::proxy; + + #[proxy( + interface = "org.freedesktop.home1.Manager", + default_service = "org.freedesktop.home1", + default_path = "/org/freedesktop/home1" + )] + pub trait Manager { + /// ListHomes method + fn list_homes( + &self, + ) -> zbus::Result< + Vec<( + String, + u32, + String, + u32, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + )>, + >; + } +} diff --git a/src/user_session_page.rs b/src/user_session_page.rs index cacb60e3..405a4084 100644 --- a/src/user_session_page.rs +++ b/src/user_session_page.rs @@ -46,6 +46,8 @@ impl UserSessionPage { mod imp { use crate::dbus::accounts::AccountsProxy; + use crate::dbus::home1_manager::ManagerProxy; + use crate::dbus::user::UserProxy; use crate::session_object::SessionObject; use crate::shell::Shell; use crate::user::User; @@ -65,6 +67,8 @@ mod imp { use libhandy::ActionRow; use std::cell::{Cell, OnceCell}; use std::sync::OnceLock; + use std::time::Duration; + use zbus::zvariant::OwnedObjectPath; #[derive(CompositeTemplate, Default, Properties)] #[properties(wrapper_type = super::UserSessionPage)] @@ -170,9 +174,9 @@ mod imp { glib::spawn_future_local(clone!(@weak self as this, @strong last_user => async move { let accounts_proxy = AccountsProxy::new(&conn).await.unwrap(); - for path in accounts_proxy.list_cached_users().await.unwrap() { - users.append(&User::new(conn.clone(), path.into())); - } + sync_cached_accounts_users(&conn, &accounts_proxy, &users).await; + cache_homed_users(&accounts_proxy, &conn).await; + sync_cached_accounts_users(&conn, &accounts_proxy, &users).await; // The initial user list has been populated. Select the first item in the list to // ensure something is selected. @@ -188,14 +192,15 @@ mod imp { this.obj().set_ready(true); - let mut added_stream = accounts_proxy.receive_user_added().await.unwrap(); - let mut deleted_stream = accounts_proxy.receive_user_deleted().await.unwrap(); + let mut added_stream = accounts_proxy.receive_user_added().await.unwrap().fuse(); + let mut deleted_stream = accounts_proxy.receive_user_deleted().await.unwrap().fuse(); + let mut homed_tick = glib::interval_stream(Duration::from_secs(2)).fuse(); loop { select! { added = added_stream.next() => if let Some(added) = added { if let Some(path) = added.args().ok().map(|v| v.user) { - users.append(&User::new(conn.clone(), path)); + append_user_if_visible(&conn, &users, path.into()).await; } }, deleted = deleted_stream.next() => if let Some(deleted) = deleted { @@ -208,6 +213,10 @@ mod imp { } } }, + _ = homed_tick.next() => { + cache_homed_users(&accounts_proxy, &conn).await; + sync_cached_accounts_users(&conn, &accounts_proxy, &users).await; + }, } } })); @@ -222,4 +231,60 @@ mod imp { impl WidgetImpl for UserSessionPage {} impl ContainerImpl for UserSessionPage {} impl BoxImpl for UserSessionPage {} + + async fn sync_cached_accounts_users( + conn: &zbus::Connection, + accounts_proxy: &AccountsProxy<'_>, + users: &ListStore, + ) { + if let Ok(paths) = accounts_proxy.list_cached_users().await { + for path in paths { + append_user_if_visible(conn, users, path).await; + } + } + } + + async fn cache_homed_users(accounts_proxy: &AccountsProxy<'_>, conn: &zbus::Connection) { + let Ok(homed_proxy) = ManagerProxy::new(conn).await else { + return; + }; + + if let Ok(homes) = homed_proxy.list_homes().await { + for (username, _, _, _, _, _, _, _) in homes { + if let Err(err) = accounts_proxy.cache_user(&username).await { + glib::warn!("failed to cache homed user {}: {}", username, err); + } + } + } + } + + async fn append_user_if_visible( + conn: &zbus::Connection, + users: &ListStore, + path: OwnedObjectPath, + ) { + if !user_path_is_visible(conn, &path).await { + return; + } + + for user in users.iter::().flatten() { + if user.path() == path.as_str() { + return; + } + } + + users.append(&User::new(conn.clone(), path.into())); + } + + async fn user_path_is_visible(conn: &zbus::Connection, path: &OwnedObjectPath) -> bool { + let Ok(user_proxy) = UserProxy::builder(conn).path(path).unwrap().build().await else { + return false; + }; + + let local_account = user_proxy.local_account().await.unwrap_or(true); + let system_account = user_proxy.system_account().await.unwrap_or(false); + let username = user_proxy.user_name().await.unwrap_or_default(); + + local_account && !system_account && username != "greetd" + } } diff --git a/tests/common/dbus.rs b/tests/common/dbus.rs index 8a815222..4761eb88 100644 --- a/tests/common/dbus.rs +++ b/tests/common/dbus.rs @@ -1,7 +1,9 @@ use crate::common::SupervisedChild; use anyhow::Context; +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::Stdio; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use zbus::zvariant::ObjectPath; @@ -54,32 +56,79 @@ pub fn dbus_daemon(kind: &str, tmpdir: &Path) -> SupervisedChild { SupervisedChild::new("dbus-daemon", child) } +#[derive(Clone)] +pub struct AccountsFixtureOptions { + pub num_users: Option, + pub users: Vec, + pub cached_users: Option>, + pub homed_users: Vec, +} + +impl Default for AccountsFixtureOptions { + fn default() -> Self { + Self { + num_users: None, + users: vec![ + UserFixture::new("Phoshi", "phoshi", "phoshi.png", true, false), + UserFixture::new("Guido", "agx", "guido.png", true, false), + UserFixture::new("Sam", "samcday", "samcday.jpeg", true, false), + ], + cached_users: None, + homed_users: vec![], + } + } +} + struct AccountsFixture { - num_users: Option, + state: Arc>, } -struct UserFixture { + +#[derive(Clone)] +pub struct UserFixture { name: String, username: String, icon_file: String, + local_account: bool, + system_account: bool, +} + +#[derive(Default)] +struct FixtureState { + users_by_name: BTreeMap, + cached_users: Vec, + homed_users: Vec, } #[zbus::interface(name = "org.freedesktop.Accounts")] impl AccountsFixture { async fn list_cached_users(&self) -> Vec> { - let mut users = vec![ - ObjectPath::from_static_str_unchecked("/org/freedesktop/Accounts/phoshi"), - ObjectPath::from_static_str_unchecked("/org/freedesktop/Accounts/agx"), - ObjectPath::from_static_str_unchecked("/org/freedesktop/Accounts/sam"), - ]; - if let Some(num_users) = self.num_users { - users.truncate(num_users as _); + let state = self.state.lock().unwrap(); + state + .cached_users + .iter() + .map(|name| ObjectPath::try_from(format!("/org/freedesktop/Accounts/{name}")).unwrap()) + .collect::>() + } + + async fn cache_user(&self, name: &str) -> zbus::fdo::Result> { + let mut state = self.state.lock().unwrap(); + if !state.cached_users.contains(&name.to_string()) && state.users_by_name.contains_key(name) + { + state.cached_users.push(name.to_string()); } - users + + Ok(ObjectPath::try_from(format!("/org/freedesktop/Accounts/{name}")).unwrap()) } } impl UserFixture { - fn new(name: &str, username: &str, icon_file: &str) -> Self { + pub fn new( + name: &str, + username: &str, + icon_file: &str, + local_account: bool, + system_account: bool, + ) -> Self { Self { name: name.into(), username: username.into(), @@ -88,6 +137,8 @@ impl UserFixture { .join(icon_file) .display() .to_string(), + local_account, + system_account, } } } @@ -106,44 +157,132 @@ impl UserFixture { async fn icon_file(&self) -> &str { &self.icon_file } + #[zbus(property)] + async fn local_account(&self) -> bool { + self.local_account + } + #[zbus(property)] + async fn system_account(&self) -> bool { + self.system_account + } +} + +struct Home1ManagerFixture { + state: Arc>, +} + +#[zbus::interface(name = "org.freedesktop.home1.Manager")] +impl Home1ManagerFixture { + async fn list_homes( + &self, + ) -> Vec<( + String, + u32, + String, + u32, + String, + String, + String, + ObjectPath<'_>, + )> { + let state = self.state.lock().unwrap(); + state + .homed_users + .iter() + .map(|name| { + ( + name.clone(), + 1000, + "active".to_string(), + 1000, + name.clone(), + format!("/home/{name}"), + "/bin/sh".to_string(), + ObjectPath::from_static_str_unchecked("/org/freedesktop/home1/home"), + ) + }) + .collect() + } } pub async fn run_accounts_fixture( connection: zbus::Connection, num_users: Option, ) -> anyhow::Result<()> { - connection - .object_server() - .at("/org/freedesktop/Accounts", AccountsFixture { num_users }) - .await - .context("failed to serve org.freedesktop.Accounts")?; - connection - .object_server() - .at( - "/org/freedesktop/Accounts/agx", - UserFixture::new("Guido", "agx", "guido.png"), - ) - .await - .context("failed to serve org.freedesktop.Accounts.User")?; + run_accounts_fixture_with_options( + connection, + AccountsFixtureOptions { + num_users, + ..Default::default() + }, + ) + .await +} + +pub async fn run_accounts_fixture_with_options( + connection: zbus::Connection, + options: AccountsFixtureOptions, +) -> anyhow::Result<()> { + let mut cached_users = options.cached_users.unwrap_or_else(|| { + options + .users + .iter() + .map(|user| user.username.clone()) + .collect() + }); + if let Some(num_users) = options.num_users { + cached_users.truncate(num_users as usize); + } + + let users_by_name = options + .users + .iter() + .map(|user| (user.username.clone(), user.clone())) + .collect(); + + let state = Arc::new(Mutex::new(FixtureState { + users_by_name, + cached_users, + homed_users: options.homed_users, + })); + connection .object_server() .at( - "/org/freedesktop/Accounts/phoshi", - UserFixture::new("Phoshi", "phoshi", "phoshi.png"), + "/org/freedesktop/Accounts", + AccountsFixture { + state: state.clone(), + }, ) .await - .context("failed to serve org.freedesktop.Accounts.User")?; + .context("failed to serve org.freedesktop.Accounts")?; + + for user in options.users { + connection + .object_server() + .at(format!("/org/freedesktop/Accounts/{}", user.username), user) + .await + .context("failed to serve org.freedesktop.Accounts.User")?; + } + connection .object_server() .at( - "/org/freedesktop/Accounts/sam", - UserFixture::new("Sam", "samcday", "samcday.jpeg"), + "/org/freedesktop/home1", + Home1ManagerFixture { + state: state.clone(), + }, ) .await - .context("failed to serve org.freedesktop.Accounts.User")?; + .context("failed to serve org.freedesktop.home1.Manager")?; + connection .request_name("org.freedesktop.Accounts") .await .context("failed to request name")?; + connection + .request_name("org.freedesktop.home1") + .await + .context("failed to request home1 name")?; Ok(()) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 508771a0..8d73ea06 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,6 +2,7 @@ pub mod dbus; pub mod virtual_keyboard; pub mod virtual_pointer; +use crate::common::dbus::AccountsFixtureOptions; use crate::common::virtual_keyboard::VirtualKeyboard; use async_channel::Receiver; use glib::{g_critical, spawn_future_local, JoinHandle, Object}; @@ -91,6 +92,7 @@ impl Test { #[derive(Default)] pub struct TestOptions { pub num_users: Option, + pub accounts_fixture: Option, pub sessions: Option>, pub last_user: Option, pub last_session: Option, @@ -136,12 +138,19 @@ pub fn test_init(options: Option) -> Test { if_settings.set_string("accent-color", "green").unwrap(); let num_users = options.as_ref().and_then(|opts| opts.num_users); + let accounts_fixture = options + .as_ref() + .and_then(|opts| opts.accounts_fixture.clone()) + .unwrap_or(AccountsFixtureOptions { + num_users, + ..Default::default() + }); let (system_dbus_conn, session_dbus_conn) = async_global_executor::block_on(async move { let system = zbus::Connection::system() .await .expect("failed to connect to system bus"); - dbus::run_accounts_fixture(system.clone(), num_users) + dbus::run_accounts_fixture_with_options(system.clone(), accounts_fixture) .await .unwrap(); diff --git a/tests/homed_accounts_bridge.rs b/tests/homed_accounts_bridge.rs new file mode 100644 index 00000000..1bf059f9 --- /dev/null +++ b/tests/homed_accounts_bridge.rs @@ -0,0 +1,66 @@ +pub mod common; + +use gtk::glib; +use gtk::glib::clone; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use libhandy::prelude::ActionRowExt; +use libphosh::prelude::ShellExt; +use phrog::lockscreen::Lockscreen; +use std::time::Duration; + +use common::dbus::{AccountsFixtureOptions, UserFixture}; +use common::*; + +#[test] +fn test_homed_users_are_cached_and_system_users_hidden() { + let mut test = test_init(Some(TestOptions { + accounts_fixture: Some(AccountsFixtureOptions { + users: vec![ + UserFixture::new("Guido", "agx", "guido.png", true, false), + UserFixture::new("Greeter", "greetd", "phoshi.png", true, true), + UserFixture::new("Homed User", "homed", "samcday.jpeg", true, false), + ], + cached_users: Some(vec!["agx".into(), "greetd".into()]), + homed_users: vec!["homed".into()], + ..Default::default() + }), + last_user: Some("agx".into()), + fake_greetd: Some(true), + ..Default::default() + })); + + let ready_rx = test.ready_rx.clone(); + let shell = test.shell.clone(); + test.start( + "homed-accounts-bridge", + glib::spawn_future_local(clone!(@weak shell => async move { + let (_vp, _) = ready_rx.recv().await.unwrap(); + glib::timeout_future(Duration::from_millis(2500)).await; + + let lockscreen = shell + .lockscreen_manager() + .lockscreen() + .unwrap() + .downcast::() + .unwrap(); + let usp = lockscreen.imp().user_session_page.get().unwrap(); + + let rows = usp.imp().box_users.children(); + let usernames = rows + .iter() + .filter_map(|row| { + row.downcast_ref::() + .and_then(|row| row.subtitle()) + .map(|s| s.to_string()) + }) + .collect::>(); + + assert!(usernames.contains(&"agx".to_string())); + assert!(usernames.contains(&"homed".to_string())); + assert!(!usernames.contains(&"greetd".to_string())); + + fade_quit(); + })), + ); +}