Add background and USB consent with XDG session autostart

This commit is contained in:
Samuel Onoja committed 2026-09-29 22:53:30 +01:00
1 parent 915bc8afd6
commit ddbed192c0
13 files changed
+690 -11

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
use std::{
collections::BTreeMap,
env, fs,
io::Read,
os::unix::process::CommandExt,
path::{Path, PathBuf},
process::{Command, Stdio},
};
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
for key in ["WAYLAND_SOCKET", "FERESE_SHELL_CONTROL_SOCKET"] {
if let Some(fd) = env::var(key)
.ok()
.and_then(|value| value.parse::<i32>().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::<Vec<_>>();
if system_directories.is_empty() {
system_directories.push(PathBuf::from("/etc/xdg"));
}
let directories = std::iter::once(home)
.chain(system_directories)
.collect::<Vec<_>>();
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<BTreeMap<String, String>> {
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<String, String>, 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"));
}
}
+9 -1
View File
@@ -1,3 +1,5 @@
mod autostart;
use std::env;
use std::error::Error;
use std::fs;
@@ -9,6 +11,12 @@ use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
use serde_json::{Value, json};
fn main() -> Result<(), Box<dyn Error>> {
if env::args().nth(1).as_deref() == Some("autostart") {
if env::args().len() != 2 {
return Err("usage: feresectl autostart".into());
}
return autostart::run();
}
let (command, args) = parse_args(env::args().skip(1))?;
let request = Request {
version: VERSION,
@@ -118,7 +126,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
}
fn usage() -> String {
"usage: feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|reload-config>".to_owned()
"usage: feresectl autostart\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|reload-config>".to_owned()
}
#[cfg(test)]