From 6ba24e341fd1e0a8b387c705ce34cc3f19dcf375 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Mon, 19 Feb 2024 21:58:36 +0400 Subject: utils/spawn: Put processes into systemd scopes This separates them from the niri scope for the purposes of e.g. the OOM killer only killing the app and not the compositor. --- src/utils.rs | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) (limited to 'src/utils.rs') diff --git a/src/utils.rs b/src/utils.rs index aedd8058..298dace1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,6 +13,7 @@ use std::time::Duration; use anyhow::{ensure, Context}; use directories::UserDirs; use git_version::git_version; +use libc::close_range; use niri_config::Config; use smithay::output::Output; use smithay::reexports::rustix; @@ -126,6 +127,22 @@ fn spawn_sync(command: impl AsRef, args: impl IntoIterator, args: impl IntoIterator return Err(io::Error::last_os_error()), @@ -158,6 +190,17 @@ fn spawn_sync(command: impl AsRef, args: impl IntoIterator, args: impl IntoIterator, args: impl IntoIterator { let pid = i32::from_ne_bytes(buf); trace!("spawned PID: {pid}"); + + // Start a systemd scope for the grandchild. + #[cfg(feature = "dbus")] + if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) { + trace!("error starting systemd scope for spawned command: {err:?}"); + } } Err(err) => { warn!("error reading child PID: {err:?}"); @@ -190,6 +240,10 @@ fn spawn_sync(command: impl AsRef, args: impl IntoIterator { if !status.success() { @@ -232,6 +286,88 @@ fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> { } } +pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false); + +/// Puts a (newly spawned) pid into a transient systemd scope. +/// +/// This separates the pid from the compositor scope, which for example prevents the OOM killer +/// from bringing down the compositor together with a misbehaving client. +#[cfg(feature = "dbus")] +fn start_systemd_scope(name: &OsStr, intermediate_pid: u32, child_pid: u32) -> anyhow::Result<()> { + use std::fmt::Write as _; + use std::path::Path; + use std::sync::OnceLock; + + use zbus::zvariant::{OwnedObjectPath, Value}; + + // We only start transient scopes if we're a systemd service ourselves. + if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) { + return Ok(()); + } + + let _span = tracy_client::span!(); + + // Extract the basename. + let name = Path::new(name).file_name().unwrap_or(name); + + let mut scope_name = String::from("app-niri-"); + + // Escape for systemd similarly to libgnome-desktop, which says it had adapted this from + // systemd source. + for &c in name.as_bytes() { + if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') { + scope_name.push(char::from(c)); + } else { + let _ = write!(scope_name, "\\x{c:02x}"); + } + } + + let _ = write!(scope_name, "-{child_pid}.scope"); + + // Ask systemd to start a transient scope. + static CONNECTION: OnceLock> = OnceLock::new(); + let conn = CONNECTION + .get_or_init(zbus::blocking::Connection::session) + .clone() + .context("error connecting to session bus")?; + + let proxy = zbus::blocking::Proxy::new( + &conn, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + ) + .context("error creating a Proxy")?; + + let signals = proxy + .receive_signal("JobRemoved") + .context("error creating a signal iterator")?; + + let pids: &[_] = &[intermediate_pid, child_pid]; + let properties: &[_] = &[ + ("PIDs", Value::new(pids)), + ("CollectMode", Value::new("inactive-or-failed")), + ]; + let aux: &[(&str, &[(&str, Value)])] = &[]; + + let job: OwnedObjectPath = proxy + .call("StartTransientUnit", &(scope_name, "fail", properties, aux)) + .context("error calling StartTransientUnit")?; + + trace!("waiting for JobRemoved"); + for message in signals { + let body: (u32, OwnedObjectPath, &str, &str) = + message.body().context("error parsing signal")?; + + if body.1 == job { + // Our transient unit had started, we're good to exit the intermediate child. + break; + } + } + + Ok(()) +} + pub fn write_png_rgba8( w: impl Write, width: u32, -- cgit