Skip to content

Commit 267b092

Browse files
committed
Implement per-pipeline process groups for background jobs
Currently in nushell, when running in an interactive session, every pipeline gets its own process group, which is set as the foreground process group. Meanwhile, in non-interactive sessions, all processes use the shell's process group. This commit makes so that background jobs also use one processs group per pipeline, but without moving them to foreground. This commit also fixes a bug in nushell's trunk, which occurs if a process in a non-interactive session gets signaled with the TSTP signal, which causes the shell to try to regain foreground status, and read a confirmation message from stdin (which should not occur in an interactive session). The `foreground.rs`'s inner `foreground_pgrp` module has been renamed to `child_pgrp` since it now manages process groups in general.
1 parent c957cec commit 267b092

File tree

4 files changed

+47
-28
lines changed

4 files changed

+47
-28
lines changed

crates/nu-command/src/env/config/config_.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pub(super) fn start_editor(
106106
let child = ForegroundChild::spawn(
107107
command,
108108
engine_state.is_interactive,
109+
engine_state.current_thread_job.is_some(),
109110
&engine_state.pipeline_externals_state,
110111
);
111112

crates/nu-command/src/experimental/job_spawn.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl Command for JobSpawn {
6060
let job_signals = Signals::new(Arc::new(AtomicBool::new(false)));
6161
job_state.set_signals(job_signals.clone());
6262

63-
// the new job has a separate process group for its processes
63+
// the new job has a separate process group state for its processes
6464
job_state.pipeline_externals_state = Arc::new((AtomicU32::new(0), AtomicU32::new(0)));
6565

6666
job_state.exit_warning_given = Arc::new(AtomicBool::new(false));

crates/nu-command/src/system/run_external.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ impl Command for External {
251251
let child = ForegroundChild::spawn(
252252
command,
253253
engine_state.is_interactive,
254+
engine_state.current_thread_job.is_some(),
254255
&engine_state.pipeline_externals_state,
255256
);
256257

crates/nu-system/src/foreground.rs

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::ExitStatus;
1010
use std::{io::IsTerminal, sync::atomic::Ordering};
1111

1212
#[cfg(unix)]
13-
pub use foreground_pgroup::stdin_fd;
13+
pub use child_pgroup::stdin_fd;
1414

1515
#[cfg(unix)]
1616
use nix::{sys::signal, sys::wait, unistd::Pid};
@@ -35,6 +35,10 @@ pub struct ForegroundChild {
3535
inner: Child,
3636
#[cfg(unix)]
3737
pipeline_state: Option<Arc<(AtomicU32, AtomicU32)>>,
38+
39+
// this is unix-only since we don't have to deal with process groups in windows
40+
#[cfg(unix)]
41+
interactive: bool,
3842
}
3943

4044
impl ForegroundChild {
@@ -47,32 +51,42 @@ impl ForegroundChild {
4751
pub fn spawn(
4852
mut command: Command,
4953
interactive: bool,
54+
background: bool,
5055
pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
5156
) -> io::Result<Self> {
52-
if interactive && io::stdin().is_terminal() {
57+
let interactive = interactive && io::stdin().is_terminal();
58+
59+
let uses_dedicated_process_group = interactive || background;
60+
61+
if uses_dedicated_process_group {
5362
let (pgrp, pcnt) = pipeline_state.as_ref();
5463
let existing_pgrp = pgrp.load(Ordering::SeqCst);
55-
foreground_pgroup::prepare_command(&mut command, existing_pgrp);
64+
child_pgroup::prepare_command(&mut command, existing_pgrp, background);
5665
command
5766
.spawn()
5867
.map(|child| {
59-
foreground_pgroup::set(&child, existing_pgrp);
68+
child_pgroup::set(&child, existing_pgrp, background);
69+
6070
let _ = pcnt.fetch_add(1, Ordering::SeqCst);
6171
if existing_pgrp == 0 {
6272
pgrp.store(child.id(), Ordering::SeqCst);
6373
}
6474
Self {
6575
inner: child,
6676
pipeline_state: Some(pipeline_state.clone()),
77+
interactive,
6778
}
6879
})
6980
.inspect_err(|_e| {
70-
foreground_pgroup::reset();
81+
if interactive {
82+
child_pgroup::reset();
83+
}
7184
})
7285
} else {
7386
command.spawn().map(|child| Self {
7487
inner: child,
7588
pipeline_state: None,
89+
interactive,
7690
})
7791
}
7892
}
@@ -103,10 +117,9 @@ impl ForegroundChild {
103117
}));
104118
}
105119
Ok(wait::WaitStatus::Stopped(_, _)) => {
106-
// TODO: consider only bringing our
107-
// process group back to foreground, if we are in an
108-
// interactive session
109-
foreground_pgroup::reset();
120+
if self.interactive {
121+
child_pgroup::reset();
122+
}
110123

111124
let handle = UnfreezeHandle { child_pid };
112125

@@ -212,7 +225,10 @@ impl Drop for ForegroundChild {
212225
if let Some((pgrp, pcnt)) = self.pipeline_state.as_deref() {
213226
if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 {
214227
pgrp.store(0, Ordering::SeqCst);
215-
foreground_pgroup::reset()
228+
229+
if self.interactive {
230+
child_pgroup::reset()
231+
}
216232
}
217233
}
218234
}
@@ -339,7 +355,7 @@ impl ForegroundGuard {
339355
if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 {
340356
// Clean up if we are the last one around
341357
pgrp.store(0, Ordering::SeqCst);
342-
foreground_pgroup::reset()
358+
child_pgroup::reset()
343359
}
344360
}
345361
}
@@ -353,7 +369,7 @@ impl Drop for ForegroundGuard {
353369

354370
// It's a simpler version of fish shell's external process handling.
355371
#[cfg(unix)]
356-
mod foreground_pgroup {
372+
mod child_pgroup {
357373
use nix::{
358374
sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal},
359375
unistd::{self, Pid},
@@ -377,7 +393,7 @@ mod foreground_pgroup {
377393
unsafe { BorrowedFd::borrow_raw(nix::libc::STDIN_FILENO) }
378394
}
379395

380-
pub fn prepare_command(external_command: &mut Command, existing_pgrp: u32) {
396+
pub fn prepare_command(external_command: &mut Command, existing_pgrp: u32, background: bool) {
381397
unsafe {
382398
// Safety:
383399
// POSIX only allows async-signal-safe functions to be called.
@@ -391,19 +407,13 @@ mod foreground_pgroup {
391407
// According to glibc's job control manual:
392408
// https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
393409
// This has to be done *both* in the parent and here in the child due to race conditions.
394-
set_foreground_pid(Pid::this(), existing_pgrp);
410+
set_foreground_pid(Pid::this(), existing_pgrp, background);
395411

396-
// Reset signal handlers for child, sync with `terminal.rs`
412+
// `terminal.rs` makes the shell process ignore some signals,
413+
// so we set them to their default behavior for our child
397414
let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
398-
// SIGINT has special handling
415+
399416
let _ = sigaction(Signal::SIGQUIT, &default);
400-
// We don't support background jobs, so keep some signals blocked for now
401-
// let _ = sigaction(Signal::SIGTTIN, &default);
402-
// let _ = sigaction(Signal::SIGTTOU, &default);
403-
// We do need to reset SIGTSTP though, since some TUI
404-
// applications implement their own Ctrl-Z handling, and
405-
// ForegroundChild::wait() needs to be able to react to the
406-
// child being stopped.
407417
let _ = sigaction(Signal::SIGTSTP, &default);
408418
let _ = sigaction(Signal::SIGTERM, &default);
409419

@@ -412,11 +422,15 @@ mod foreground_pgroup {
412422
}
413423
}
414424

415-
pub fn set(process: &Child, existing_pgrp: u32) {
416-
set_foreground_pid(Pid::from_raw(process.id() as i32), existing_pgrp);
425+
pub fn set(process: &Child, existing_pgrp: u32, background: bool) {
426+
set_foreground_pid(
427+
Pid::from_raw(process.id() as i32),
428+
existing_pgrp,
429+
background,
430+
);
417431
}
418432

419-
pub fn set_foreground_pid(pid: Pid, existing_pgrp: u32) {
433+
fn set_foreground_pid(pid: Pid, existing_pgrp: u32, background: bool) {
420434
// Safety: needs to be async-signal-safe.
421435
// `setpgid` and `tcsetpgrp` are async-signal-safe.
422436

@@ -428,7 +442,10 @@ mod foreground_pgroup {
428442
Pid::from_raw(existing_pgrp as i32)
429443
};
430444
let _ = unistd::setpgid(pid, pgrp);
431-
let _ = unistd::tcsetpgrp(unsafe { stdin_fd() }, pgrp);
445+
446+
if !background {
447+
let _ = unistd::tcsetpgrp(unsafe { stdin_fd() }, pgrp);
448+
}
432449
}
433450

434451
/// Reset the foreground process group to the shell

0 commit comments

Comments
 (0)