diff --git a/compositor/src/daemon.rs b/compositor/src/daemon.rs index d65971d..d266961 100644 --- a/compositor/src/daemon.rs +++ b/compositor/src/daemon.rs @@ -1,7 +1,15 @@ //! 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::rc::Rc; 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::config::DaemonConfig; use crate::private_client::ClientCapabilities; @@ -13,9 +21,109 @@ struct Service { finished: bool, } -pub(crate) struct Runner(Vec); +pub(crate) struct Runner( + Vec, + HashMap, + Option<(RegistrationToken, Instant)>, +); + +fn pidfd(pid: u32) -> std::io::Result { + // 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 { + pub fn start(state: &mut Ferese) -> Rc> { + 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>, 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::>(); + 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) { let mut previous = std::mem::take(&mut self.0); for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) { @@ -62,6 +170,8 @@ impl Runner { finished: false, }) .collect(), + HashMap::new(), + None, ) } @@ -117,6 +227,29 @@ impl Drop for Runner { #[cfg(test)] mod tests { 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::::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] fn live_reconcile_keeps_existing_process_and_changes_policy_without_restart() { let mut config = DaemonConfig { diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 6927da1..27900a9 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -80,7 +80,7 @@ fn main() -> Result<(), Box> { let directory = config::config_path() .and_then(|path| path.parent().map(ToOwned::to_owned)) .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 signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM])?; event_loop @@ -95,22 +95,10 @@ fn main() -> Result<(), Box> { theme::init(&mut event_loop, &mut state, candidate)?; overview::init_font_loader(&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"); 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( - &config.autostart, - 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 runner = daemon::Runner::start(&mut state); + state.daemons = Some(runner.clone()); let reload_handle = event_loop.handle(); let result = event_loop.run(None, &mut state, |state| { state.poll_theme(); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 8214b00..0accbb0 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -334,6 +334,7 @@ pub struct Ferese { output_identity_ids: HashMap, pub(crate) windows: WindowRegistry, pub(crate) render: crate::render::RenderResources, + pub(crate) daemons: Option>>, pub(crate) display_presentation: crate::display_presentation::DisplayPresentation, pub(crate) nested_backend: Option, pub(crate) wallpaper: WallpaperState, @@ -563,6 +564,7 @@ impl Ferese { windows: WindowRegistry::default(), render: Default::default(), display_presentation: Default::default(), + daemons: None, nested_backend: None, wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), window_stack: WindowStack::default(), @@ -747,6 +749,9 @@ impl Ferese { self.viewport_spring_config = config.viewport_spring_config; self.output_profiles = config.output_profiles; self.autostart = config.autostart; + if let Some(runner) = self.daemons.clone() { + crate::daemon::Runner::refresh(&runner, self); + } if touchpad_changed { crate::backends::direct::reload_input_devices(self);