Skip to main content

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}