nu_system/foreground.rs
1use std::sync::{Arc, atomic::AtomicU32};
2
3use std::io;
4
5use std::process::{Child, Command};
6
7use crate::ExitStatus;
8
9#[cfg(unix)]
10use std::{io::IsTerminal, sync::atomic::Ordering};
11
12#[cfg(unix)]
13pub use child_pgroup::stdin_fd;
14
15/// Detach `command` from the parent's controlling terminal/console.
16///
17/// Used for background completion subprocesses so they cannot call
18/// `tcsetattr` / `SetConsoleMode` and corrupt reedline. Caller must also
19/// redirect stdin to null. No-op on platforms without a process API.
20pub fn prepare_background_command(command: &mut Command) {
21 #[cfg(unix)]
22 child_pgroup::prepare_isolated_command(command);
23
24 #[cfg(windows)]
25 {
26 use std::os::windows::process::CommandExt;
27 // CREATE_NO_WINDOW: run console apps without creating a console window at all
28 // (required for non-blocking completions so completer subprocesses cannot
29 // SetConsoleMode / AttachConsole / AllocConsole and corrupt the parent's reedline).
30 // Do not use DETACHED_PROCESS here: completions rely on CREATE_NO_WINDOW, and
31 // MSDN documents that CREATE_NO_WINDOW is ignored when combined with DETACHED_PROCESS.
32 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
33 command.creation_flags(CREATE_NO_WINDOW);
34 }
35}
36
37#[cfg(unix)]
38use nix::{sys::signal, sys::wait, unistd::Pid};
39
40/// A simple wrapper for [`std::process::Child`]
41///
42/// It can only be created by [`ForegroundChild::spawn`].
43///
44/// # Spawn behavior
45/// ## Unix
46///
47/// For interactive shells, the spawned child process will get its own process group id,
48/// and it will be put in the foreground (by making stdin belong to the child's process group).
49/// On drop, the calling process's group will become the foreground process group once again.
50///
51/// For non-interactive mode, processes are spawned normally without any foreground process handling.
52///
53/// ## Other systems
54///
55/// It does nothing special on non-unix systems, so `spawn` is the same as [`std::process::Command::spawn`].
56pub struct ForegroundChild {
57 inner: Child,
58 #[cfg(unix)]
59 pipeline_state: Option<Arc<(AtomicU32, AtomicU32)>>,
60
61 // this is unix-only since we don't have to deal with process groups in windows
62 #[cfg(unix)]
63 interactive: bool,
64}
65
66impl ForegroundChild {
67 #[cfg(not(unix))]
68 pub fn spawn(mut command: Command) -> io::Result<Self> {
69 command.spawn().map(|child| Self { inner: child })
70 }
71
72 #[cfg(unix)]
73 pub fn spawn(
74 mut command: Command,
75 interactive: bool,
76 background: bool,
77 pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
78 ) -> io::Result<Self> {
79 let interactive = interactive && io::stdin().is_terminal();
80
81 let uses_dedicated_process_group = interactive || background;
82
83 if uses_dedicated_process_group {
84 let (pgrp, pcnt) = pipeline_state.as_ref();
85 let existing_pgrp = pgrp.load(Ordering::SeqCst);
86 child_pgroup::prepare_command(&mut command, existing_pgrp, background);
87 command
88 .spawn()
89 .map(|child| {
90 child_pgroup::set(&child, existing_pgrp, background);
91
92 let _ = pcnt.fetch_add(1, Ordering::SeqCst);
93 if existing_pgrp == 0 {
94 pgrp.store(child.id(), Ordering::SeqCst);
95 }
96 Self {
97 inner: child,
98 pipeline_state: Some(pipeline_state.clone()),
99 interactive,
100 }
101 })
102 .inspect_err(|_e| {
103 if interactive {
104 child_pgroup::reset();
105 }
106 })
107 } else {
108 command.spawn().map(|child| Self {
109 inner: child,
110 pipeline_state: None,
111 interactive,
112 })
113 }
114 }
115
116 pub fn wait(&mut self) -> io::Result<ForegroundWaitStatus> {
117 #[cfg(unix)]
118 {
119 let child_pid = Pid::from_raw(self.inner.id() as i32);
120
121 unix_wait(child_pid).inspect(|result| {
122 if let (true, ForegroundWaitStatus::Frozen(_)) = (self.interactive, result) {
123 child_pgroup::reset();
124 }
125 })
126 }
127 #[cfg(not(unix))]
128 self.as_mut().wait().map(Into::into)
129 }
130
131 pub fn pid(&self) -> u32 {
132 self.inner.id()
133 }
134}
135
136#[cfg(unix)]
137fn unix_wait(child_pid: Pid) -> std::io::Result<ForegroundWaitStatus> {
138 use ForegroundWaitStatus::*;
139
140 // the child may be stopped multiple times, we loop until it exits
141 loop {
142 let status = wait::waitpid(child_pid, Some(wait::WaitPidFlag::WUNTRACED));
143 match status {
144 Err(e) => {
145 return Err(e.into());
146 }
147 Ok(wait::WaitStatus::Exited(_, status)) => {
148 return Ok(Finished(ExitStatus::Exited(status)));
149 }
150 Ok(wait::WaitStatus::Signaled(_, signal, core_dumped)) => {
151 return Ok(Finished(ExitStatus::Signaled {
152 signal: signal as i32,
153 core_dumped,
154 }));
155 }
156 Ok(wait::WaitStatus::Stopped(_, _)) => {
157 return Ok(Frozen(UnfreezeHandle { child_pid }));
158 }
159 Ok(_) => {
160 // keep waiting
161 }
162 };
163 }
164}
165
166pub enum ForegroundWaitStatus {
167 Finished(ExitStatus),
168 Frozen(UnfreezeHandle),
169}
170
171impl From<std::process::ExitStatus> for ForegroundWaitStatus {
172 fn from(status: std::process::ExitStatus) -> Self {
173 ForegroundWaitStatus::Finished(status.into())
174 }
175}
176
177#[derive(Debug)]
178pub struct UnfreezeHandle {
179 #[cfg(unix)]
180 child_pid: Pid,
181}
182
183impl UnfreezeHandle {
184 #[cfg(unix)]
185 pub fn unfreeze(
186 self,
187 pipeline_state: Option<Arc<(AtomicU32, AtomicU32)>>,
188 ) -> io::Result<ForegroundWaitStatus> {
189 // bring child's process group back into foreground and continue it
190
191 // we only keep the guard for its drop impl
192 let _guard = pipeline_state.map(|pipeline_state| {
193 ForegroundGuard::new(self.child_pid.as_raw() as u32, &pipeline_state)
194 });
195
196 if let Err(err) = signal::killpg(self.child_pid, signal::SIGCONT) {
197 return Err(err.into());
198 }
199
200 let child_pid = self.child_pid;
201
202 unix_wait(child_pid)
203 }
204
205 pub fn pid(&self) -> u32 {
206 #[cfg(unix)]
207 {
208 self.child_pid.as_raw() as u32
209 }
210
211 #[cfg(not(unix))]
212 0
213 }
214}
215
216impl AsMut<Child> for ForegroundChild {
217 fn as_mut(&mut self) -> &mut Child {
218 &mut self.inner
219 }
220}
221
222#[cfg(unix)]
223impl Drop for ForegroundChild {
224 fn drop(&mut self) {
225 if let Some((pgrp, pcnt)) = self.pipeline_state.as_deref()
226 && pcnt.fetch_sub(1, Ordering::SeqCst) == 1
227 {
228 pgrp.store(0, Ordering::SeqCst);
229
230 if self.interactive {
231 child_pgroup::reset()
232 }
233 }
234 }
235}
236
237/// Keeps a specific already existing process in the foreground as long as the [`ForegroundGuard`].
238/// If the process needs to be spawned in the foreground, use [`ForegroundChild`] instead. This is
239/// used to temporarily bring frozen and plugin processes into the foreground.
240///
241/// # OS-specific behavior
242/// ## Unix
243///
244/// If there is already a foreground external process running, spawned with [`ForegroundChild`],
245/// this expects the process ID to remain in the process group created by the [`ForegroundChild`]
246/// for the lifetime of the guard, and keeps the terminal controlling process group set to that.
247/// If there is no foreground external process running, this sets the foreground process group to
248/// the provided process ID. The process group that is expected can be retrieved with
249/// [`.pgrp()`](Self::pgrp) if different from the provided process ID.
250///
251/// ## Other systems
252///
253/// It does nothing special on non-unix systems.
254#[derive(Debug)]
255pub struct ForegroundGuard {
256 #[cfg(unix)]
257 pgrp: Option<u32>,
258 #[cfg(unix)]
259 pipeline_state: Arc<(AtomicU32, AtomicU32)>,
260}
261
262impl ForegroundGuard {
263 /// Move the given process to the foreground.
264 #[cfg(unix)]
265 pub fn new(
266 pid: u32,
267 pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
268 ) -> std::io::Result<ForegroundGuard> {
269 use nix::unistd::{self, Pid};
270
271 let pid_nix = Pid::from_raw(pid as i32);
272 let (pgrp, pcnt) = pipeline_state.as_ref();
273
274 // Might have to retry due to race conditions on the atomics
275 loop {
276 // Try to give control to the child, if there isn't currently a foreground group
277 if pgrp
278 .compare_exchange(0, pid, Ordering::SeqCst, Ordering::SeqCst)
279 .is_ok()
280 {
281 let _ = pcnt.fetch_add(1, Ordering::SeqCst);
282
283 // We don't need the child to change process group. Make the guard now so that if there
284 // is an error, it will be cleaned up
285 let guard = ForegroundGuard {
286 pgrp: None,
287 pipeline_state: pipeline_state.clone(),
288 };
289
290 log::trace!("Giving control of the terminal to the process group, pid={pid}");
291
292 // Set the terminal controlling process group to the child process
293 unistd::tcsetpgrp(unsafe { stdin_fd() }, pid_nix)?;
294
295 return Ok(guard);
296 } else if pcnt
297 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
298 // Avoid a race condition: only increment if count is > 0
299 if count > 0 { Some(count + 1) } else { None }
300 })
301 .is_ok()
302 {
303 // We successfully added another count to the foreground process group, which means
304 // we only need to tell the child process to join this one
305 let pgrp = pgrp.load(Ordering::SeqCst);
306 log::trace!(
307 "Will ask the process pid={pid} to join pgrp={pgrp} for control of the \
308 terminal"
309 );
310 return Ok(ForegroundGuard {
311 pgrp: Some(pgrp),
312 pipeline_state: pipeline_state.clone(),
313 });
314 } else {
315 // The state has changed, we'll have to retry
316 continue;
317 }
318 }
319 }
320
321 /// Move the given process to the foreground.
322 #[cfg(not(unix))]
323 pub fn new(
324 pid: u32,
325 pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
326 ) -> std::io::Result<ForegroundGuard> {
327 let _ = (pid, pipeline_state);
328 Ok(ForegroundGuard {})
329 }
330
331 /// If the child process is expected to join a different process group to be in the foreground,
332 /// this returns `Some(pgrp)`. This only ever returns `Some` on Unix.
333 pub fn pgrp(&self) -> Option<u32> {
334 #[cfg(unix)]
335 {
336 self.pgrp
337 }
338 #[cfg(not(unix))]
339 {
340 None
341 }
342 }
343
344 /// This should only be called once by `Drop`
345 fn reset_internal(&mut self) {
346 #[cfg(unix)]
347 {
348 log::trace!("Leaving the foreground group");
349
350 let (pgrp, pcnt) = self.pipeline_state.as_ref();
351 if pcnt.fetch_sub(1, Ordering::SeqCst) == 1 {
352 // Clean up if we are the last one around
353 pgrp.store(0, Ordering::SeqCst);
354 child_pgroup::reset()
355 }
356 }
357 }
358}
359
360impl Drop for ForegroundGuard {
361 fn drop(&mut self) {
362 self.reset_internal();
363 }
364}
365
366// It's a simpler version of fish shell's external process handling.
367#[cfg(unix)]
368mod child_pgroup {
369 use nix::{
370 sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction},
371 unistd::{self, Pid},
372 };
373 use std::{
374 io::Write,
375 os::{
376 fd::{AsFd, BorrowedFd},
377 unix::prelude::CommandExt,
378 },
379 process::{Child, Command},
380 };
381
382 /// Alternative to having to call `std::io::stdin()` just to get the file descriptor of stdin
383 ///
384 /// # Safety
385 /// I/O safety of reading from `STDIN_FILENO` unclear.
386 ///
387 /// Currently only intended to access `tcsetpgrp` and `tcgetpgrp` with the I/O safe `nix`
388 /// interface.
389 pub unsafe fn stdin_fd() -> impl AsFd {
390 unsafe { BorrowedFd::borrow_raw(nix::libc::STDIN_FILENO) }
391 }
392
393 /// `setsid` in `pre_exec`: new session, no controlling terminal.
394 ///
395 /// Not the same as [`prepare_command`] with `background = true` (that only
396 /// skips `tcsetpgrp`; this fully detaches). Completions skip job-control
397 /// signal resets intentionally.
398 pub fn prepare_isolated_command(command: &mut Command) {
399 // SAFETY: `setsid` is async-signal-safe (POSIX signal-safety(7)); legal
400 // in `pre_exec` between fork and exec.
401 unsafe {
402 command.pre_exec(|| {
403 // Ignore EPERM if we are already a session leader.
404 let _ = unistd::setsid();
405 Ok(())
406 });
407 }
408 }
409
410 pub fn prepare_command(external_command: &mut Command, existing_pgrp: u32, background: bool) {
411 unsafe {
412 // Safety:
413 // POSIX only allows async-signal-safe functions to be called.
414 // `sigaction` and `getpid` are async-signal-safe according to:
415 // https://manpages.ubuntu.com/manpages/bionic/man7/signal-safety.7.html
416 // Also, `set_foreground_pid` is async-signal-safe.
417 external_command.pre_exec(move || {
418 // When this callback is run, std::process has already:
419 // - reset SIGPIPE to SIG_DFL
420
421 // According to glibc's job control manual:
422 // https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
423 // This has to be done *both* in the parent and here in the child due to race conditions.
424 set_foreground_pid(Pid::this(), existing_pgrp, background);
425
426 // `terminal.rs` makes the shell process ignore some signals,
427 // so we set them to their default behavior for our child
428 let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
429
430 let _ = sigaction(Signal::SIGQUIT, &default);
431 let _ = sigaction(Signal::SIGTSTP, &default);
432 let _ = sigaction(Signal::SIGTERM, &default);
433
434 Ok(())
435 });
436 }
437 }
438
439 pub fn set(process: &Child, existing_pgrp: u32, background: bool) {
440 set_foreground_pid(
441 Pid::from_raw(process.id() as i32),
442 existing_pgrp,
443 background,
444 );
445 }
446
447 fn set_foreground_pid(pid: Pid, existing_pgrp: u32, background: bool) {
448 // Safety: needs to be async-signal-safe.
449 // `setpgid` and `tcsetpgrp` are async-signal-safe.
450
451 // `existing_pgrp` is 0 when we don't have an existing foreground process in the pipeline.
452 // A pgrp of 0 means the calling process's pid for `setpgid`. But not for `tcsetpgrp`.
453 let pgrp = if existing_pgrp == 0 {
454 pid
455 } else {
456 Pid::from_raw(existing_pgrp as i32)
457 };
458 let _ = unistd::setpgid(pid, pgrp);
459
460 if !background {
461 let _ = unistd::tcsetpgrp(unsafe { stdin_fd() }, pgrp);
462 }
463 }
464
465 /// Reset the foreground process group to the shell
466 pub fn reset() {
467 if let Err(e) = unistd::tcsetpgrp(unsafe { stdin_fd() }, unistd::getpgrp()) {
468 // Use write_all instead of eprintln! to avoid panicking (and
469 // subsequently aborting due to a double-panic) when stderr is
470 // unavailable — e.g. the terminal has already been torn down.
471 let _ = writeln!(
472 std::io::stderr(),
473 "ERROR: reset foreground id failed, tcsetpgrp result: {e:?}"
474 );
475 }
476 }
477}