CKGreeter/src/callbacks.rs
CanadianBaconBoi 4f2902a847 REFACTOR REFACTOR REFACTOR
Added logging
Improved control flow
Stopped blocking the goddamn runtime with my background code
Got rid of a potential double free
2026-06-16 19:39:32 +02:00

677 lines
32 KiB
Rust

use crate::config::Config;
use crate::input::KeyboardLockStates;
use crate::{GreeterData, GreeterDisplay, LoginOptionTileData};
use anyhow::{Context, anyhow};
use greetd_ipc::codec::TokioCodec;
use greetd_ipc::{AuthMessageType, ErrorType, Request, Response};
use i_slint_core::api::{ComponentHandle, Global, Image, Rgba8Pixel, SharedPixelBuffer, Weak};
use i_slint_core::model::{Model, VecModel};
use log::{debug, error, info, trace};
use magick_rust::MagickWand;
use pexels_api::{PexelsClient, SearchParams};
use rand::seq::SliceRandom;
use std::collections::VecDeque;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader, Lines};
use tokio::net::UnixStream;
#[cfg(not(debug_assertions))]
use slint::SharedString;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReadDirStream;
#[cfg(not(debug_assertions))]
use zbus::{Connection, proxy};
pub fn register_login_callback(
data: &GreeterData,
display_weak: Weak<GreeterDisplay>,
tiles: Rc<VecModel<LoginOptionTileData>>,
) {
data.on_login({
let tiles = tiles.clone();
let display_weak = display_weak.clone();
move |username, password| {
debug!(target: "ckgreeter::callbacks::login", "----- Login callback called");
let tiles = tiles.clone();
let display_weak = display_weak.clone();
debug!(target: "ckgreeter::callbacks::login", "spawning slint thread");
slint::spawn_local(async move {
debug!(target: "ckgreeter::callbacks::login", "spawned slint thread");
let display = display_weak.unwrap();
let username = username.to_string();
let password = password.to_string();
debug!(target: "ckgreeter::callbacks::login", "username: {username:?}");
let index = display.get_selected_index();
let tile = &tiles.row_data(index as usize);
let tile = match tile {
Some(tile) => tile,
None => {
display.invoke_set_error("Failed to login: Invalid index for tile".into());
error!("Failed to login: Invalid index for tile: {index}");
return;
}
};
match login(username.as_str(), password.as_str(), tile.command.as_str()).await {
Ok(successful) => {
if !successful {
display.invoke_set_error("Failed to login: Bad Login".into());
error!("Failed to login: Bad Login");
return;
}
if let Err(err) = slint::quit_event_loop() {
display
.invoke_set_error(format!("Failed to exit greeter: {err}").into());
error!("Failed to exit greeter: {err}");
}
}
Err(err) => {
display.invoke_set_error(format!("Failed to login: {err}").into());
error!("Failed to login: {err}");
return;
}
};
})
.unwrap();
}
});
async fn login(username: &str, password: &str, cmd: &str) -> anyhow::Result<bool> {
if username.trim().is_empty() {
return Err(anyhow::anyhow!("Username is required"));
}
if cmd.trim().is_empty() {
return Err(anyhow::anyhow!("Command is required"));
}
debug!(target: "ckgreeter::callbacks::login", "connecting to greetd socket");
let socket = std::env::var("GREETD_SOCK").context("GREETD_SOCK is not set")?;
let mut stream = UnixStream::connect(socket)
.await
.context("failed to connect to greetd socket")?;
debug!(target: "ckgreeter::callbacks::login", "connected to greetd socket");
let mut next_request = Request::CreateSession {
username: username.to_string(),
};
trace!(target: "ckgreeter::callbacks::login", "sending request to create session: {next_request:?}");
let mut starting = false;
loop {
next_request.write_to(&mut stream).await?;
match Response::read_from(&mut stream).await? {
Response::AuthMessage {
auth_message: _auth_message,
auth_message_type,
} => {
let response = match auth_message_type {
AuthMessageType::Visible => None,
AuthMessageType::Secret => {
if !password.is_empty() {
Some(password.to_string())
} else {
None
}
}
AuthMessageType::Info => None,
AuthMessageType::Error => None,
};
trace!(target: "ckgreeter::callbacks::login", "sending request: {next_request:?}");
next_request = Request::PostAuthMessageResponse { response };
}
Response::Success => {
if starting {
debug!(target: "ckgreeter::callbacks::login", "starting session");
return Ok(true);
} else {
starting = true;
trace!(target: "ckgreeter::callbacks::login", "sending request: {next_request:?}");
next_request = Request::StartSession {
env: vec![],
cmd: vec![cmd.to_string()],
}
}
}
Response::Error {
error_type,
description,
} => {
debug!(target: "ckgreeter::callbacks::login", "error logging in");
Request::CancelSession.write_to(&mut stream).await?;
return match error_type {
ErrorType::AuthError => Ok(false),
ErrorType::Error => Err(anyhow::anyhow!("login error: {description:?}")),
};
}
}
}
}
}
pub fn register_power_callback(_data: &GreeterData) {
#[cfg(not(debug_assertions))]
_data.on_loginctl(move |command: SharedString| {
debug!(target: "ckgreeter::callbacks::power", "----- Power callback called");
slint::spawn_local(async move {
if let Err(err) = handle_power_command(command.as_str()).await {
eprintln!("Power command failed: {err}");
}
})
.unwrap();
});
#[cfg(not(debug_assertions))]
async fn handle_power_command(command: &str) -> anyhow::Result<()> {
debug!(target: "ckgreeter::callbacks::power", "command: {command:?}");
trace!(target: "ckgreeter::callbacks::power", "connecting to dbus");
let connection = Connection::system().await?;
trace!(target: "ckgreeter::callbacks::power", "connected to dbus");
trace!(target: "ckgreeter::callbacks::power", "getting power manager proxy");
let proxy = PowerManagerProxy::new(&connection).await?;
trace!(target: "ckgreeter::callbacks::power", "got power manager proxy");
debug!(target: "ckgreeter::callbacks::power", "running power command: {command}");
match command {
"suspend" => proxy.suspend(&false).await?,
"poweroff" => proxy.power_off(&false).await?,
"reboot" => proxy.reboot(&false).await?,
command => return Err(anyhow::anyhow!("Unknown power command: {command}")),
}
Ok(())
}
#[cfg(not(debug_assertions))]
#[proxy(
interface = "org.freedesktop.login1.Manager",
default_service = "org.freedesktop.login1",
default_path = "/org/freedesktop/login1"
)]
trait PowerManager {
fn suspend(&self, interactive: &bool) -> zbus::Result<()>;
fn reboot(&self, interactive: &bool) -> zbus::Result<()>;
fn power_off(&self, interactive: &bool) -> zbus::Result<()>;
}
}
pub fn register_lock_state_callback(data: &GreeterData, data_weak: Weak<GreeterData<'static>>) {
data.on_check_lock_states({
let data_weak = data_weak.clone();
move || {
let data_weak = data_weak.clone();
slint::spawn_local(async move {
debug!(target: "ckgreeter::callbacks::lock_state", "----- lock state callback called");
let data = data_weak.unwrap();
if let Some(states) = KeyboardLockStates::get_locks_state() {
data.set_caps_lock_state(states.caps_lock);
trace!(target: "ckgreeter::callbacks::lock_state", "caps lock state: {}", states.caps_lock);
data.set_num_lock_state(states.num_lock);
trace!(target: "ckgreeter::callbacks::lock_state", "num lock state: {}", states.num_lock);
data.set_scroll_lock_state(states.scroll_lock);
trace!(target: "ckgreeter::callbacks::lock_state", "scroll lock state: {}", states.scroll_lock)
}
})
.unwrap();
}
});
}
pub fn register_wallpaper_callback(
data: &GreeterData,
display_weak: Weak<GreeterDisplay>,
data_weak: Weak<GreeterData<'static>>,
cache_dir: PathBuf,
config: &Config,
) {
//TODO: Seperate this out into seperate methods, cleanup!!!
const MAX_RECENT_WALLPAPERS: usize = 25;
async fn read_lines<P>(filename: P) -> std::io::Result<Lines<BufReader<File>>>
where
P: AsRef<Path> + std::fmt::Debug,
{
debug!(target: "ckgreeter::callbacks::wallpaper::read_lines", "reading lines from file: {:?}", filename);
let file = File::open(filename).await?;
Ok(BufReader::new(file).lines())
}
async fn write_lines<P>(filename: &P, lines: VecDeque<String>) -> std::io::Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
let mut contents = String::new();
for line in lines {
contents.push_str(&line);
contents.push('\n');
}
debug!(target: "ckgreeter::callbacks::wallpaper::write_lines", "writing lines to file: {:?}", filename);
std::fs::write(filename, contents)
}
fn set_background_image(
display: &GreeterDisplay,
data: &GreeterData,
pixels: &[u8],
w: u32,
h: u32,
) {
debug!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "setting background image");
if !display.get_background_toggle() {
trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "setting background image 1");
let old = data.get_wallpaper_image();
data.set_wallpaper_image(Image::from_rgba8(
SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice(pixels, w, h),
));
trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "set background image 1");
drop(old);
} else {
trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "setting background image 2");
let old = data.get_wallpaper_image_1();
data.set_wallpaper_image_1(Image::from_rgba8(
SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice(pixels, w, h),
));
trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "set background image 2");
drop(old);
}
}
fn export_wand_rgba(wand: &MagickWand) -> anyhow::Result<(u32, u32, Vec<u8>)> {
trace!(target: "ckgreeter::callbacks::wallpaper::export_wand_rgba", "exporting wand");
let pixels = wand
.export_image_pixels(
0,
0,
wand.get_image_width(),
wand.get_image_height(),
"RGBA",
)
.ok_or_else(|| anyhow!("Failed to export image pixels"))?;
trace!(target: "ckgreeter::callbacks::wallpaper::export_wand_rgba", "exported wand");
Ok((
wand.get_image_width() as u32,
wand.get_image_height() as u32,
pixels,
))
}
let reqwest_client = match reqwest::ClientBuilder::new()
.tls_backend_rustls()
.http2_prior_knowledge()
.http2_keep_alive_timeout(std::time::Duration::from_secs(10))
.build()
{
Ok(client) => client,
Err(err) => {
eprintln!("Failed to create reqwest client: {err}");
return;
}
};
debug!(target: "ckgreeter::callbacks::wallpaper", "reqwest client created");
if let Some(pexels_api_key) = config.pexels_api_key.clone() {
info!(target: "ckgreeter::callbacks::wallpaper", "Using pexels api");
debug!(target: "ckgreeter::callbacks::wallpaper", "pexels api key found");
let pexels_client = Arc::new(PexelsClient::new(pexels_api_key));
let pexels_query = config
.pexels_query
.clone()
.unwrap_or_else(|| String::from("nature"));
debug!(target: "ckgreeter::callbacks::wallpaper", "pexels query: {pexels_query}");
info!(target: "ckgreeter::callbacks::wallpaper", "Registering wallpaper callback");
data.on_get_new_wallpaper({
let data_weak = data_weak.clone();
let display_weak = display_weak.clone();
move || {
debug!(target: "ckgreeter::callbacks::wallpaper", "----- wallpaper callback called");
let pexels_client = pexels_client.clone();
let reqwest_client = reqwest_client.clone();
let cache_dir = cache_dir.clone();
let pexels_query = pexels_query.clone();
let display_weak = display_weak.clone();
let data_weak = data_weak.clone();
slint::spawn_local(async move {
trait ResultExt<T, E> {
fn wpaper_err(self, data: &GreeterData) -> Result<T, E>;
}
impl<T, E: Display> ResultExt<T, E> for Result<T, E> {
fn wpaper_err(self, data: &GreeterData) -> Result<T, E> {
self.inspect_err(|e| {
error!(target: "ckgreeter::callbacks::wallpaper", "Failed to get wallpaper: {e}");
data.set_wallpaper_author("Error".into());
data.set_wallpaper_alt(e.to_string().into());
data.set_wallpaper_url("Failed to get wallpaper".into());
})
}
}
let data = data_weak.unwrap();
let display = display_weak.unwrap();
let last_used_log = cache_dir.join("last_used_log.txt");
debug!(target: "ckgreeter::callbacks::wallpaper", "last used log: {:?}", last_used_log);
if !tokio::fs::try_exists(&last_used_log).await.wpaper_err(&data)? {
File::create(&last_used_log).await.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "created last used log");
}
let mut file_lines = tokio::time::timeout(
std::time::Duration::from_secs(5),
read_lines(&last_used_log),
)
.await.wpaper_err(&data)?.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "read last used log");
let mut idx = 0;
let mut last_used_lines = VecDeque::with_capacity(MAX_RECENT_WALLPAPERS);
while let Some(line) = file_lines.next_line().await.wpaper_err(&data)? {
last_used_lines.push_back(line);
idx += 1;
if idx > MAX_RECENT_WALLPAPERS {
last_used_lines.pop_front();
}
}
drop(file_lines);
let mut page = 0;
loop {
debug!(target: "ckgreeter::callbacks::wallpaper", "fetched photos list from pexels api");
let mut is_photo_set: bool = false;
let mut found_photo: bool = false;
match pexels_client
.search_photos(
pexels_query.as_str(),
&SearchParams::new()
.page(page)
.per_page(10)
.size(pexels_api::Size::Medium)
.orientation(pexels_api::Orientation::Landscape),
)
.await
{
Ok(photos) => {
if photos.total_results > 0 {
found_photo = true;
debug!(target: "ckgreeter::callbacks::wallpaper", "got photos from pexels api");
for photo in photos.photos {
let string_id = photo.id.to_string();
trace!(target: "ckgreeter::callbacks::wallpaper", "pexels photo id: {}", string_id);
if last_used_lines.contains(&string_id) {
trace!(target: "ckgreeter::callbacks::wallpaper", "pexels photo id already used recently");
continue;
}
is_photo_set = true;
last_used_lines.push_back(string_id);
if last_used_lines.len() > MAX_RECENT_WALLPAPERS {
last_used_lines.pop_front();
}
let image_path = cache_dir.join(format!("{}.png", photo.id));
trace!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image path: {:?}", image_path);
let _ = if !tokio::fs::try_exists(&image_path).await.wpaper_err(&data)? {
debug!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image not found, downloading");
let response = reqwest_client
.get(photo.src.large2x.clone()) // TODO: determine which photo src size is best, maybe determine based on the monitor size to reduce memory pressure
.send()
.await.wpaper_err(&data)?;
let image_bytes = response.bytes().await.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image downloaded");
let size = display.window().size();
debug!(target: "ckgreeter::callbacks::wallpaper", "resizing image with ImageMagick");
let (w, h, pixels) =
tokio::task::spawn_blocking(move || {
let wand = MagickWand::new();
wand.read_image_blob(&image_bytes)?;
drop(image_bytes);
if size.width >= size.height {
wand.fit(
size.width as usize,
size.width as usize,
);
} else {
wand.fit(
size.height as usize,
size.height as usize,
);
}
trace!(target: "ckgreeter::callbacks::wallpaper", "exporting png data");
let png_data = wand.write_image_blob("png")?;
std::fs::write(image_path, png_data)?;
trace!(target: "ckgreeter::callbacks::wallpaper", "exported png data to disk");
export_wand_rgba(&wand)
})
.await.wpaper_err(&data)?.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "image resized with ImageMagick");
set_background_image(
&display,
&data,
pixels.as_slice(),
w,
h,
);
debug!(target: "ckgreeter::callbacks::wallpaper", "set background image");
drop(pixels);
} else {
debug!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image found, using cached");
let contents = tokio::fs::read(image_path).await.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "resizing image with ImageMagick");
let (w, h, pixels) =
tokio::task::spawn_blocking(move || {
let wand = MagickWand::new();
wand.read_image_blob(contents.as_slice())?;
export_wand_rgba(&wand)
})
.await.wpaper_err(&data)?.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "image resized with ImageMagick");
set_background_image(
&display,
&data,
pixels.as_slice(),
w,
h,
);
debug!(target: "ckgreeter::callbacks::wallpaper", "set background image");
drop(pixels);
};
debug!(target: "ckgreeter::callbacks::wallpaper", "set pexels photo author: {}", photo.photographer);
data.set_wallpaper_author(photo.photographer.into());
let photo_alt = photo.alt.unwrap_or("".into());
debug!(target: "ckgreeter::callbacks::wallpaper", "set pexels photo alt: {}", photo_alt);
data.set_wallpaper_alt(
photo_alt.into(),
);
debug!(target: "ckgreeter::callbacks::wallpaper", "set pexels photo url: {}", photo.url);
data.set_wallpaper_url(photo.url.into());
debug!(target: "ckgreeter::callbacks::wallpaper", "switching background to foreground");
display.invoke_switch_background();
break;
}
}
}
Err(err) => {
error!("Failed to fetch online images, using cached. {err}");
debug!(target: "ckgreeter::callbacks::wallpaper", "getting cached wallpaper images from disk");
let mut photos =
ReadDirStream::new(tokio::fs::read_dir(&cache_dir).await.wpaper_err(&data)?)
.filter(|de| match de {
Ok(de) => {
de.path().is_file()
&& de.path().extension()
== Some(std::ffi::OsStr::new("png"))
}
Err(_) => false,
})
.collect::<Vec<_>>()
.await;
debug!(target: "ckgreeter::callbacks::wallpaper", "got cached wallpaper images from disk");
photos.shuffle(&mut rand::rng());
debug!(target: "ckgreeter::callbacks::wallpaper", "shuffled cached wallpaper images");
let mut photos = tokio_stream::iter(photos);
while let Some(photo) = photos.next().await {
match photo {
Ok(photo) => {
let photo_path = photo.path();
let string_id = photo_path
.file_prefix()
.context("failed to get filename").wpaper_err(&data)?
.to_string_lossy()
.to_string();
if last_used_lines
.front()
.is_some_and(|first| first == &string_id)
{
continue;
}
last_used_lines.push_back(string_id);
if last_used_lines.len() > MAX_RECENT_WALLPAPERS {
last_used_lines.pop_front();
}
debug!(target: "ckgreeter::callbacks::wallpaper", "using cached wallpaper image: {:?}", photo_path);
debug!(target: "ckgreeter::callbacks::wallpaper", "resizing image with ImageMagick");
let (w, h, pixels) =
tokio::task::spawn_blocking(move || {
let wand = MagickWand::new();
wand.read_image(photo_path.to_str().context(
"failed to convert image path to UTF-8",
)?)?;
export_wand_rgba(&wand)
})
.await.wpaper_err(&data)?.wpaper_err(&data)?;
debug!(target: "ckgreeter::callbacks::wallpaper", "image resized with ImageMagick");
set_background_image(
&display,
&data,
pixels.as_slice(),
w,
h,
);
debug!(target: "ckgreeter::callbacks::wallpaper", "set background image");
drop(pixels);
data.set_wallpaper_author("Unknown".into());
data.set_wallpaper_alt("".into());
data.set_wallpaper_url("Cached wallpaper".into());
debug!(target: "ckgreeter::callbacks::wallpaper", "switching background to foreground");
display.invoke_switch_background();
break;
}
Err(_err) => {
continue;
}
}
}
}
}
if is_photo_set || !found_photo {
break;
}
page += 1;
}
tokio::time::timeout(
std::time::Duration::from_secs(5),
write_lines(&last_used_log, last_used_lines),
)
.await
.context("timed out while writing wallpaper log").wpaper_err(&data)?.wpaper_err(&data)?;
return anyhow::Ok(());
}).unwrap();
}
});
}
}
pub fn register_get_default_font_size(data: &GreeterData, display_weak: Weak<GreeterDisplay>) {
data.on_get_default_font_size(move || {
debug!(target: "ckgreeter::callbacks::font_size", "----- get default font size callback called");
let display = display_weak.unwrap();
let size = display.window().size();
trace!(target: "ckgreeter::callbacks::font_size", "window size: {:?}", size);
let font_size = ((size.width as f32).min(size.height as f32)) * 0.01;
trace!(target: "ckgreeter::callbacks::font_size", "font size: {}", font_size);
font_size
});
}
pub fn register_callbacks(
display: &GreeterDisplay,
data: &GreeterData,
tiles: Rc<VecModel<LoginOptionTileData>>,
cache_dir: PathBuf,
config: &Config,
) {
let display_weak = display.as_weak();
let data_weak = data.as_weak();
debug!(target: "ckgreeter::callbacks", "Registering callbacks");
register_login_callback(data, display_weak.clone(), tiles);
debug!(target: "ckgreeter::callbacks", "Registered login callback");
register_power_callback(data);
debug!(target: "ckgreeter::callbacks", "Registered power callback");
register_lock_state_callback(data, data_weak.clone());
debug!(target: "ckgreeter::callbacks", "Registered lock state callback");
register_wallpaper_callback(
data,
display_weak.clone(),
data_weak.clone(),
cache_dir,
config,
);
debug!(target: "ckgreeter::callbacks", "Registered clean inactive wallpaper callback");
register_get_default_font_size(data, display_weak.clone());
debug!(target: "ckgreeter::callbacks", "Registered get default font size callback");
}