Make the new login button stuff more automatic

By default it'll look at your environment to try to figure out which
display manager is used in order to start a new session.  We first try
the org.freedesktop.DisplayManager dbus interface, and if that fails,
inspect XDG_SESSION_DESKTOP to try to figure out which display manager
is running.

The user can also still specify the correct display manager, or a custom
command.
This commit is contained in:
Brian Tarricone 2022-05-15 20:50:45 -07:00
parent 99ffa88657
commit dda1a53856
8 changed files with 287 additions and 36 deletions

3
Cargo.lock generated
View File

@ -318,6 +318,7 @@ dependencies = [
"bscreensaver-util", "bscreensaver-util",
"glib", "glib",
"gtk", "gtk",
"log",
] ]
[[package]] [[package]]
@ -347,9 +348,11 @@ dependencies = [
"humantime", "humantime",
"lazy_static", "lazy_static",
"libc", "libc",
"shell-words",
"toml", "toml",
"xcb", "xcb",
"xdg", "xdg",
"zbus",
] ]
[[package]] [[package]]

View File

@ -4,9 +4,9 @@ use gethostname::gethostname;
use glib::GString; use glib::GString;
use gtk::{prelude::*, Button, Entry, Label, Plug, Window}; use gtk::{prelude::*, Button, Entry, Label, Plug, Window};
use log::{debug, error, warn}; use log::{debug, error, warn};
use std::{io::{self, Write}, process::{exit, Command}, rc::Rc, thread, time::Duration}; use std::{io::{self, Write}, process::exit, rc::Rc, thread, time::Duration};
use bscreensaver_util::{init_logging, settings::Configuration}; use bscreensaver_util::{init_logging, settings::Configuration, desktop::NewLoginCommand};
const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100); const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
const DIALOG_TIMEOUT: Duration = Duration::from_secs(60); const DIALOG_TIMEOUT: Duration = Duration::from_secs(60);
@ -15,11 +15,13 @@ fn main() -> anyhow::Result<()> {
init_logging("BSCREENSAVER_DIALOG_GTK3_LOG"); init_logging("BSCREENSAVER_DIALOG_GTK3_LOG");
glib::log_set_default_handler(glib::rust_log_handler); glib::log_set_default_handler(glib::rust_log_handler);
let mut config = Configuration::load()?; let config = Configuration::load()?;
let new_login_command = config.new_login_command let new_login_command =
.take() if config.new_login_command == NewLoginCommand::Disabled {
.map(|nlc| shell_words::split(&nlc)) None
.transpose()?; } else {
Some(config.new_login_command.clone())
};
let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok(); let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok();
@ -245,15 +247,15 @@ fn main() -> anyhow::Result<()> {
.label("New Login") .label("New Login")
.build(); .build();
new_login_button.connect_clicked(move |_| { new_login_button.connect_clicked(move |_| {
let cmd = &new_login_command[0]; let new_login_command = new_login_command.clone();
let empty = Vec::<String>::new(); thread::spawn(move || {
let args = if new_login_command.len() > 1 { &new_login_command[1..] } else { &empty }; if let Err(err) = new_login_command.run() {
if let Err(err) = Command::new(cmd).args(args).spawn() {
warn!("Failed to run new login command: {}", err); warn!("Failed to run new login command: {}", err);
} else { } else {
exit(1); exit(1);
} }
}); });
});
hbox.pack_start(&new_login_button, false, true, 8); hbox.pack_start(&new_login_button, false, true, 8);
} }

View File

@ -8,3 +8,4 @@ anyhow = "1"
bscreensaver-util = { path = "../util" } bscreensaver-util = { path = "../util" }
glib = "0.15" glib = "0.15"
gtk = { version = "0.15", features = ["v3_20"] } gtk = { version = "0.15", features = ["v3_20"] }
log = "0.4"

View File

