Skip to main content

shrub_core/sys/unix/
commands.rs

1//! Command execution utilities.
2
3use std::ffi::{CStr, CString};
4pub use std::os::unix::process::{CommandExt, ExitStatusExt};
5use std::sync::Arc;
6
7use command_fds::{CommandFdExt, FdMapping};
8
9use crate::{ShellFd, error, openfiles};
10
11/// Extension trait for injecting file descriptors into commands.
12pub trait CommandFdInjectionExt {
13    /// Injects the given open files as file descriptors into the command.
14    ///
15    /// # Arguments
16    ///
17    /// * `open_files` - A mapping of child file descriptors to open files.
18    ///
19    /// # Errors
20    ///
21    /// - An error if a file's descriptor cannot be duplicated.
22    /// - [`crate::error::ErrorKind::ChildCreationFailure`] if the descriptor
23    ///   mappings cannot be applied to the command, for example because two
24    ///   files map to the same child descriptor.
25    fn inject_fds(
26        &mut self,
27        open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
28    ) -> Result<(), error::Error>;
29}
30
31impl CommandFdInjectionExt for std::process::Command {
32    fn inject_fds(
33        &mut self,
34        open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
35    ) -> Result<(), error::Error> {
36        let fd_mappings: Vec<FdMapping> = open_files
37            .map(|(child_fd, open_file)| -> Result<FdMapping, error::Error> {
38                let parent_fd = open_file.try_clone_to_owned()?;
39                Ok(FdMapping {
40                    child_fd,
41                    parent_fd,
42                })
43            })
44            .collect::<Result<Vec<_>, _>>()?;
45
46        self.fd_mappings(fd_mappings)
47            .map_err(|_e| error::ErrorKind::ChildCreationFailure)?;
48
49        Ok(())
50    }
51}
52
53/// Extension trait for arranging for commands to take the foreground.
54pub trait CommandFgControlExt {
55    /// Arranges for the command to take the foreground when it is executed.
56    fn take_foreground(&mut self);
57    /// Arranges for the command to become a session leader when it is executed.
58    fn lead_session(&mut self);
59}
60
61impl CommandFgControlExt for std::process::Command {
62    fn take_foreground(&mut self) {
63        // SAFETY:
64        // This arranges for a provided function to run in the context of
65        // the forked process before it exec's the target command. In general,
66        // rust can't guarantee safety of code running in such a context.
67        unsafe {
68            self.pre_exec(pre_exec_take_foreground);
69        }
70    }
71
72    fn lead_session(&mut self) {
73        // SAFETY:
74        // This arranges for a provided function to run in the context of
75        // the forked process before it exec's the target command. In general,
76        // rust can't guarantee safety of code running in such a context.
77        unsafe {
78            self.pre_exec(pre_exec_lead_session);
79        }
80    }
81}
82
83fn pre_exec_take_foreground() -> Result<(), std::io::Error> {
84    use crate::sys;
85
86    sys::terminal::move_self_to_foreground()?;
87    Ok(())
88}
89
90fn pre_exec_lead_session() -> Result<(), std::io::Error> {
91    if let Err(e) = nix::unistd::setsid() {
92        return Err(std::io::Error::other(format!(
93            "failed to become session leader: {e}"
94        )));
95    }
96
97    #[cfg(not(target_os = "macos"))]
98    let control = libc::TIOCSCTTY;
99    #[cfg(target_os = "macos")]
100    let control: u64 = libc::TIOCSCTTY.into();
101
102    // SAFETY:
103    // This is calling a libc function to set the controlling terminal.
104    let result = unsafe { libc::ioctl(0, control, 0) };
105    if result != 0 {
106        return Err(std::io::Error::other("failed to set controlling terminal"));
107    }
108
109    Ok(())
110}
111
112/// Extension trait for moving a command into a cgroup before it runs.
113pub trait CommandCgroupExt {
114    /// Arranges for the command to write `0` to `cgroup_procs`, a cgroup v2's
115    /// `cgroup.procs` file, after it is forked and before it runs its program,
116    /// which moves it into that cgroup.
117    ///
118    /// # Errors
119    ///
120    /// None here: a child that cannot open or write the file fails its spawn.
121    fn join_cgroup(&mut self, cgroup_procs: Arc<CString>) -> Result<(), error::Error>;
122}
123
124impl CommandCgroupExt for std::process::Command {
125    fn join_cgroup(&mut self, cgroup_procs: Arc<CString>) -> Result<(), error::Error> {
126        // SAFETY:
127        // This arranges for a provided function to run in the context of
128        // the forked process before it exec's the target command. The
129        // function allocates nothing and only calls open, write and close,
130        // which are async-signal-safe.
131        unsafe {
132            self.pre_exec(move || pre_exec_join_cgroup(&cgroup_procs));
133        }
134        Ok(())
135    }
136}
137
138fn pre_exec_join_cgroup(cgroup_procs: &CStr) -> Result<(), std::io::Error> {
139    use nix::fcntl::{OFlag, open};
140    use nix::sys::stat::Mode;
141
142    let file = open(
143        cgroup_procs,
144        OFlag::O_WRONLY | OFlag::O_CLOEXEC,
145        Mode::empty(),
146    )?;
147    nix::unistd::write(&file, b"0")?;
148    Ok(())
149}
150
151/// Whether nothing exists at `path`, such as a `cgroup.procs` file whose
152/// cgroup was removed.
153pub(crate) fn is_missing(path: &CStr) -> bool {
154    use std::os::unix::ffi::OsStrExt as _;
155
156    !std::path::Path::new(std::ffi::OsStr::from_bytes(path.to_bytes())).exists()
157}