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:
Ian Manske 2024-01-15 22:08:21 +00:00 committed by GitHub
parent 7071617f18
commit 924986576d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 231 additions and 273 deletions

1
Cargo.lock generated
View File

@ -2637,7 +2637,6 @@ dependencies = [
"rstest", "rstest",
"serde_json", "serde_json",
"serial_test", "serial_test",
"signal-hook",
"simplelog", "simplelog",
"tempfile", "tempfile",
"time", "time",

View File

@ -90,7 +90,6 @@ time = "0.3"
[target.'cfg(not(target_os = "windows"))'.dependencies] [target.'cfg(not(target_os = "windows"))'.dependencies]
# Our dependencies don't use OpenSSL on Windows # Our dependencies don't use OpenSSL on Windows
openssl = { version = "0.10", features = ["vendored"], optional = true } openssl = { version = "0.10", features = ["vendored"], optional = true }
signal-hook = { version = "0.3", default-features = false }
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winresource = "0.1" winresource = "0.1"

View File

@ -9,7 +9,7 @@ use nu_protocol::{
Category, Example, ListStream, PipelineData, RawStream, ShellError, Signature, Span, Spanned, Category, Example, ListStream, PipelineData, RawStream, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value, SyntaxShape, Type, Value,
}; };
use nu_system::ForegroundProcess; use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use os_pipe::PipeReader; use os_pipe::PipeReader;
use pathdiff::diff_paths; use pathdiff::diff_paths;
@ -216,82 +216,69 @@ impl ExternalCommand {
#[allow(unused_mut)] #[allow(unused_mut)]
let (cmd, mut reader) = self.create_process(&input, false, head)?; let (cmd, mut reader) = self.create_process(&input, false, head)?;
let mut fg_process =
ForegroundProcess::new(cmd, engine_state.pipeline_externals_state.clone()); #[cfg(all(not(unix), not(windows)))] // are there any systems like this?
// mut is used in the windows branch only, suppress warning on other platforms let child = ForegroundChild::spawn(cmd);
#[allow(unused_mut)]
let mut child;
#[cfg(windows)] #[cfg(windows)]
{ let child = match ForegroundChild::spawn(cmd) {
// Running external commands on Windows has 2 points of complication: Ok(child) => Ok(child),
// 1. Some common Windows commands are actually built in to cmd.exe, not executables in their own right. Err(err) => {
// 2. We need to let users run batch scripts etc. (.bat, .cmd) without typing their extension // Running external commands on Windows has 2 points of complication:
// 1. Some common Windows commands are actually built in to cmd.exe, not executables in their own right.
// 2. We need to let users run batch scripts etc. (.bat, .cmd) without typing their extension
// To support these situations, we have a fallback path that gets run if a command // To support these situations, we have a fallback path that gets run if a command
// fails to be run as a normal executable: // fails to be run as a normal executable:
// 1. "shell out" to cmd.exe if the command is a known cmd.exe internal command // 1. "shell out" to cmd.exe if the command is a known cmd.exe internal command
// 2. Otherwise, use `which-rs` to look for batch files etc. then run those in cmd.exe // 2. Otherwise, use `which-rs` to look for batch files etc. then run those in cmd.exe
match fg_process.spawn(engine_state.is_interactive) {
Err(err) => {
// set the default value, maybe we'll override it later
child = Err(err);
// This has the full list of cmd.exe "internal" commands: https://ss64.com/nt/syntax-internal.html // set the default value, maybe we'll override it later
// I (Reilly) went through the full list and whittled it down to ones that are potentially useful: let mut child = Err(err);
const CMD_INTERNAL_COMMANDS: [&str; 9] = [
"ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
];
let command_name = &self.name.item;
let looks_like_cmd_internal = CMD_INTERNAL_COMMANDS
.iter()
.any(|&cmd| command_name.eq_ignore_ascii_case(cmd));
if looks_like_cmd_internal { // This has the full list of cmd.exe "internal" commands: https://ss64.com/nt/syntax-internal.html
let (cmd, new_reader) = self.create_process(&input, true, head)?; // I (Reilly) went through the full list and whittled it down to ones that are potentially useful:
reader = new_reader; const CMD_INTERNAL_COMMANDS: [&str; 9] = [
let mut cmd_process = ForegroundProcess::new( "ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
cmd, ];
engine_state.pipeline_externals_state.clone(), let command_name = &self.name.item;
); let looks_like_cmd_internal = CMD_INTERNAL_COMMANDS
child = cmd_process.spawn(engine_state.is_interactive); .iter()
} else { .any(|&cmd| command_name.eq_ignore_ascii_case(cmd));
#[cfg(feature = "which-support")]
if looks_like_cmd_internal {
let (cmd, new_reader) = self.create_process(&input, true, head)?;
reader = new_reader;
child = ForegroundChild::spawn(cmd);
} else {
#[cfg(feature = "which-support")]
{
// maybe it's a batch file (foo.cmd) and the user typed `foo`. Try to find it with `which-rs`
// TODO: clean this up with an if-let chain once those are stable
if let Ok(path) =
nu_engine::env::path_str(engine_state, stack, self.name.span)
{ {
// maybe it's a batch file (foo.cmd) and the user typed `foo`. Try to find it with `which-rs` if let Some(cwd) = self.env_vars.get("PWD") {
// TODO: clean this up with an if-let chain once those are stable // append cwd to PATH so `which-rs` looks in the cwd too.
if let Ok(path) = // this approximates what cmd.exe does.
nu_engine::env::path_str(engine_state, stack, self.name.span) let path_with_cwd = format!("{};{}", cwd, path);
{ if let Ok(which_path) =
if let Some(cwd) = self.env_vars.get("PWD") { which::which_in(&self.name.item, Some(path_with_cwd), cwd)
// append cwd to PATH so `which-rs` looks in the cwd too. {
// this approximates what cmd.exe does. if let Some(file_name) = which_path.file_name() {
let path_with_cwd = format!("{};{}", cwd, path); if !file_name.to_string_lossy().eq_ignore_case(command_name)
if let Ok(which_path) = {
which::which_in(&self.name.item, Some(path_with_cwd), cwd) // which-rs found an executable file with a slightly different name
{ // than the one the user tried. Let's try running it
if let Some(file_name) = which_path.file_name() { let mut new_command = self.clone();
if !file_name new_command.name = Spanned {
.to_string_lossy() item: file_name.to_string_lossy().to_string(),
.eq_ignore_case(command_name) span: self.name.span,
{ };
// which-rs found an executable file with a slightly different name let (cmd, new_reader) =
// than the one the user tried. Let's try running it new_command.create_process(&input, true, head)?;
let mut new_command = self.clone(); reader = new_reader;
new_command.name = Spanned { child = ForegroundChild::spawn(cmd);
item: file_name.to_string_lossy().to_string(),
span: self.name.span,
};
let (cmd, new_reader) = new_command
.create_process(&input, true, head)?;
reader = new_reader;
let mut cmd_process = ForegroundProcess::new(
cmd,
engine_state.pipeline_externals_state.clone(),
);
child =
cmd_process.spawn(engine_state.is_interactive);
}
} }
} }
} }
@ -299,16 +286,17 @@ impl ExternalCommand {
} }
} }
} }
Ok(process) => {
child = Ok(process);
}
}
}
#[cfg(not(windows))] child
{ }
child = fg_process.spawn(engine_state.is_interactive) };
}
#[cfg(unix)]
let child = ForegroundChild::spawn(
cmd,
engine_state.is_interactive,
&engine_state.pipeline_externals_state,
);
match child { match child {
Err(err) => { Err(err) => {

View File

@ -1,66 +1,78 @@
use std::{ use std::{
io,
process::{Child, Command}, process::{Child, Command},
};
#[cfg(unix)]
use std::{
io::IsTerminal,
sync::{ sync::{
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
Arc, Arc,
}, },
}; };
/// A simple wrapper for `std::process::Command` /// A simple wrapper for [`std::process::Child`]
/// ///
/// ## Spawn behavior /// It can only be created by [`ForegroundChild::spawn`].
/// ### Unix
/// ///
/// 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. /// On drop, the calling process's group will become the foreground process group once again.
/// ///
/// ### Windows /// For non-interactive mode, processes are spawned normally without any foreground process handling.
/// 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`
/// ///
/// 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 { pub struct ForegroundChild {
inner: Child, inner: Child,
pipeline_state: Arc<(AtomicU32, AtomicU32)>, #[cfg(unix)]
interactive: bool, pipeline_state: Option<Arc<(AtomicU32, AtomicU32)>>,
} }
impl ForegroundProcess { impl ForegroundChild {
pub fn new(cmd: Command, pipeline_state: Arc<(AtomicU32, AtomicU32)>) -> Self { #[cfg(not(unix))]
Self { pub fn spawn(mut command: Command) -> io::Result<Self> {
inner: cmd, command.spawn().map(|child| Self { inner: child })
pipeline_state,
}
} }
pub fn spawn(&mut self, interactive: bool) -> std::io::Result<ForegroundChild> { #[cfg(unix)]
let (ref pgrp, ref pcnt) = *self.pipeline_state; pub fn spawn(
let existing_pgrp = pgrp.load(Ordering::SeqCst); mut command: Command,
fg_process_setup::prepare_to_foreground(&mut self.inner, existing_pgrp, interactive); interactive: bool,
self.inner pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
.spawn() ) -> io::Result<Self> {
.map(|child| { if interactive && io::stdin().is_terminal() {
fg_process_setup::set_foreground(&child, existing_pgrp, interactive); let (pgrp, pcnt) = pipeline_state.as_ref();
let _ = pcnt.fetch_add(1, Ordering::SeqCst); let existing_pgrp = pgrp.load(Ordering::SeqCst);
if existing_pgrp == 0 { foreground_pgroup::prepare_command(&mut command, existing_pgrp);
pgrp.store(child.id(), Ordering::SeqCst); command
} .spawn()
ForegroundChild { .map(|child| {
inner: child, foreground_pgroup::set(&child, existing_pgrp);
pipeline_state: self.pipeline_state.clone(), let _ = pcnt.fetch_add(1, Ordering::SeqCst);
interactive, if existing_pgrp == 0 {
} pgrp.store(child.id(), Ordering::SeqCst);
}) }
.map_err(|e| { Self {
fg_process_setup::reset_foreground_id(interactive); inner: child,
e 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 { impl Drop for ForegroundChild {
fn drop(&mut self) { fn drop(&mut self) {
let (ref pgrp, ref pcnt) = *self.pipeline_state; if let Some((pgrp, pcnt)) = self.pipeline_state.as_deref() {
if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 { if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 {
pgrp.store(0, Ordering::SeqCst); pgrp.store(0, Ordering::SeqCst);
fg_process_setup::reset_foreground_id(self.interactive) foreground_pgroup::reset()
}
} }
} }
} }
// It's a simpler version of fish shell's external process handling. // It's a simpler version of fish shell's external process handling.
#[cfg(unix)] #[cfg(unix)]
mod fg_process_setup { mod foreground_pgroup {
use nix::{ use nix::{
sys::signal, libc,
sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal},
unistd::{self, Pid}, unistd::{self, Pid},
}; };
use std::io::IsTerminal; use std::{
use std::os::unix::prelude::{CommandExt, RawFd}; os::unix::prelude::CommandExt,
process::{Child, Command},
};
// TODO: when raising MSRV past 1.63.0, switch to OwnedFd pub fn prepare_command(external_command: &mut Command, existing_pgrp: u32) {
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();
unsafe { unsafe {
// Safety: // Safety:
// POSIX only allows async-signal-safe functions to be called. // 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 // https://manpages.ubuntu.com/manpages/bionic/man7/signal-safety.7.html
// Also, `set_foreground_pid` is async-signal-safe.
external_command.pre_exec(move || { external_command.pre_exec(move || {
// When this callback is run, std::process has already done: // When this callback is run, std::process has already:
// - pthread_sigmask(SIG_SETMASK) with an empty sigset // - reset SIGPIPE to SIG_DFL
// - 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");
// According to glibc's job control manual: // According to glibc's job control manual:
// https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html // 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. // This has to be done *both* in the parent and here in the child due to race conditions.
if interactive { set_foreground_pid(Pid::this(), existing_pgrp);
set_foreground_pid(unistd::getpid(), existing_pgrp, tty.0);
}
// Now let the child process have all the signals by resetting with SIG_SETMASK. // Reset signal handlers for child, sync with `terminal.rs`
let mut sigset = signal::SigSet::empty(); let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
sigset.add(signal::Signal::SIGTSTP); // for now not really all: we don't support background jobs, so keep this one blocked // SIGINT has special handling
signal::sigprocmask(signal::SigmaskHow::SIG_SETMASK, Some(&sigset), None) let _ = sigaction(Signal::SIGQUIT, &default);
.expect("signal mask"); // 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(()) Ok(())
}); });
} }
} }
pub(super) fn set_foreground( pub fn set(process: &Child, existing_pgrp: u32) {
process: &std::process::Child, set_foreground_pid(Pid::from_raw(process.id() as i32), existing_pgrp);
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,
);
}
} }
// existing_pgrp is 0 when we don't have an existing foreground process in the pipeline. fn set_foreground_pid(pid: Pid, existing_pgrp: u32) {
// Conveniently, 0 means "current pid" to setpgid. But not to tcsetpgrp. // Safety: needs to be async-signal-safe.
fn set_foreground_pid(pid: Pid, existing_pgrp: u32, tty: RawFd) { // `setpgid` and `tcsetpgrp` are async-signal-safe.
let _ = unistd::setpgid(pid, Pid::from_raw(existing_pgrp as i32));
let _ = unistd::tcsetpgrp( // `existing_pgrp` is 0 when we don't have an existing foreground process in the pipeline.
tty, // A pgrp of 0 means the calling process's pid for `setpgid`. But not for `tcsetpgrp`.
if existing_pgrp == 0 { let pgrp = if existing_pgrp == 0 {
pid pid
} else { } else {
Pid::from_raw(existing_pgrp as i32) 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 /// Reset the foreground process group to the shell
pub(super) fn reset_foreground_id(interactive: bool) { pub fn reset() {
if interactive && std::io::stdin().is_terminal() { if let Err(e) = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpgrp()) {
if let Err(e) = nix::unistd::tcsetpgrp(nix::libc::STDIN_FILENO, unistd::getpgrp()) { eprintln!("ERROR: reset foreground id failed, tcsetpgrp result: {e:?}");
println!("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) {}
}

View File

@ -7,7 +7,7 @@ pub mod os_info;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod windows; mod windows;
pub use self::foreground::{ForegroundChild, ForegroundProcess}; pub use self::foreground::ForegroundChild;
#[cfg(any(target_os = "android", target_os = "linux"))] #[cfg(any(target_os = "android", target_os = "linux"))]
pub use self::linux::*; pub use self::linux::*;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View File

@ -4,6 +4,7 @@ mod ide;
mod logger; mod logger;
mod run; mod run;
mod signals; mod signals;
#[cfg(unix)]
mod terminal; mod terminal;
mod test_bins; mod test_bins;
#[cfg(test)] #[cfg(test)]
@ -17,7 +18,6 @@ use crate::{
command::parse_commandline_args, command::parse_commandline_args,
config_files::set_config_path, config_files::set_config_path,
logger::{configure, logger}, logger::{configure, logger},
terminal::acquire_terminal,
}; };
use command::gather_commandline_args; use command::gather_commandline_args;
use log::Level; use log::Level;
@ -185,16 +185,19 @@ fn main() -> Result<()> {
use_color, use_color,
); );
start_time = std::time::Instant::now(); #[cfg(unix)]
acquire_terminal(engine_state.is_interactive); {
perf( start_time = std::time::Instant::now();
"acquire_terminal", terminal::acquire(engine_state.is_interactive);
start_time, perf(
file!(), "acquire_terminal",
line!(), start_time,
column!(), file!(),
use_color, line!(),
); column!(),
use_color,
);
}
if let Some(include_path) = &parsed_nu_cli_args.include_path { if let Some(include_path) = &parsed_nu_cli_args.include_path {
let span = include_path.span; let span = include_path.span;

View File

@ -1,63 +1,46 @@
#[cfg(unix)]
use std::{ use std::{
io::IsTerminal, io::IsTerminal,
sync::atomic::{AtomicI32, Ordering}, sync::atomic::{AtomicI32, Ordering},
}; };
#[cfg(unix)]
use nix::{ use nix::{
errno::Errno, errno::Errno,
libc, libc,
sys::signal::{self, raise, signal, SaFlags, SigAction, SigHandler, SigSet, Signal}, sys::signal::{killpg, raise, sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal},
unistd::{self, Pid}, unistd::{self, Pid},
}; };
#[cfg(unix)]
static INITIAL_PGID: AtomicI32 = AtomicI32::new(-1); static INITIAL_PGID: AtomicI32 = AtomicI32::new(-1);
#[cfg(unix)] pub(crate) fn acquire(interactive: bool) {
pub(crate) fn acquire_terminal(interactive: bool) {
if interactive && std::io::stdin().is_terminal() { if interactive && std::io::stdin().is_terminal() {
// see also: https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html // see also: https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html
if unsafe { libc::atexit(restore_terminal) } != 0 {
eprintln!("ERROR: failed to set exit function");
std::process::exit(1);
};
let initial_pgid = take_control(); let initial_pgid = take_control();
INITIAL_PGID.store(initial_pgid.into(), Ordering::Relaxed); INITIAL_PGID.store(initial_pgid.into(), Ordering::Relaxed);
unsafe { unsafe {
if libc::atexit(restore_terminal) != 0 {
eprintln!("ERROR: failed to set exit function");
std::process::exit(1);
};
// SIGINT has special handling // SIGINT has special handling
signal(Signal::SIGQUIT, SigHandler::SigIgn).expect("signal ignore"); let ignore = SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty());
signal(Signal::SIGTSTP, SigHandler::SigIgn).expect("signal ignore"); sigaction(Signal::SIGQUIT, &ignore).expect("signal ignore");
signal(Signal::SIGTTIN, SigHandler::SigIgn).expect("signal ignore"); sigaction(Signal::SIGTSTP, &ignore).expect("signal ignore");
signal(Signal::SIGTTOU, SigHandler::SigIgn).expect("signal ignore"); sigaction(Signal::SIGTTIN, &ignore).expect("signal ignore");
sigaction(Signal::SIGTTOU, &ignore).expect("signal ignore");
// TODO: determine if this is necessary or not, since this breaks `rm` on macOS sigaction(
// signal(Signal::SIGCHLD, SigHandler::SigIgn).expect("signal ignore"); Signal::SIGTERM,
&SigAction::new(
signal_hook::low_level::register(signal_hook::consts::SIGTERM, || { SigHandler::Handler(sigterm_handler),
// Safety: can only call async-signal-safe functions here SaFlags::empty(),
// restore_terminal, signal, and raise are all async-signal-safe SigSet::empty(),
),
restore_terminal(); )
.expect("signal action");
if signal(Signal::SIGTERM, SigHandler::SigDfl).is_err() {
// Failed to set signal handler to default.
// This should not be possible, but if it does happen,
// then this could result in an infitite loop due to the raise below.
// So, we'll just exit immediately if this happens.
std::process::exit(1);
};
if raise(Signal::SIGTERM).is_err() {
std::process::exit(1);
};
})
.expect("signal hook");
} }
// Put ourselves in our own process group, if not already // Put ourselves in our own process group, if not already
@ -78,12 +61,8 @@ pub(crate) fn acquire_terminal(interactive: bool) {
} }
} }
#[cfg(not(unix))]
pub(crate) fn acquire_terminal(_: bool) {}
// Inspired by fish's acquire_tty_or_exit // Inspired by fish's acquire_tty_or_exit
// Returns our original pgid // Returns our original pgid
#[cfg(unix)]
fn take_control() -> Pid { fn take_control() -> Pid {
let shell_pgid = unistd::getpgrp(); let shell_pgid = unistd::getpgrp();
@ -101,16 +80,12 @@ fn take_control() -> Pid {
} }
// Reset all signal handlers to default // Reset all signal handlers to default
let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
for sig in Signal::iterator() { for sig in Signal::iterator() {
unsafe { if let Ok(old_act) = unsafe { sigaction(sig, &default) } {
if let Ok(old_act) = signal::sigaction( // fish preserves ignored SIGHUP, presumably for nohup support, so let's do the same
sig, if sig == Signal::SIGHUP && old_act.handler() == SigHandler::SigIgn {
&SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()), let _ = unsafe { sigaction(sig, &old_act) };
) {
// fish preserves ignored SIGHUP, presumably for nohup support, so let's do the same
if sig == Signal::SIGHUP && old_act.handler() == SigHandler::SigIgn {
let _ = signal::sigaction(sig, &old_act);
}
} }
} }
} }
@ -131,7 +106,7 @@ fn take_control() -> Pid {
} }
_ => { _ => {
// fish also has other heuristics than "too many attempts" for the orphan check, but they're optional // fish also has other heuristics than "too many attempts" for the orphan check, but they're optional
if signal::killpg(shell_pgid, Signal::SIGTTIN).is_err() { if killpg(shell_pgid, Signal::SIGTTIN).is_err() {
eprintln!("ERROR: failed to SIGTTIN ourselves"); eprintln!("ERROR: failed to SIGTTIN ourselves");
std::process::exit(1); std::process::exit(1);
} }
@ -143,12 +118,31 @@ fn take_control() -> Pid {
std::process::exit(1); std::process::exit(1);
} }
#[cfg(unix)]
extern "C" fn restore_terminal() { extern "C" fn restore_terminal() {
// Safety: can only call async-signal-safe functions here // Safety: can only call async-signal-safe functions here
// tcsetpgrp and getpgrp are async-signal-safe // `tcsetpgrp` and `getpgrp` are async-signal-safe
let initial_pgid = Pid::from_raw(INITIAL_PGID.load(Ordering::Relaxed)); let initial_pgid = Pid::from_raw(INITIAL_PGID.load(Ordering::Relaxed));
if initial_pgid.as_raw() > 0 && initial_pgid != unistd::getpgrp() { if initial_pgid.as_raw() > 0 && initial_pgid != unistd::getpgrp() {
let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, initial_pgid); let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, initial_pgid);
} }
} }
extern "C" fn sigterm_handler(_signum: libc::c_int) {
// Safety: can only call async-signal-safe functions here
// `restore_terminal`, `sigaction`, `raise`, and `_exit` are all async-signal-safe
restore_terminal();
let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
if unsafe { sigaction(Signal::SIGTERM, &default) }.is_err() {
// Failed to set signal handler to default.
// This should not be possible, but if it does happen,
// then this could result in an infinite loop due to the raise below.
// So, we'll just exit immediately if this happens.
unsafe { libc::_exit(1) };
};
if raise(Signal::SIGTERM).is_err() {
unsafe { libc::_exit(1) };
};
}