@ -1,17 +1,20 @@
use gtk::{glib, prelude::*}; use gtk::{glib, prelude::*};
use glib::clone; use glib::clone;
use log::warn;
use std::{env, process::exit, time::Duration}; use std::{env, process::exit, time::Duration};
use bscreensaver_util::settings::Configuration; use bscreensaver_util::{init_logging, settings::Configuration, desktop::NewLoginCommand};
#[derive(Clone)] #[derive(Clone)]
struct Widgets { struct Widgets {
lock_timeout: gtk::SpinButton, lock_timeout: gtk::SpinButton,
blank_before_locking: gtk::SpinButton, blank_before_locking: gtk::SpinButton,
new_login_command: gtk::Entry, new_login_command_combo: gtk::ComboBoxText,
custom_new_login_command_entry: gtk::Entry,
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
init_logging("BSCREENSAVER_SETTINGS");
let config = Configuration::load()?; let config = Configuration::load()?;
let app = gtk::Application::builder() let app = gtk::Application::builder()
@ -122,27 +125,83 @@ fn show_ui(app: &gtk::Application, config: &Configuration) {
label_sg.add_widget(&label); label_sg.add_widget(&label);
hbox.pack_start(&label, false, false, 0); hbox.pack_start(&label, false, false, 0);
let new_login_command_entry = gtk::Entry::builder() let vbox = gtk::Box::builder()
.text(config.new_login_command.as_ref().unwrap_or(&"".to_string())) .orientation(gtk::Orientation::Vertical)
.spacing(8)
.build();
hbox.pack_start(&vbox, true, true, 0);
let new_login_command_combo = gtk::ComboBoxText::builder()
.build();
new_login_command_combo.append_text(NewLoginCommand::Auto.as_str());
new_login_command_combo.append_text(NewLoginCommand::DisplayManagerDBus.as_str());
new_login_command_combo.append_text(NewLoginCommand::Gdm.as_str());
new_login_command_combo.append_text(NewLoginCommand::Kdm.as_str());
new_login_command_combo.append_text(NewLoginCommand::LightDm.as_str());
new_login_command_combo.append_text(NewLoginCommand::Lxdm.as_str());
new_login_command_combo.append_text(NewLoginCommand::Custom("".to_string()).as_str());
new_login_command_combo.append_text(NewLoginCommand::Disabled.as_str());
vbox.pack_start(&new_login_command_combo, false, false, 0);
let (custom_new_login_command_hbox, custom_new_login_command_entry, custom_new_login_command_button) = {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.sensitive(false)
.build();
topvbox.pack_start(&hbox, false, false, 0);
let spacer = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(0)
.build();
label_sg.add_widget(&spacer);
hbox.pack_start(&spacer, false, false, 0);
let label = gtk::Label::builder()
.label("Custom Command:")
.tooltip_text("Custom command to run when the 'New Login' button is clicked in the unlock dialog")
.xalign(0.0)
.build();
hbox.pack_start(&label, false, false, 0);
let entry = gtk::Entry::builder()
.width_chars(30) .width_chars(30)
.activates_default(true) .activates_default(true)
.build(); .build();
hbox.pack_start(&new_login_command_entry, false, false, 0); hbox.pack_start(&entry, false, false, 0);
let button = gtk::Button::from_icon_name(Some("folder-open"), gtk::IconSize::Button);
hbox.pack_start(&button, false, false, 0);
(hbox, entry, button)
};
match &config.new_login_command {
NewLoginCommand::Custom(cmd) => {
custom_new_login_command_entry.set_text(cmd.as_str());
custom_new_login_command_hbox.set_sensitive(true);
},
_ => (),
};
new_login_command_combo.set_active(Some(config.new_login_command.ord()));
new_login_command_combo.connect_changed(clone!(@strong custom_new_login_command_hbox => move |combo| {
let sensitive = combo.active().unwrap_or(0) == NewLoginCommand::Custom("".to_string()).ord();
custom_new_login_command_hbox.set_sensitive(sensitive);
}));
let widgets = Widgets { let widgets = Widgets {
lock_timeout: lock_timeout_spinbutton.clone(), lock_timeout: lock_timeout_spinbutton.clone(),
blank_before_locking: blank_before_locking_spinbutton.clone(), blank_before_locking: blank_before_locking_spinbutton.clone(),
new_login_command: new_login_command_entry.clone(), new_login_command_combo: new_login_command_combo.clone(),
custom_new_login_command_entry: custom_new_login_command_entry.clone(),
}; };
mainwin.connect_delete_event(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_,_| { mainwin.connect_delete_event(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_,_| {
Inhibit(!confirm_cancel(&config, &widgets, &mainwin)) Inhibit(!confirm_cancel(&config, &widgets, &mainwin))
})); }));
custom_new_login_command_button.connect_clicked(clone!(@strong mainwin, @strong widgets => move |_| {
let button = gtk::Button::from_icon_name(Some("folder-open"), gtk::IconSize::Button);
button.connect_clicked(clone!(@strong mainwin, @strong widgets => move |_| {
run_file_chooser(&mainwin, &widgets); run_file_chooser(&mainwin, &widgets);
})); }));
hbox.pack_start(&button, false, false, 0);
let button_box = gtk::ButtonBox::builder() let button_box = gtk::ButtonBox::builder()
.spacing(8) .spacing(8)
@ -177,7 +236,7 @@ fn show_ui(app: &gtk::Application, config: &Configuration) {
fn run_file_chooser(mainwin: &gtk::ApplicationWindow, widgets: &Widgets) { fn run_file_chooser(mainwin: &gtk::ApplicationWindow, widgets: &Widgets) {
let file_chooser = gtk::FileChooserNative::new( let file_chooser = gtk::FileChooserNative::new(
Some("New Login Command"), Some("Custom New Login Command"),
Some(mainwin), Some(mainwin),
gtk::FileChooserAction::Open, gtk::FileChooserAction::Open,
Some("Select"), Some("Select"),
@ -187,7 +246,7 @@ fn run_file_chooser(mainwin: &gtk::ApplicationWindow, widgets: &Widgets) {
file_chooser.hide(); file_chooser.hide();
if response == gtk::ResponseType::Accept { if response == gtk::ResponseType::Accept {
if let Some(filename) = file_chooser.filename() { if let Some(filename) = file_chooser.filename() {
widgets.new_login_command.set_text(&filename.to_string_lossy()); widgets.custom_new_login_command_entry.set_text(&filename.to_string_lossy());
} }
} }
} }
@ -243,7 +302,19 @@ fn build_new_configuration(old_config: &Configuration, widgets: &Widgets) -> (Co
let mut new_config = old_config.clone(); let mut new_config = old_config.clone();
new_config.lock_timeout = Duration::from_secs(widgets.lock_timeout.adjustment().value() as u64 * 60); new_config.lock_timeout = Duration::from_secs(widgets.lock_timeout.adjustment().value() as u64 * 60);
new_config.blank_before_locking = Duration::from_secs(widgets.blank_before_locking.adjustment().value() as u64 * 60); new_config.blank_before_locking = Duration::from_secs(widgets.blank_before_locking.adjustment().value() as u64 * 60);
new_config.new_login_command = Some(widgets.new_login_command.text()).filter(|s| !s.is_empty()).map(|s| s.to_string());
new_config.new_login_command = match NewLoginCommand::try_from(widgets.new_login_command_combo.active().unwrap_or(0) as u32) {
Ok(NewLoginCommand::Custom(_)) => Some(widgets.custom_new_login_command_entry.text())
.filter(|s| !s.is_empty())
.map(|s| NewLoginCommand::Custom(s.to_string()))
.unwrap_or(NewLoginCommand::Auto),
Ok(nlc) => nlc,
Err(_) => {
warn!("BUG: couldn't figure out new login command type from combo box");
NewLoginCommand::Auto
},
};
let changed = old_config != &new_config; let changed = old_config != &new_config;
(new_config, changed) (new_config, changed)
} }

View File

@ -12,6 +12,8 @@ env_logger = "0.9"
humantime = "2" humantime = "2"
lazy_static = "1" lazy_static = "1"
libc = "0.2" libc = "0.2"
shell-words = "1"
toml = "0.5" toml = "0.5"
xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] } xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] }
xdg = "2" xdg = "2"
zbus = "2"

