Add background and USB consent with XDG session autostart
This commit is contained in:
1 parent
915bc8afd6
commit
ddbed192c0
13 files changed
+690
-11
No files matched your search
@@ -8,3 +8,4 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
ferese-ipc = { path = "../crates/ferese-ipc" }
|
||||
serde_json.workspace = true
|
||||
libc.workspace = true
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user