#[cfg(unix)] use std::io::prelude::*; use std::{ io, process::{Child, Command}, sync::{atomic::AtomicU32, Arc}, }; use crate::ExitStatus; #[cfg(unix)] use std::{io::IsTerminal, sync::atomic::Ordering}; #[cfg(unix)] pub use foreground_pgroup::stdin_fd; #[cfg(unix)] use nix::{sys::signal, sys::wait, unistd::Pid}; /// A simple wrapper for [`std::process::Child`] /// /// It can only be created by [`ForegroundChild::spawn`]. /// /// # Spawn behavior /// ## Unix /// /// For interactive shells, the spawned child process will get its own process group id, /// and it will be put in the foreground (by making stdin belong to the child's process group). /// On drop, the calling process's group will become the foreground process group once again. /// /// For non-interactive mode, processes are spawned normally without any foreground process handling. /// /// ## Other systems /// /// It does nothing special on non-unix systems, so `spawn` is the same as [`std::process::Command::spawn`]. pub struct ForegroundChild { inner: Child, #[cfg(unix)] pipeline_state: Option>, } impl ForegroundChild { #[cfg(not(unix))] pub fn spawn(mut command: Command) -> io::Result { command.spawn().map(|child| Self { inner: child }) } #[cfg(unix)] pub fn spawn( mut command: Command, interactive: bool, pipeline_state: &Arc<(AtomicU32, AtomicU32)>, ) -> io::Result { if interactive && io::stdin().is_terminal() { let (pgrp, pcnt) = pipeline_state.as_ref(); let existing_pgrp = pgrp.load(Ordering::SeqCst); foreground_pgroup::prepare_command(&mut command, existing_pgrp); command .spawn() .map(|child| { foreground_pgroup::set(&child, existing_pgrp); let _ = pcnt.fetch_add(1, Ordering::SeqCst); if existing_pgrp == 0 { pgrp.store(child.id(), Ordering::SeqCst); } Self { inner: child, pipeline_state: Some(pipeline_state.clone()), } }) .inspect_err(|_e| { foreground_pgroup::reset(); }) } else { command.spawn().map(|child| Self { inner: child, pipeline_state: None, }) } } pub fn wait(&mut self) -> io::Result { #[cfg(unix)] { // the child may be stopped multiple times, we loop until it exits loop { let child_pid = Pid::from_raw(self.inner.id() as i32); let status = wait::waitpid(child_pid, Some(wait::WaitPidFlag::WUNTRACED)); match status { Err(e) => { drop(self.inner.stdin.take()); return Err(e.into()); } Ok(wait::WaitStatus::Exited(_, status)) => { drop(self.inner.stdin.take()); return Ok(ExitStatus::Exited(status)); } Ok(wait::WaitStatus::Signaled(_, signal, core_dumped)) => { drop(self.inner.stdin.take()); return Ok(ExitStatus::Signaled { signal: signal as i32, core_dumped, }); } Ok(wait::WaitStatus::Stopped(_, _)) => { println!("nushell currently does not support background jobs"); // acquire terminal in order to be able to read from stdin foreground_pgroup::reset(); let mut stdin = io::stdin(); let mut stdout = io::stdout(); write!(stdout, "press any key to continue")?; stdout.flush()?; stdin.read_exact(&mut [0u8])?; // bring child's pg back into foreground and continue it if let Some(state) = self.pipeline_state.as_ref() { let existing_pgrp = state.0.load(Ordering::SeqCst); foreground_pgroup::set(&self.inner, existing_pgrp); } signal::killpg(child_pid, signal::SIGCONT)?; } Ok(_) => { // keep waiting } }; } } #[cfg(not(unix))] self.as_mut().wait().map(Into::into) } } impl AsMut for ForegroundChild { fn as_mut(&mut self) -> &mut Child { &mut self.inner } } #[cfg(unix)] impl Drop for ForegroundChild { fn drop(&mut self) { if let Some((pgrp, pcnt)) = self.pipeline_state.as_deref() { if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 { pgrp.store(0, Ordering::SeqCst); foreground_pgroup::reset() } } } } /// Keeps a specific already existing process in the foreground as long as the [`ForegroundGuard`]. /// If the process needs to be spawned in the foreground, use [`ForegroundChild`] instead. This is /// used to temporarily bring plugin processes into the foreground. /// /// # OS-specific behavior /// ## Unix /// /// If there is already a foreground external process running, spawned with [`ForegroundChild`], /// this expects the process ID to remain in the process group created by the [`ForegroundChild`] /// for the lifetime of the guard, and keeps the terminal controlling process group set to that. /// If there is no foreground external process running, this sets the foreground process group to /// the plugin's process ID. The process group that is expected can be retrieved with /// [`.pgrp()`](Self::pgrp) if different from the plugin process ID. /// /// ## Other systems /// /// It does nothing special on non-unix systems. #[derive(Debug)] pub struct ForegroundGuard { #[cfg(unix)] pgrp: Option, #[cfg(unix)] pipeline_state: Arc<(AtomicU32, AtomicU32)>, } impl ForegroundGuard { /// Move the given process to the foreground. #[cfg(unix)] pub fn new( pid: u32, pipeline_state: &Arc<(AtomicU32, AtomicU32)>, ) -> std::io::Result { use nix::unistd::{self, Pid}; let pid_nix = Pid::from_raw(pid as i32); let (pgrp, pcnt) = pipeline_state.as_ref(); // Might have to retry due to race conditions on the atomics loop { // Try to give control to the child, if there isn't currently a foreground group if pgrp .compare_exchange(0, pid, Ordering::SeqCst, Ordering::SeqCst) .is_ok() { let _ = pcnt.fetch_add(1, Ordering::SeqCst); // We don't need the child to change process group. Make the guard now so that if there // is an error, it will be cleaned up let guard = ForegroundGuard { pgrp: None, pipeline_state: pipeline_state.clone(), }; log::trace!("Giving control of the terminal to the plugin group, pid={pid}"); // Set the terminal controlling process group to the child process unistd::tcsetpgrp(unsafe { stdin_fd() }, pid_nix)?; return Ok(guard); } else if pcnt .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { // Avoid a race condition: only increment if count is > 0 if count > 0 { Some(count + 1) } else { None } }) .is_ok() { // We successfully added another count to the foreground process group, which means // we only need to tell the child process to join this one let pgrp = pgrp.load(Ordering::SeqCst); log::trace!( "Will ask the plugin pid={pid} to join pgrp={pgrp} for control of the \ terminal" ); return Ok(ForegroundGuard { pgrp: Some(pgrp), pipeline_state: pipeline_state.clone(), }); } else { // The state has changed, we'll have to retry continue; } } } /// Move the given process to the foreground. #[cfg(not(unix))] pub fn new( pid: u32, pipeline_state: &Arc<(AtomicU32, AtomicU32)>, ) -> std::io::Result { let _ = (pid, pipeline_state); Ok(ForegroundGuard {}) } /// If the child process is expected to join a different process group to be in the foreground, /// this returns `Some(pgrp)`. This only ever returns `Some` on Unix. pub fn pgrp(&self) -> Option { #[cfg(unix)] { self.pgrp } #[cfg(not(unix))] { None } } /// This should only be called once by `Drop` fn reset_internal(&mut self) { #[cfg(unix)] { log::trace!("Leaving the foreground group"); let (pgrp, pcnt) = self.pipeline_state.as_ref(); if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 { // Clean up if we are the last one around pgrp.store(0, Ordering::SeqCst); foreground_pgroup::reset() } } } } impl Drop for ForegroundGuard { fn drop(&mut self) { self.reset_internal(); } } // It's a simpler version of fish shell's external process handling. #[cfg(unix)] mod foreground_pgroup { use nix::{ sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}, unistd::{self, Pid}, }; use std::{ os::{ fd::{AsFd, BorrowedFd}, unix::prelude::CommandExt, }, process::{Child, Command}, }; /// Alternative to having to call `std::io::stdin()` just to get the file descriptor of stdin /// /// # Safety /// I/O safety of reading from `STDIN_FILENO` unclear. /// /// Currently only intended to access `tcsetpgrp` and `tcgetpgrp` with the I/O safe `nix` /// interface. pub unsafe fn stdin_fd() -> impl AsFd { unsafe { BorrowedFd::borrow_raw(nix::libc::STDIN_FILENO) } } pub fn prepare_command(external_command: &mut Command, existing_pgrp: u32) { unsafe { // Safety: // POSIX only allows async-signal-safe functions to be called. // `sigaction` and `getpid` are async-signal-safe according to: // https://manpages.ubuntu.com/manpages/bionic/man7/signal-safety.7.html // Also, `set_foreground_pid` is async-signal-safe. external_command.pre_exec(move || { // When this callback is run, std::process has already: // - reset SIGPIPE to SIG_DFL // According to glibc's job control manual: // https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html // This has to be done *both* in the parent and here in the child due to race conditions. set_foreground_pid(Pid::this(), existing_pgrp); // Reset signal handlers for child, sync with `terminal.rs` let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); // SIGINT has special handling let _ = sigaction(Signal::SIGQUIT, &default); // We don't support background jobs, so keep some signals blocked for now // let _ = sigaction(Signal::SIGTTIN, &default); // let _ = sigaction(Signal::SIGTTOU, &default); // We do need to reset SIGTSTP though, since some TUI // applications implement their own Ctrl-Z handling, and // ForegroundChild::wait() needs to be able to react to the // child being stopped. let _ = sigaction(Signal::SIGTSTP, &default); let _ = sigaction(Signal::SIGTERM, &default); Ok(()) }); } } pub fn set(process: &Child, existing_pgrp: u32) { set_foreground_pid(Pid::from_raw(process.id() as i32), existing_pgrp); } fn set_foreground_pid(pid: Pid, existing_pgrp: u32) { // Safety: needs to be async-signal-safe. // `setpgid` and `tcsetpgrp` are async-signal-safe. // `existing_pgrp` is 0 when we don't have an existing foreground process in the pipeline. // A pgrp of 0 means the calling process's pid for `setpgid`. But not for `tcsetpgrp`. let pgrp = if existing_pgrp == 0 { pid } else { Pid::from_raw(existing_pgrp as i32) }; let _ = unistd::setpgid(pid, pgrp); let _ = unistd::tcsetpgrp(unsafe { stdin_fd() }, pgrp); } /// Reset the foreground process group to the shell pub fn reset() { if let Err(e) = unistd::tcsetpgrp(unsafe { stdin_fd() }, unistd::getpgrp()) { eprintln!("ERROR: reset foreground id failed, tcsetpgrp result: {e:?}"); } } }