171
util/src/desktop.rs Normal file
View File

@ -0,0 +1,171 @@
use std::{env, fmt, process::Command};
const GDM_CMD: &[&str] = &[ "gdmflexiserver", "-ls" ];
const KDM_CMD: &[&str] = &[ "kdmctl", "reserve" ];
const LIGHTDM_CMD: &[&str] = &[ "dm-tool", "switch-to-greeter" ];
const LXDM_CMD: &[&str] = &[ "lxdm", "-c", "USER_SWITCH" ];
#[derive(Debug, Clone, PartialEq)]
pub enum NewLoginCommand {
Auto,
DisplayManagerDBus,
Gdm,
Kdm,
LightDm,
Lxdm,
Custom(String),
Disabled,
}
impl NewLoginCommand {
pub fn run(&self) -> anyhow::Result<()> {
match self {
Self::Auto => auto_run(),
Self::DisplayManagerDBus => call_dbus(),
Self::Gdm => run(GDM_CMD),
Self::Kdm => run(KDM_CMD),
Self::LightDm => run(LIGHTDM_CMD),
Self::Lxdm => run(LXDM_CMD),
Self::Custom(cmd) => shell_words::split(cmd.as_str())
.map_err(|err| err.into())
.and_then(|argvec| {
let argv: Vec<&str> = argvec.iter().map(|s| s.as_str()).collect();
run(&argv).map_err(|err| err.into())
}),
Self::Disabled => Ok(()),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Auto => "Auto",
Self::DisplayManagerDBus => "DBus (desktop-agnostic)",
Self::Gdm => "GDM (GNOME)",
Self::Kdm => "KDM (KDE)",
Self::LightDm => "LightDM",
Self::Lxdm => "LXDM (LXDE)",
Self::Custom(_) => "Custom",
Self::Disabled => "Disabled",
}
}
pub fn ord(&self) -> u32 {
match self {
Self::Auto => 0,
Self::DisplayManagerDBus => 1,
Self::Gdm => 2,
Self::Kdm => 3,
Self::LightDm => 4,
Self::Lxdm => 5,
Self::Custom(_) => 6,
Self::Disabled => 7,
}
}
}
impl fmt::Display for NewLoginCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl TryFrom<u32> for NewLoginCommand {
type Error = anyhow::Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Auto),
1 => Ok(Self::DisplayManagerDBus),
2 => Ok(Self::Gdm),
3 => Ok(Self::Kdm),
4 => Ok(Self::LightDm),
5 => Ok(Self::Lxdm),
6 => Ok(Self::Custom("".to_string())),
7 => Ok(Self::Disabled),
other => Err(anyhow::anyhow!("Unknown ordinal for NewLoginCommand: {}", other)),
}
}
}
impl TryFrom<&toml::Value> for NewLoginCommand {
type Error = anyhow::Error;
fn try_from(value: &toml::Value) -> Result<Self, Self::Error> {
if let Some(s) = value.as_str() {
return match s {
"auto" => Ok(Self::Auto),
"dbus" => Ok(Self::DisplayManagerDBus),
"gdm" => Ok(Self::Gdm),
"kdm" => Ok(Self::Kdm),
"lightdm" => Ok(Self::LightDm),
"lxdm" => Ok(Self::Lxdm),
"disabled" => Ok(Self::Disabled),
other => Err(anyhow::anyhow!("Unknown value '{}' for new login command", other)),
};
}
if let Some(arr) = value.as_array() {
let argv = arr.into_iter()
.map(|v| v.as_str().map(|s| s.to_string()).ok_or_else(|| anyhow::anyhow!("Custom command must be made of strings")))
.collect::<anyhow::Result<Vec<String>>>()?;
return Ok(Self::Custom(shell_words::join(argv)))
}
Err(anyhow::anyhow!("Unknown value for new login command"))
}
}
impl TryInto<toml::Value> for NewLoginCommand {
type Error = anyhow::Error;
fn try_into(self) -> Result<toml::Value, Self::Error> {
match self {
Self::Auto => Ok(toml::Value::String("auto".to_string())),
Self::DisplayManagerDBus => Ok(toml::Value::String("dbus".to_string())),
Self::Gdm => Ok(toml::Value::String("gdm".to_string())),
Self::Kdm => Ok(toml::Value::String("kdm".to_string())),
Self::LightDm => Ok(toml::Value::String("lightdm".to_string())),
Self::Lxdm => Ok(toml::Value::String("lxdm".to_string())),
Self::Custom(cmd) => shell_words::split(cmd.as_str())
.map_err(|err| err.into())
.map(|words| toml::Value::Array(words.into_iter().map(|s| toml::Value::String(s)).collect())),
Self::Disabled => Ok(toml::Value::String("disabled".to_string())),
}
}
}
fn auto_run() -> anyhow::Result<()> {
if let Ok(_) = call_dbus() {
return Ok(())
}
if let Some(session_desktop) = env::var("XDG_SESSION_DESKTOP").map(|s| s.to_lowercase()).ok() {
match session_desktop.as_str() {
"gnome" | "gdm" => run(GDM_CMD),
"kde" | "kdm" => run(KDM_CMD),
"lightdm-session" => run(LIGHTDM_CMD),
"lxde" | "lxdm" => run(LXDM_CMD),
other => Err(anyhow::anyhow!("Couldn't determine how to start a new login session for session type {}", other)),
}
} else {
Err(anyhow::anyhow!("Couldn't determine how to start a new login session"))
}
}
fn call_dbus() -> anyhow::Result<()> {
let seat_path = env::var("XDG_SEAT_PATH")
.unwrap_or_else(|_| "/org/freedesktop/DisplayManager/Seat0".to_string());
let bus = zbus::blocking::Connection::system()?;
let proxy = zbus::blocking::Proxy::new(
&bus,
"org.freedesktop.DisplayManager",
seat_path,
"org.freedesktop.DisplayManager.Seat"
)?;
proxy.call("SwitchToGreeter", &())?;
Ok(())
}
fn run(argv: &[&str]) -> anyhow::Result<()> {
Command::new(argv[0])
.args(if argv.len() > 1 { &argv[1..argv.len()-1] } else { &[] })
.spawn()?;
Ok(())
}

