atagen

monolith

public
Star0
Watch0

Commit 7c46c935996a

tarball

atagen <boss@atagen.co>2026-10-17 05:10 UTC

1 file changed+29-3
monolith: reap spawned children from the event loop instead of a thread each

Every launch left an OS thread blocked in wait for the child's whole lifetime. A pidfd per child wakes the loop once at exit. A SIGCHLD handler would be simpler but would reap smithay's Xwayland child out from under it.
Modified file: crates/monolith/src/commands.rs+29 additions−3 deletions
@@ -3,7 +3,10 @@ use monolith_config::{Config, Mods, MoveMode};
33 use monolith_ipc::Command;
44 use monolith_layout::{ExpandScope, WindowId};
55 use smithay::input::keyboard::{Keysym, ModifiersState, xkb};
6-use std::process::Stdio;
6+use smithay::reexports::calloop::generic::Generic;
7+use smithay::reexports::calloop::{Interest, Mode, PostAction};
8+use std::os::fd::{FromRawFd, OwnedFd, RawFd};
9+use std::process::{Child, Stdio};
710
811 pub struct Binds(Vec<(Mods, Keysym, Command)>);
912
@@ -173,14 +176,37 @@ impl State {
173176 let token = self.launch_token();
174177 cmd.env("XDG_ACTIVATION_TOKEN", &token)
175178 .env("DESKTOP_STARTUP_ID", &token);
176- let mut child = cmd.spawn().map_err(|e| format!("{program}: {e}"))?;
179+ let child = cmd.spawn().map_err(|e| format!("{program}: {e}"))?;
177180 tracing::info!(pid = child.id(), ?argv, "spawned");
178181 let (output, tag) = self.launch_context();
179182 self.services.launches.record(child.id(), output, tag);
180- std::thread::spawn(move || child.wait());
183+ self.reap_on_exit(child);
181184 Ok(())
182185 }
183186
187+ /// Each child gets a pidfd, since a SIGCHLD reaper would also steal the
188+ /// exit of smithay's Xwayland.
189+ fn reap_on_exit(&mut self, mut child: Child) {
190+ let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, child.id() as libc::pid_t, 0) };
191+ if raw < 0 {
192+ std::thread::spawn(move || child.wait());
193+ return;
194+ }
195+ // SAFETY: pidfd_open returned a new fd that nothing else owns.
196+ let fd = unsafe { OwnedFd::from_raw_fd(raw as RawFd) };
197+ let mut child = Some(child);
198+ let source = Generic::new(fd, Interest::READ, Mode::Level);
199+ let inserted = self.core.handle.insert_source(source, move |_, _, _| {
200+ if let Some(mut c) = child.take() {
201+ let _ = c.wait();
202+ }
203+ Ok(PostAction::Remove)
204+ });
205+ if let Err(e) = inserted {
206+ tracing::debug!("cannot watch a spawned child: {e}");
207+ }
208+ }
209+
184210 /// Swaps in a freshly parsed config. A broken file leaves the running
185211 /// config untouched.
186212 pub fn reload(&mut self) -> Result<(), String> {