Files
sausage/userland/brush/source/brush-core/src/processes.rs
T
2026-11-11 22:04:16 +00:00

191 lines
7.2 KiB
Rust

//! Process management
use std::{path::PathBuf, sync::PoisonError};
use futures::FutureExt;
use crate::{ExecutionResult, error, openfiles::OpenFiles, sys};
/// A waitable future that will yield the results of a child process's execution.
pub(crate) type WaitableChildProcess = std::pin::Pin<
Box<dyn futures::Future<Output = Result<ExecutionResult, std::io::Error>> + Send>,
>;
/// Tracks a child process being awaited.
pub struct ChildProcess {
/// A waitable future that will yield the results of a child process's execution.
/// It's only ever reached through `&mut self` (with `get_mut`, which never locks); the
/// mutex just makes this `Sync` without the future having to be.
exec_future: std::sync::Mutex<WaitableChildProcess>,
/// The process ID of the child, if it's a native process of the shell's.
pid: Option<sys::process::ProcessId>,
/// If available, the process group ID of the child.
pgid: Option<sys::process::ProcessId>,
}
impl ChildProcess {
/// Wraps a future that yields the result of a process, for a
/// [`ProcessLauncher`](crate::extensions::ProcessLauncher) to return.
///
/// # Arguments
///
/// * `future` - The future that will yield the process's result.
pub fn from_future(
future: impl futures::Future<Output = Result<ExecutionResult, std::io::Error>> + Send + 'static,
) -> Self {
Self {
exec_future: std::sync::Mutex::new(Box::pin(future)),
pid: None,
pgid: None,
}
}
/// Marks the process as a native child of the shell's, with the given IDs: job
/// control signals it, and waiting for it notices when it stops.
///
/// # Arguments
///
/// * `pid` - The process ID, if known.
/// * `pgid` - The process group ID, if the process has one.
#[allow(dead_code, reason = "unused on platforms that can't start processes")]
#[must_use]
pub(crate) const fn native_child(
mut self,
pid: Option<sys::process::ProcessId>,
pgid: Option<sys::process::ProcessId>,
) -> Self {
self.pid = pid;
self.pgid = pgid;
self
}
/// Returns the process's ID, if it's a native process of the shell's.
pub const fn pid(&self) -> Option<sys::process::ProcessId> {
self.pid
}
/// Returns the process's group ID.
pub const fn pgid(&self) -> Option<sys::process::ProcessId> {
self.pgid
}
/// Waits for the process to exit.
pub async fn wait(&mut self) -> Result<ProcessWaitResult, error::Error> {
#[allow(unused_mut, reason = "only mutated on some platforms")]
let mut sigtstp = sys::signal::tstp_signal_listener()?;
#[allow(unused_mut, reason = "only mutated on some platforms")]
let mut sigchld = sys::signal::chld_signal_listener()?;
// The child may have stopped before there were listeners to hear about it
// (e.g., straight after `fg` resumed it).
if let Some(pid) = self.pid
&& sys::signal::poll_for_stopped_child(pid)?
{
return Ok(ProcessWaitResult::Stopped);
}
#[allow(clippy::ignored_unit_patterns)]
loop {
tokio::select! {
output = self.exec_future.get_mut().unwrap_or_else(PoisonError::into_inner) => {
break Ok(ProcessWaitResult::Completed(output?))
},
// Only this child's stop counts: another wait may be under way at the same time
// (e.g., a background job's), for a child of its own, and the terminal's stop
// may reach the shell without stopping this child.
_ = sigtstp.recv() => {
if self.poll_for_stop()? {
break Ok(ProcessWaitResult::Stopped);
}
},
_ = sigchld.recv() => {
if self.poll_for_stop()? {
break Ok(ProcessWaitResult::Stopped);
}
},
_ = sys::signal::await_ctrl_c() => {
// SIGINT got thrown. Handle it and continue looping. The child should
// have received it as well, and either handled it or ended up getting
// terminated (in which case we'll see the child exit).
},
}
}
}
/// Returns whether the process has stopped, consuming its stop status.
fn poll_for_stop(&self) -> Result<bool, error::Error> {
match self.pid {
Some(pid) => sys::signal::poll_for_stopped_children(pid),
None => Ok(false),
}
}
pub(crate) fn poll(&mut self) -> Option<Result<ExecutionResult, error::Error>> {
let checkable_future = self
.exec_future
.get_mut()
.unwrap_or_else(PoisonError::into_inner);
checkable_future
.now_or_never()
.map(|result| result.map_err(Into::into))
}
}
/// Represents the result of waiting for an executing process.
pub enum ProcessWaitResult {
/// The process completed.
Completed(ExecutionResult),
/// The process stopped and has not yet completed.
Stopped,
}
/// A process for the shell to launch: a program the shell has already resolved
/// (builtins, functions, and the like never get this far), with everything it
/// should start with.
#[non_exhaustive]
pub struct LaunchRequest {
/// Path to the executable, as resolved by the shell; relative to
/// [`Self::working_dir`], if it's relative.
pub program: PathBuf,
/// The value the process should observe as `argv[0]`.
pub argv0: String,
/// Arguments to the process, not including `argv[0]`.
pub args: Vec<String>,
/// The complete environment for the process.
pub env: Vec<(String, String)>,
/// The working directory the process should start in.
pub working_dir: PathBuf,
/// The process's file descriptors. A standard stream (0, 1, or 2) that isn't
/// among them is closed; a launcher that can't start a process with one closed
/// gives it the null device, as the default launcher does, never one of the
/// shell's own. The shell's own standard streams
/// ([`OpenFile::Stdin`](crate::openfiles::OpenFile::Stdin), and so on) are
/// shared with the process, under whichever descriptors hold them.
pub open_files: OpenFiles,
/// The process group the process should run in.
pub process_group: ProcessGroup,
/// Whether to kill the process if the shell drops the [`ChildProcess`] for it
/// before it exits.
pub kill_on_drop: bool,
}
/// The process group a process should run in. Platforms and
/// [`ProcessLauncher`](crate::extensions::ProcessLauncher)s without process
/// groups ignore this.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProcessGroup {
/// The shell's process group.
Inherit,
/// An existing process group, with the given ID.
Join(sys::process::ProcessId),
/// A new process group, led by the process.
New {
/// Whether the new group should become the terminal's foreground group.
take_foreground: bool,
},
/// A new session, led by the process, with the terminal as its controlling
/// terminal.
NewSession,
}