mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 08:23:24 +01: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:
parent
7071617f18
commit
924986576d
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2637,7 +2637,6 @@ dependencies = [
|
|||||||
"rstest",
|
"rstest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"signal-hook",
|
|
||||||
"simplelog",
|
"simplelog",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
"time",
|
||||||
|
@ -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"
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) {}
|
|
||||||
}
|
|
||||||
|
@ -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")]
|
||||||
|
25
src/main.rs
25
src/main.rs
@ -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;
|
||||||
|
102
src/terminal.rs
102
src/terminal.rs
@ -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) };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user