mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 14:40:06 +02:00
Do not block signals for child processes (#11402)
# Description / User-Facing Changes Signals are no longer blocked for child processes launched from both interactive and non-interactive mode. The only exception is that `SIGTSTP`, `SIGTTIN`, and `SIGTTOU` remain blocked for child processes launched only from **interactive** mode. This is to help prevent nushell from getting into an unrecoverable state, since we don't support background jobs. Anyways, this fully fixes #9026. # Other Notes - Needs Rust version `>= 1.66` for a fix in `std::process::Command::spawn`, but it looks our current Rust version is way above this. - Uses `sigaction` instead of `signal`, since the behavior of `signal` can apparently differ across systems. Also, the `sigaction` man page says: > The sigaction() function supersedes the signal() function, and should be used in preference. Additionally, using both `sigaction` and `signal` is not recommended. Since we were already using `sigaction` in some places (and possibly some of our dependencies as well), this PR replaces all usages of `signal`. # Tests Might want to wait for #11178 for testing.
This commit is contained in:
@ -1,66 +1,78 @@
|
||||
use std::{
|
||||
io,
|
||||
process::{Child, Command},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
io::IsTerminal,
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
/// A simple wrapper for `std::process::Command`
|
||||
/// A simple wrapper for [`std::process::Child`]
|
||||
///
|
||||
/// ## Spawn behavior
|
||||
/// ### Unix
|
||||
/// It can only be created by [`ForegroundChild::spawn`].
|
||||
///
|
||||
/// The spawned child process will get its own process group id, and it's going to foreground (by making stdin belong's to child's process group).
|
||||
/// # 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.
|
||||
///
|
||||
/// ### Windows
|
||||
/// It does nothing special on windows system, `spawn` is the same as [std::process::Command::spawn](std::process::Command::spawn)
|
||||
pub struct ForegroundProcess {
|
||||
inner: Command,
|
||||
pipeline_state: Arc<(AtomicU32, AtomicU32)>,
|
||||
}
|
||||
|
||||
/// A simple wrapper for `std::process::Child`
|
||||
/// For non-interactive mode, processes are spawned normally without any foreground process handling.
|
||||
///
|
||||
/// It can only be created by `ForegroundProcess::spawn`.
|
||||
/// ## 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,
|
||||
pipeline_state: Arc<(AtomicU32, AtomicU32)>,
|
||||
interactive: bool,
|
||||
#[cfg(unix)]
|
||||
pipeline_state: Option<Arc<(AtomicU32, AtomicU32)>>,
|
||||
}
|
||||
|
||||
impl ForegroundProcess {
|
||||
pub fn new(cmd: Command, pipeline_state: Arc<(AtomicU32, AtomicU32)>) -> Self {
|
||||
Self {
|
||||
inner: cmd,
|
||||
pipeline_state,
|
||||
}
|
||||
impl ForegroundChild {
|
||||
#[cfg(not(unix))]
|
||||
pub fn spawn(mut command: Command) -> io::Result<Self> {
|
||||
command.spawn().map(|child| Self { inner: child })
|
||||
}
|
||||
|
||||
pub fn spawn(&mut self, interactive: bool) -> std::io::Result<ForegroundChild> {
|
||||
let (ref pgrp, ref pcnt) = *self.pipeline_state;
|
||||
let existing_pgrp = pgrp.load(Ordering::SeqCst);
|
||||
fg_process_setup::prepare_to_foreground(&mut self.inner, existing_pgrp, interactive);
|
||||
self.inner
|
||||
.spawn()
|
||||
.map(|child| {
|
||||
fg_process_setup::set_foreground(&child, existing_pgrp, interactive);
|
||||
let _ = pcnt.fetch_add(1, Ordering::SeqCst);
|
||||
if existing_pgrp == 0 {
|
||||
pgrp.store(child.id(), Ordering::SeqCst);
|
||||
}
|
||||
ForegroundChild {
|
||||
inner: child,
|
||||
pipeline_state: self.pipeline_state.clone(),
|
||||
interactive,
|
||||
}
|
||||
})
|
||||
.map_err(|e| {
|
||||
fg_process_setup::reset_foreground_id(interactive);
|
||||
e
|
||||
#[cfg(unix)]
|
||||
pub fn spawn(
|
||||
mut command: Command,
|
||||
interactive: bool,
|
||||
pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
|
||||
) -> io::Result<Self> {
|
||||
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()),
|
||||
}
|
||||
})
|
||||
.map_err(|e| {
|
||||
foreground_pgroup::reset();
|
||||
e
|
||||
})
|
||||
} else {
|
||||
command.spawn().map(|child| Self {
|
||||
inner: child,
|
||||
pipeline_state: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,122 +82,85 @@ impl AsMut<Child> for ForegroundChild {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for ForegroundChild {
|
||||
fn drop(&mut self) {
|
||||
let (ref pgrp, ref pcnt) = *self.pipeline_state;
|
||||
if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 {
|
||||
pgrp.store(0, Ordering::SeqCst);
|
||||
fg_process_setup::reset_foreground_id(self.interactive)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's a simpler version of fish shell's external process handling.
|
||||
#[cfg(unix)]
|
||||
mod fg_process_setup {
|
||||
mod foreground_pgroup {
|
||||
use nix::{
|
||||
sys::signal,
|
||||
libc,
|
||||
sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal},
|
||||
unistd::{self, Pid},
|
||||
};
|
||||
use std::io::IsTerminal;
|
||||
use std::os::unix::prelude::{CommandExt, RawFd};
|
||||
use std::{
|
||||
os::unix::prelude::CommandExt,
|
||||
process::{Child, Command},
|
||||
};
|
||||
|
||||
// TODO: when raising MSRV past 1.63.0, switch to OwnedFd
|
||||
struct TtyHandle(RawFd);
|
||||
|
||||
impl Drop for TtyHandle {
|
||||
fn drop(&mut self) {
|
||||
let _ = unistd::close(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn prepare_to_foreground(
|
||||
external_command: &mut std::process::Command,
|
||||
existing_pgrp: u32,
|
||||
interactive: bool,
|
||||
) {
|
||||
let tty = TtyHandle(unistd::dup(nix::libc::STDIN_FILENO).expect("dup"));
|
||||
let interactive = interactive && std::io::stdin().is_terminal();
|
||||
pub fn prepare_command(external_command: &mut Command, existing_pgrp: u32) {
|
||||
unsafe {
|
||||
// Safety:
|
||||
// POSIX only allows async-signal-safe functions to be called.
|
||||
// `sigprocmask`, `setpgid` and `tcsetpgrp` are async-signal-safe according to:
|
||||
// `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 done:
|
||||
// - pthread_sigmask(SIG_SETMASK) with an empty sigset
|
||||
// - signal(SIGPIPE, SIG_DFL)
|
||||
// However, we do need TTOU/TTIN blocked again during this setup.
|
||||
let mut sigset = signal::SigSet::empty();
|
||||
sigset.add(signal::Signal::SIGTSTP);
|
||||
sigset.add(signal::Signal::SIGTTOU);
|
||||
sigset.add(signal::Signal::SIGTTIN);
|
||||
sigset.add(signal::Signal::SIGCHLD);
|
||||
signal::sigprocmask(signal::SigmaskHow::SIG_BLOCK, Some(&sigset), None)
|
||||
.expect("signal mask");
|
||||
// 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.
|
||||
if interactive {
|
||||
set_foreground_pid(unistd::getpid(), existing_pgrp, tty.0);
|
||||
}
|
||||
set_foreground_pid(Pid::this(), existing_pgrp);
|
||||
|
||||
// Now let the child process have all the signals by resetting with SIG_SETMASK.
|
||||
let mut sigset = signal::SigSet::empty();
|
||||
sigset.add(signal::Signal::SIGTSTP); // for now not really all: we don't support background jobs, so keep this one blocked
|
||||
signal::sigprocmask(signal::SigmaskHow::SIG_SETMASK, Some(&sigset), None)
|
||||
.expect("signal mask");
|
||||
// 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::SIGTSTP, &default);
|
||||
// let _ = sigaction(Signal::SIGTTIN, &default);
|
||||
// let _ = sigaction(Signal::SIGTTOU, &default);
|
||||
let _ = sigaction(Signal::SIGTERM, &default);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_foreground(
|
||||
process: &std::process::Child,
|
||||
existing_pgrp: u32,
|
||||
interactive: bool,
|
||||
) {
|
||||
// called from the parent shell process - do the stdin tty check here
|
||||
if interactive && std::io::stdin().is_terminal() {
|
||||
set_foreground_pid(
|
||||
Pid::from_raw(process.id() as i32),
|
||||
existing_pgrp,
|
||||
nix::libc::STDIN_FILENO,
|
||||
);
|
||||
}
|
||||
pub fn set(process: &Child, existing_pgrp: u32) {
|
||||
set_foreground_pid(Pid::from_raw(process.id() as i32), existing_pgrp);
|
||||
}
|
||||
|
||||
// existing_pgrp is 0 when we don't have an existing foreground process in the pipeline.
|
||||
// Conveniently, 0 means "current pid" to setpgid. But not to tcsetpgrp.
|
||||
fn set_foreground_pid(pid: Pid, existing_pgrp: u32, tty: RawFd) {
|
||||
let _ = unistd::setpgid(pid, Pid::from_raw(existing_pgrp as i32));
|
||||
let _ = unistd::tcsetpgrp(
|
||||
tty,
|
||||
if existing_pgrp == 0 {
|
||||
pid
|
||||
} else {
|
||||
Pid::from_raw(existing_pgrp as i32)
|
||||
},
|
||||
);
|
||||
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(libc::STDIN_FILENO, pgrp);
|
||||
}
|
||||
|
||||
/// Reset the foreground process group to the shell
|
||||
pub(super) fn reset_foreground_id(interactive: bool) {
|
||||
if interactive && std::io::stdin().is_terminal() {
|
||||
if let Err(e) = nix::unistd::tcsetpgrp(nix::libc::STDIN_FILENO, unistd::getpgrp()) {
|
||||
println!("ERROR: reset foreground id failed, tcsetpgrp result: {e:?}");
|
||||
}
|
||||
pub fn reset() {
|
||||
if let Err(e) = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpgrp()) {
|
||||
eprintln!("ERROR: reset foreground id failed, tcsetpgrp result: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
mod fg_process_setup {
|
||||
pub(super) fn prepare_to_foreground(_: &mut std::process::Command, _: u32, _: bool) {}
|
||||
|
||||
pub(super) fn set_foreground(_: &std::process::Child, _: u32, _: bool) {}
|
||||
|
||||
pub(super) fn reset_foreground_id(_: bool) {}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ pub mod os_info;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
pub use self::foreground::{ForegroundChild, ForegroundProcess};
|
||||
pub use self::foreground::ForegroundChild;
|
||||
#[cfg(any(target_os = "android", target_os = "linux"))]
|
||||
pub use self::linux::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
|
Reference in New Issue
Block a user