aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/utils/spawning.rs458
1 files changed, 256 insertions, 202 deletions
diff --git a/src/utils/spawning.rs b/src/utils/spawning.rs
index 494ec997..ad23a784 100644
--- a/src/utils/spawning.rs
+++ b/src/utils/spawning.rs
@@ -1,17 +1,12 @@
use std::ffi::OsStr;
-use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::path::Path;
-use std::process::{Command, Stdio};
+use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use std::{io, thread};
-use libc::close_range;
use niri_config::Environment;
-use smithay::reexports::rustix;
-use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
-use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
use crate::utils::expand_home;
@@ -81,244 +76,303 @@ fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl As
}
drop(env);
- // When running as a systemd session, we want to put children into their own transient scopes
- // in order to separate them from the niri process. This is helpful for example to prevent the
- // OOM killer from taking down niri together with a misbehaving client.
- //
- // Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
- // with a PID. Unfortunately, there seems to be a race in systemd where if the child exits at
- // just the right time, the transient unit will be created but empty, so it will linger around
- // forever.
- //
- // To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
- // intermediate child we will send back the grandchild PID, and in niri we will create a
- // transient scope with both our intermediate child and the grandchild PIDs set. Only then we
- // will signal our intermediate child to exit. This way, even if the grandchild exits quickly,
- // a non-empty scope will be created (with just our intermediate child), then cleaned up when
- // our intermediate child exits.
-
- // Make a pipe to receive the grandchild PID.
- let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
- .map_err(|err| {
- warn!("error creating a pipe to transfer child PID: {err:?}");
- })
- .ok()
- .unzip();
- // Make a pipe to wait in the intermediate child.
- let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
- .map_err(|err| {
- warn!("error creating a pipe for child to wait on: {err:?}");
- })
- .ok()
- .unzip();
+ let Some(mut child) = do_spawn(command, process) else {
+ return;
+ };
- unsafe {
- // The fds will be duplicated after a fork and closed on exec or exit automatically. Get
- // the raw fd inside so that it's not closed any extra times.
- let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
- let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
- let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
- let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
+ match child.wait() {
+ Ok(status) => {
+ if !status.success() {
+ warn!("child did not exit successfully: {status:?}");
+ }
+ }
+ Err(err) => {
+ warn!("error waiting for child: {err:?}");
+ }
+ }
+}
+#[cfg(not(feature = "systemd"))]
+fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
+ unsafe {
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
- // Close FDs that we don't need. Especially important for the write ones to unblock the
- // readers.
- if let Some(fd) = pipe_pid_read_fd.take() {
- close(fd);
- }
- if let Some(fd) = pipe_wait_write_fd.take() {
- close(fd);
- }
-
- // Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
- let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
- let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
-
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
- grandchild_pid => {
- // Send back the PID.
- if let Some(pipe) = pipe_pid_write {
- let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
- }
-
- // Wait until the parent signals us to exit.
- if let Some(pipe) = pipe_wait_read {
- // We're going to exit afterwards. Close all other FDs to allow
- // Command::spawn() to return in the parent process.
- let raw = pipe.as_raw_fd() as u32;
- let _ = close_range(0, raw - 1, 0);
- let _ = close_range(raw + 1, !0, 0);
-
- let _ = read_all(pipe, &mut [0]);
- }
-
- libc::_exit(0)
- }
+ _ => libc::_exit(0),
}
Ok(())
});
}
- let mut child = match process.spawn() {
+ let child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
- return;
+ return None;
}
};
- drop(pipe_pid_write);
- drop(pipe_wait_read);
-
- // Wait for the grandchild PID.
- if let Some(pipe) = pipe_pid_read {
- let mut buf = [0; 4];
- match read_all(pipe, &mut buf) {
- Ok(()) => {
- let pid = i32::from_ne_bytes(buf);
- trace!("spawned PID: {pid}");
-
- // Start a systemd scope for the grandchild.
- #[cfg(feature = "systemd")]
- if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
- trace!("error starting systemd scope for spawned command: {err:?}");
+ Some(child)
+}
+
+#[cfg(feature = "systemd")]
+use systemd::do_spawn;
+
+#[cfg(feature = "systemd")]
+mod systemd {
+ use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
+
+ use smithay::reexports::rustix;
+ use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
+ use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
+
+ use super::*;
+
+ pub fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
+ use libc::close_range;
+
+ // When running as a systemd session, we want to put children into their own transient
+ // scopes in order to separate them from the niri process. This is helpful for
+ // example to prevent the OOM killer from taking down niri together with a
+ // misbehaving client.
+ //
+ // Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
+ // with a PID. Unfortunately, there seems to be a race in systemd where if the child exits
+ // at just the right time, the transient unit will be created but empty, so it will
+ // linger around forever.
+ //
+ // To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
+ // intermediate child we will send back the grandchild PID, and in niri we will create a
+ // transient scope with both our intermediate child and the grandchild PIDs set. Only then
+ // we will signal our intermediate child to exit. This way, even if the grandchild
+ // exits quickly, a non-empty scope will be created (with just our intermediate
+ // child), then cleaned up when our intermediate child exits.
+
+ // Make a pipe to receive the grandchild PID.
+
+ let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
+ .map_err(|err| {
+ warn!("error creating a pipe to transfer child PID: {err:?}");
+ })
+ .ok()
+ .unzip();
+ // Make a pipe to wait in the intermediate child.
+ let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
+ .map_err(|err| {
+ warn!("error creating a pipe for child to wait on: {err:?}");
+ })
+ .ok()
+ .unzip();
+
+ unsafe {
+ // The fds will be duplicated after a fork and closed on exec or exit automatically. Get
+ // the raw fd inside so that it's not closed any extra times.
+ let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
+ let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
+ let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
+ let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
+
+ // Double-fork to avoid having to waitpid the child.
+ process.pre_exec(move || {
+ // Close FDs that we don't need. Especially important for the write ones to unblock
+ // the readers.
+ if let Some(fd) = pipe_pid_read_fd.take() {
+ close(fd);
}
- }
+ if let Some(fd) = pipe_wait_write_fd.take() {
+ close(fd);
+ }
+
+ // Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
+ let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
+ let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
+
+ match libc::fork() {
+ -1 => return Err(io::Error::last_os_error()),
+ 0 => (),
+ grandchild_pid => {
+ // Send back the PID.
+ if let Some(pipe) = pipe_pid_write {
+ let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
+ }
+
+ // Wait until the parent signals us to exit.
+ if let Some(pipe) = pipe_wait_read {
+ // We're going to exit afterwards. Close all other FDs to allow
+ // Command::spawn() to return in the parent process.
+ let raw = pipe.as_raw_fd() as u32;
+ let _ = close_range(0, raw - 1, 0);
+ let _ = close_range(raw + 1, !0, 0);
+
+ let _ = read_all(pipe, &mut [0]);
+ }
+
+ libc::_exit(0)
+ }
+ }
+
+ Ok(())
+ });
+ }
+
+ let child = match process.spawn() {
+ Ok(child) => child,
Err(err) => {
- warn!("error reading child PID: {err:?}");
+ warn!("error spawning {command:?}: {err:?}");
+ return None;
+ }
+ };
+
+ drop(pipe_pid_write);
+ drop(pipe_wait_read);
+
+ // Wait for the grandchild PID.
+ if let Some(pipe) = pipe_pid_read {
+ let mut buf = [0; 4];
+ match read_all(pipe, &mut buf) {
+ Ok(()) => {
+ let pid = i32::from_ne_bytes(buf);
+ trace!("spawned PID: {pid}");
+
+ // Start a systemd scope for the grandchild.
+ #[cfg(feature = "systemd")]
+ 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:?}");
+ }
}
}
+
+ // Signal the intermediate child to exit now that we're done trying to creating a systemd
+ // scope.
+ trace!("signaling child to exit");
+ drop(pipe_wait_write);
+
+ Some(child)
}
- // Signal the intermediate child to exit now that we're done trying to creating a systemd scope.
- trace!("signaling child to exit");
- drop(pipe_wait_write);
+ #[cfg(feature = "systemd")]
+ fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
+ let mut written = 0;
+ loop {
+ let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
+ if n == 0 {
+ return Err(rustix::io::Errno::CANCELED);
+ }
- match child.wait() {
- Ok(status) => {
- if !status.success() {
- warn!("child did not exit successfully: {status:?}");
+ written += n;
+ if written == buf.len() {
+ return Ok(());
}
}
- Err(err) => {
- warn!("error waiting for child: {err:?}");
- }
}
-}
-fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
- let mut written = 0;
- loop {
- let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
- if n == 0 {
- return Err(rustix::io::Errno::CANCELED);
- }
+ #[cfg(feature = "systemd")]
+ fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
+ let mut start = 0;
+ loop {
+ let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
+ if n == 0 {
+ return Err(rustix::io::Errno::CANCELED);
+ }
- written += n;
- if written == buf.len() {
- return Ok(());
+ start += n;
+ if start == buf.len() {
+ return Ok(());
+ }
}
}
-}
-
-fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
- let mut start = 0;
- loop {
- let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
- if n == 0 {
- return Err(rustix::io::Errno::CANCELED);
- }
- start += n;
- if start == buf.len() {
+ /// 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 = "systemd")]
+ fn start_systemd_scope(
+ name: &OsStr,
+ intermediate_pid: u32,
+ child_pid: u32,
+ ) -> anyhow::Result<()> {
+ use std::fmt::Write as _;
+ use std::os::unix::ffi::OsStrExt;
+ use std::sync::OnceLock;
+
+ use anyhow::Context;
+ use zbus::zvariant::{OwnedObjectPath, Value};
+
+ use crate::utils::IS_SYSTEMD_SERVICE;
+
+ // We only start transient scopes if we're a systemd service ourselves.
+ if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
return Ok(());
}
- }
-}
-
-/// 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 = "systemd")]
-fn start_systemd_scope(name: &OsStr, intermediate_pid: u32, child_pid: u32) -> anyhow::Result<()> {
- use std::fmt::Write as _;
- use std::os::unix::ffi::OsStrExt;
- use std::sync::OnceLock;
-
- use anyhow::Context;
- use zbus::zvariant::{OwnedObjectPath, Value};
-
- use crate::utils::IS_SYSTEMD_SERVICE;
- // 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!();
+ let _span = tracy_client::span!();
- // Extract the basename.
- let name = Path::new(name).file_name().unwrap_or(name);
+ // Extract the basename.
+ let name = Path::new(name).file_name().unwrap_or(name);
- let mut scope_name = String::from("app-niri-");
+ 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}");
+ // 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<zbus::Result<zbus::blocking::Connection>> = 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;
+ let _ = write!(scope_name, "-{child_pid}.scope");
+
+ // Ask systemd to start a transient scope.
+ static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = 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(())
+ Ok(())
+ }
}