View File

@ -1,6 +1,7 @@
use std::{ffi::CStr, io}; use std::{ffi::CStr, io};
use xcb::x; use xcb::x;
pub mod desktop;
pub mod settings; pub mod settings;
pub const BSCREENSAVER_WM_CLASS: &[u8] = b"bscreensaver\0Bscreensaver\0"; pub const BSCREENSAVER_WM_CLASS: &[u8] = b"bscreensaver\0Bscreensaver\0";

View File

@ -2,6 +2,8 @@ use anyhow::anyhow;
use std::{fmt, fs::{self, File}, io::{Read, Write}, path::PathBuf, time::Duration}; use std::{fmt, fs::{self, File}, io::{Read, Write}, path::PathBuf, time::Duration};
use toml::Value; use toml::Value;
use crate::desktop::NewLoginCommand;
const CONFIG_FILE_RELATIVE_PATH: &str = "bscreensaver/bscreensaver.toml"; const CONFIG_FILE_RELATIVE_PATH: &str = "bscreensaver/bscreensaver.toml";
const LOCK_TIMEOUT: &str = "lock-timeout"; const LOCK_TIMEOUT: &str = "lock-timeout";
@ -51,7 +53,7 @@ pub struct Configuration {
pub lock_timeout: Duration, pub lock_timeout: Duration,
pub blank_before_locking: Duration, pub blank_before_locking: Duration,
pub dialog_backend: DialogBackend, pub dialog_backend: DialogBackend,
pub new_login_command: Option<String>, pub new_login_command: NewLoginCommand,
} }
impl Configuration { impl Configuration {
@ -80,7 +82,7 @@ impl Configuration {
}; };
config.new_login_command = match config_toml.get("new-login-command") { config.new_login_command = match config_toml.get("new-login-command") {
None => config.new_login_command, None => config.new_login_command,
Some(val) => val.as_str().map(|s| s.to_string()), Some(val) => val.try_into().unwrap_or(NewLoginCommand::Auto),
}; };
} }
@ -99,9 +101,7 @@ impl Configuration {
config_map.insert(LOCK_TIMEOUT.to_string(), Value::String(format_duration(self.lock_timeout).to_string())); config_map.insert(LOCK_TIMEOUT.to_string(), Value::String(format_duration(self.lock_timeout).to_string()));
config_map.insert(BLANK_BEFORE_LOCKING.to_string(), Value::String(format_duration(self.blank_before_locking).to_string())); config_map.insert(BLANK_BEFORE_LOCKING.to_string(), Value::String(format_duration(self.blank_before_locking).to_string()));
config_map.insert(DIALOG_BACKEND.to_string(), Value::String(self.dialog_backend.to_string())); config_map.insert(DIALOG_BACKEND.to_string(), Value::String(self.dialog_backend.to_string()));
if let Some(new_login_command) = &self.new_login_command { config_map.insert(NEW_LOGIN_COMMAND.to_string(), self.new_login_command.clone().try_into()?);
config_map.insert(NEW_LOGIN_COMMAND.to_string(), Value::String(new_login_command.clone()));
}
let config_path = xdg::BaseDirectories::new()?.place_config_file(CONFIG_FILE_RELATIVE_PATH)?; let config_path = xdg::BaseDirectories::new()?.place_config_file(CONFIG_FILE_RELATIVE_PATH)?;
let mut tmp_filename = config_path.file_name().unwrap().to_os_string(); let mut tmp_filename = config_path.file_name().unwrap().to_os_string();
@ -122,7 +122,7 @@ impl Default for Configuration {
lock_timeout: Duration::from_secs(60 * 10), lock_timeout: Duration::from_secs(60 * 10),
blank_before_locking: Duration::ZERO, blank_before_locking: Duration::ZERO,
dialog_backend: DialogBackend::Gtk3, dialog_backend: DialogBackend::Gtk3,
new_login_command: None, new_login_command: NewLoginCommand::Auto,
} }
} }
} }