use std::{ collections::BTreeMap, env, fs, io::Read, os::unix::process::CommandExt, path::{Path, PathBuf}, process::{Command, Stdio}, }; pub(crate) fn run() -> Result<(), Box> { for key in ["WAYLAND_SOCKET", "FERESE_SHELL_CONTROL_SOCKET"] { if let Some(fd) = env::var(key) .ok() .and_then(|value| value.parse::().ok()) .filter(|fd| *fd >= 3) { // These session-helper descriptors must never reach ordinary startup apps. unsafe { let flags = libc::fcntl(fd, libc::F_GETFD); if flags < 0 || libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0 { return Err(std::io::Error::last_os_error().into()); } } } } let home = env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .filter(|path| path.is_absolute()) .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config"))) .ok_or("Missing user config directory")?; let system = env::var_os("XDG_CONFIG_DIRS").unwrap_or_default(); let mut system_directories = env::split_paths(&system) .filter(|path| path.is_absolute()) .collect::>(); if system_directories.is_empty() { system_directories.push(PathBuf::from("/etc/xdg")); } let directories = std::iter::once(home) .chain(system_directories) .collect::>(); let mut entries = BTreeMap::new(); for directory in directories { let Ok(files) = fs::read_dir(directory.join("autostart")) else { continue; }; for file in files.flatten() { if file .path() .extension() .is_some_and(|extension| extension == "desktop") { entries.entry(file.file_name()).or_insert(file.path()); } } } let desktop = env::var("XDG_CURRENT_DESKTOP").unwrap_or_else(|_| "Ferese".into()); for path in entries.into_values() { let Some(entry) = read_entry(&path) else { continue; }; if !enabled(&entry, &desktop) { continue; } if let Some(program) = entry.get("TryExec").filter(|program| !program.is_empty()) { if !available(program) { continue; } } let parent = std::process::id() as libc::pid_t; let mut command = Command::new("timeout"); command .args(["--kill-after=1s", "15s", "gio", "launch"]) .arg(&path) .stdin(Stdio::null()) .stdout(Stdio::null()) .env_remove("WAYLAND_SOCKET") .env_remove("FERESE_SHELL_CONTROL_SOCKET") .env_remove("FERESE_PUBLIC_WAYLAND_DISPLAY"); // Only signal-safe calls run in the child before exec. A pending launcher // dies with this helper, while apps already launched by GIO remain independent. unsafe { command.pre_exec(move || { if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 { return Err(std::io::Error::last_os_error()); } if libc::getppid() != parent { libc::_exit(1); } Ok(()) }); } let result = command.status(); if !matches!(result, Ok(status) if status.success()) { eprintln!("ferese: could not autostart {}", path.display()); } } Ok(()) } fn read_entry(path: &Path) -> Option> { use std::os::unix::fs::OpenOptionsExt; let file = fs::OpenOptions::new() .read(true) .custom_flags(libc::O_NONBLOCK) .open(path) .ok()?; if !file.metadata().ok()?.is_file() { return None; } let mut source = String::new(); file.take(1024 * 1024 + 1) .read_to_string(&mut source) .ok()?; if source.len() > 1024 * 1024 { return None; } let mut in_entry = false; let mut fields = BTreeMap::new(); for line in source.lines().map(str::trim) { if line.starts_with('[') { in_entry = line == "[Desktop Entry]"; continue; } if !in_entry || line.starts_with('#') { continue; } if let Some((key, value)) = line.split_once('=') { fields.insert(key.trim().to_owned(), value.to_owned()); } } Some(fields) } fn enabled(entry: &BTreeMap, desktop: &str) -> bool { if entry.get("Type").is_none_or(|value| value != "Application") || entry.get("Hidden").is_some_and(|value| value == "true") || entry .get("X-GNOME-Autostart-enabled") .is_some_and(|value| value == "false") { return false; } let matching = |list: &str| { list.split(';') .filter(|name| !name.is_empty()) .any(|name| desktop.split(':').any(|current| current == name)) }; entry.get("OnlyShowIn").is_none_or(|list| matching(list)) && entry.get("NotShowIn").is_none_or(|list| !matching(list)) } fn available(program: &str) -> bool { use std::os::unix::fs::PermissionsExt; let executable = |path: &Path| { fs::metadata(path) .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) }; if program.contains('/') { executable(Path::new(program)) } else { env::var_os("PATH").is_some_and(|paths| { env::split_paths(&paths).any(|path| executable(&path.join(program))) }) } } #[cfg(test)] mod tests { use super::*; #[test] fn respects_hidden_and_desktop_filters() { let mut entry = BTreeMap::from([("Type".into(), "Application".into())]); assert!(enabled(&entry, "Ferese")); entry.insert("OnlyShowIn".into(), "GNOME;".into()); assert!(!enabled(&entry, "Ferese")); entry.insert("OnlyShowIn".into(), "Ferese;".into()); assert!(enabled(&entry, "Ferese:Other")); entry.insert("NotShowIn".into(), "Ferese;".into()); assert!(!enabled(&entry, "Ferese")); entry.remove("NotShowIn"); entry.insert("Hidden".into(), "true".into()); assert!(!enabled(&entry, "Ferese")); } }