Supervise daemons with pidfds and restart deadlines

This commit is contained in:
Samuel Onoja committed 2026-10-02 08:23:47 +01:00
1 parent 181b7bce9a
commit 8bb0b1da9c
3 files changed
+142 -16

No files matched your search

+134 -1
View File
@@ -1,7 +1,15 @@
//! Session-owned services. Never grant shell/effects privileges to daemons. //! Session-owned services. Never grant shell/effects privileges to daemons.
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::fd::{FromRawFd, OwnedFd};
use std::process::Child; use std::process::Child;
use std::rc::Rc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};
use crate::Ferese; use crate::Ferese;
use crate::config::DaemonConfig; use crate::config::DaemonConfig;
use crate::private_client::ClientCapabilities; use crate::private_client::ClientCapabilities;
@@ -13,9 +21,109 @@ struct Service {
finished: bool, finished: bool,
} }
pub(crate) struct Runner(Vec<Service>); pub(crate) struct Runner(
Vec<Service>,
HashMap<u32, RegistrationToken>,
Option<(RegistrationToken, Instant)>,
);
fn pidfd(pid: u32) -> std::io::Result<OwnedFd> {
// SAFETY: pidfd_open has no pointer arguments and returns a new owned fd.
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
// SAFETY: a successful syscall returned a unique valid descriptor.
Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
}
impl Runner { impl Runner {
pub fn start(state: &mut Ferese) -> Rc<RefCell<Self>> {
let runner = Rc::new(RefCell::new(Self::new(
&state.autostart,
state.direct_backend.is_none(),
)));
Self::refresh(&runner, state);
runner
}
pub fn refresh(owner: &Rc<RefCell<Self>>, state: &mut Ferese) {
let mut runner = owner.borrow_mut();
runner.tick(state);
let pids = runner
.0
.iter()
.filter_map(|service| service.child.as_ref().map(Child::id))
.collect::<Vec<_>>();
runner.1.retain(|pid, token| {
if pids.contains(pid) {
true
} else {
state.loop_handle.remove(*token);
false
}
});
let mut watch_retry = None;
for pid in pids {
if runner.1.contains_key(&pid) {
continue;
}
let weak = Rc::downgrade(owner);
let source = pidfd(pid).map(|fd| Generic::new(fd, Interest::READ, Mode::Level));
let result = source.and_then(|source| {
state
.loop_handle
.insert_source(source, move |_, _, state| {
if let Some(owner) = weak.upgrade() {
owner.borrow_mut().1.remove(&pid);
Self::refresh(&owner, state);
}
Ok(PostAction::Remove)
})
.map_err(std::io::Error::other)
});
match result {
Ok(token) => {
runner.1.insert(pid, token);
}
Err(error) => {
tracing::warn!(%error, pid, "could not watch daemon exit; retrying registration");
watch_retry = Some(Instant::now() + Duration::from_secs(5));
}
}
}
let deadline = runner
.0
.iter()
.filter(|service| service.child.is_none() && !service.finished)
.map(|service| service.next_start)
.chain(watch_retry)
.min();
if runner.2.as_ref().map(|(_, deadline)| *deadline) == deadline {
return;
}
if let Some((token, _)) = runner.2.take() {
state.loop_handle.remove(token);
}
if let Some(deadline) = deadline {
let weak = Rc::downgrade(owner);
match state
.loop_handle
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
if let Some(owner) = weak.upgrade() {
owner.borrow_mut().2 = None;
Self::refresh(&owner, state);
}
TimeoutAction::Drop
}) {
Ok(token) => runner.2 = Some((token, deadline)),
Err(error) => tracing::warn!(%error, "could not arm daemon restart deadline"),
}
}
}
pub fn reconcile(&mut self, configs: &[DaemonConfig], nested: bool) { pub fn reconcile(&mut self, configs: &[DaemonConfig], nested: bool) {
let mut previous = std::mem::take(&mut self.0); let mut previous = std::mem::take(&mut self.0);
for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) { for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) {
@@ -62,6 +170,8 @@ impl Runner {
finished: false, finished: false,
}) })
.collect(), .collect(),
HashMap::new(),
None,
) )
} }
@@ -117,6 +227,29 @@ impl Drop for Runner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn pidfd_wakes_the_loop_once_and_the_child_is_reaped() {
let mut child = std::process::Command::new("/bin/sh")
.args(["-c", "exit 7"])
.spawn()
.unwrap();
let fd = pidfd(child.id()).unwrap();
let mut events = smithay::reexports::calloop::EventLoop::<usize>::try_new().unwrap();
events
.handle()
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, count| {
assert_eq!(child.wait().unwrap().code(), Some(7));
*count += 1;
Ok(PostAction::Remove)
})
.unwrap();
let mut count = 0;
events.dispatch(Some(Duration::from_secs(2)), &mut count).unwrap();
assert_eq!(count, 1);
events.dispatch(Some(Duration::ZERO), &mut count).unwrap();
assert_eq!(count, 1);
}
#[test] #[test]
fn live_reconcile_keeps_existing_process_and_changes_policy_without_restart() { fn live_reconcile_keeps_existing_process_and_changes_policy_without_restart() {
let mut config = DaemonConfig { let mut config = DaemonConfig {
+3 -15
View File
@@ -80,7 +80,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let directory = config::config_path() let directory = config::config_path()
.and_then(|path| path.parent().map(ToOwned::to_owned)) .and_then(|path| path.parent().map(ToOwned::to_owned))
.unwrap_or_else(|| std::path::PathBuf::from(".")); .unwrap_or_else(|| std::path::PathBuf::from("."));
let (config, runtime, candidate) = theme::prepare(source, &directory)?; let (_, runtime, candidate) = theme::prepare(source, &directory)?;
let mut event_loop = EventLoop::try_new()?; let mut event_loop = EventLoop::try_new()?;
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM])?; let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM])?;
event_loop event_loop
@@ -95,22 +95,10 @@ fn main() -> Result<(), Box<dyn Error>> {
theme::init(&mut event_loop, &mut state, candidate)?; theme::init(&mut event_loop, &mut state, candidate)?;
overview::init_font_loader(&mut event_loop, &mut state)?; overview::init_font_loader(&mut event_loop, &mut state)?;
backends::init(launch.backend, &mut event_loop, &mut state)?; backends::init(launch.backend, &mut event_loop, &mut state)?;
use calloop::timer::{TimeoutAction, Timer};
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients"); info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities); let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
let runner = std::rc::Rc::new(std::cell::RefCell::new(daemon::Runner::new( let runner = daemon::Runner::start(&mut state);
&config.autostart, state.daemons = Some(runner.clone());
launch.backend == backends::BackendKind::Nested,
)));
runner.borrow_mut().tick(&mut state);
let services = runner.clone();
event_loop.handle().insert_source(
Timer::from_duration(Duration::from_secs(1)),
move |_, _, state: &mut Ferese| {
services.borrow_mut().tick(state);
TimeoutAction::ToDuration(Duration::from_secs(1))
},
)?;
let reload_handle = event_loop.handle(); let reload_handle = event_loop.handle();
let result = event_loop.run(None, &mut state, |state| { let result = event_loop.run(None, &mut state, |state| {
state.poll_theme(); state.poll_theme();
+5
View File
@@ -334,6 +334,7 @@ pub struct Ferese {
output_identity_ids: HashMap<String, OutputId>, output_identity_ids: HashMap<String, OutputId>,
pub(crate) windows: WindowRegistry<Window>, pub(crate) windows: WindowRegistry<Window>,
pub(crate) render: crate::render::RenderResources, pub(crate) render: crate::render::RenderResources,
pub(crate) daemons: Option<std::rc::Rc<std::cell::RefCell<crate::daemon::Runner>>>,
pub(crate) display_presentation: crate::display_presentation::DisplayPresentation, pub(crate) display_presentation: crate::display_presentation::DisplayPresentation,
pub(crate) nested_backend: Option<NestedBackend>, pub(crate) nested_backend: Option<NestedBackend>,
pub(crate) wallpaper: WallpaperState, pub(crate) wallpaper: WallpaperState,
@@ -563,6 +564,7 @@ impl Ferese {
windows: WindowRegistry::default(), windows: WindowRegistry::default(),
render: Default::default(), render: Default::default(),
display_presentation: Default::default(), display_presentation: Default::default(),
daemons: None,
nested_backend: None, nested_backend: None,
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
window_stack: WindowStack::default(), window_stack: WindowStack::default(),
@@ -747,6 +749,9 @@ impl Ferese {
self.viewport_spring_config = config.viewport_spring_config; self.viewport_spring_config = config.viewport_spring_config;
self.output_profiles = config.output_profiles; self.output_profiles = config.output_profiles;
self.autostart = config.autostart; self.autostart = config.autostart;
if let Some(runner) = self.daemons.clone() {
crate::daemon::Runner::refresh(&runner, self);
}
if touchpad_changed { if touchpad_changed {
crate::backends::direct::reload_input_devices(self); crate::backends::direct::reload_input_devices(self);