nextest_runner/runner/
unix.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::{InternalTerminateReason, ShutdownRequest, TerminateChildResult, UnitContext};
5use crate::{
6    errors::ConfigureHandleInheritanceError,
7    reporter::events::{
8        UnitState, UnitTerminateMethod, UnitTerminateReason, UnitTerminateSignal,
9        UnitTerminatingState,
10    },
11    runner::{RunUnitQuery, RunUnitRequest, SignalRequest},
12    signal::{JobControlEvent, ShutdownEvent},
13    test_command::ChildAccumulator,
14    time::StopwatchStart,
15};
16use libc::{SIGCONT, SIGHUP, SIGINT, SIGKILL, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP};
17use std::{convert::Infallible, os::unix::process::CommandExt, time::Duration};
18use tokio::{process::Child, sync::mpsc::UnboundedReceiver};
19
20// This is a no-op on non-windows platforms.
21pub(super) fn configure_handle_inheritance_impl(
22    _no_capture: bool,
23) -> Result<(), ConfigureHandleInheritanceError> {
24    Ok(())
25}
26
27/// Pre-execution configuration on Unix.
28///
29/// This sets up just the process group ID.
30pub(super) fn set_process_group(cmd: &mut std::process::Command) {
31    cmd.process_group(0);
32}
33
34#[derive(Debug)]
35pub(super) struct Job(());
36
37impl Job {
38    pub(super) fn create() -> Result<Self, Infallible> {
39        Ok(Self(()))
40    }
41}
42
43pub(super) fn assign_process_to_job(
44    _child: &tokio::process::Child,
45    _job: Option<&Job>,
46) -> Result<(), Infallible> {
47    Ok(())
48}
49
50pub(super) fn job_control_child(child: &Child, event: JobControlEvent) {
51    if let Some(pid) = child.id() {
52        let pid = pid as i32;
53        // Send the signal to the process group.
54        let signal = match event {
55            JobControlEvent::Stop => SIGTSTP,
56            JobControlEvent::Continue => SIGCONT,
57        };
58        unsafe {
59            // We set up a process group while starting the test -- now send a signal to that
60            // group.
61            libc::kill(-pid, signal);
62        }
63    } else {
64        // The child exited already -- don't send a signal.
65    }
66}
67
68// Note this is SIGSTOP rather than SIGTSTP to avoid triggering our signal handler.
69pub(super) fn raise_stop() {
70    // This can never error out because SIGSTOP is a valid signal.
71    unsafe { libc::raise(SIGSTOP) };
72}
73
74// TODO: should this indicate whether the process exited immediately? Could
75// do this with a non-async fn that optionally returns a future to await on.
76//
77// TODO: it would be nice to find a way to gather data like job (only on
78// Windows) or grace_period (only relevant on Unix) together.
79#[expect(clippy::too_many_arguments)]
80pub(super) async fn terminate_child<'a>(
81    cx: &UnitContext<'a>,
82    child: &mut Child,
83    child_acc: &mut ChildAccumulator,
84    reason: InternalTerminateReason,
85    stopwatch: &mut StopwatchStart,
86    req_rx: &mut UnboundedReceiver<RunUnitRequest<'a>>,
87    _job: Option<&Job>,
88    grace_period: Duration,
89) -> TerminateChildResult {
90    let Some(pid) = child.id() else {
91        return TerminateChildResult::Exited;
92    };
93
94    let pid_i32 = pid as i32;
95    let (term_reason, term_method) = to_terminate_reason_and_method(&reason, grace_period);
96
97    // This is infallible in regular mode and fallible with cfg(test).
98    #[allow(clippy::infallible_destructuring_match)]
99    let term_signal = match term_method {
100        UnitTerminateMethod::Signal(term_signal) => term_signal,
101        #[cfg(test)]
102        UnitTerminateMethod::Fake => {
103            unreachable!("fake method is only used for reporter tests")
104        }
105    };
106
107    unsafe {
108        // We set up a process group while starting the test -- now send a signal to that
109        // group.
110        libc::kill(-pid_i32, term_signal.signal())
111    };
112
113    if term_signal == UnitTerminateSignal::Kill {
114        // SIGKILL guarantees the process group is dead.
115        return TerminateChildResult::Killed;
116    }
117
118    let mut sleep = std::pin::pin!(crate::time::pausable_sleep(grace_period));
119    let mut waiting_stopwatch = crate::time::stopwatch();
120
121    loop {
122        tokio::select! {
123            () = child_acc.fill_buf(), if !child_acc.fds.is_done() => {}
124            _ = child.wait() => {
125                // The process exited.
126                break TerminateChildResult::Exited;
127            }
128            recv = req_rx.recv() => {
129                // The sender stays open longer than the whole loop, and the buffer is big
130                // enough for all messages ever sent through this channel, so a RecvError
131                // should never happen.
132                let req = recv.expect("a RecvError should never happen here");
133
134                match req {
135                    RunUnitRequest::Signal(SignalRequest::Stop(sender)) => {
136                        stopwatch.pause();
137                        sleep.as_mut().pause();
138                        waiting_stopwatch.pause();
139
140                        job_control_child(child, JobControlEvent::Stop);
141                        let _ = sender.send(());
142                    }
143                    RunUnitRequest::Signal(SignalRequest::Continue) => {
144                        // Possible to receive a Continue at the beginning of execution.
145                        if !sleep.is_paused() {
146                            stopwatch.resume();
147                            sleep.as_mut().resume();
148                            waiting_stopwatch.resume();
149                        }
150                        job_control_child(child, JobControlEvent::Continue);
151                    }
152                    RunUnitRequest::Signal(SignalRequest::Shutdown(_)) => {
153                        // Receiving a shutdown signal while in this state always means kill
154                        // immediately.
155                        unsafe {
156                            // Send SIGKILL to the entire process group.
157                            libc::kill(-pid_i32, SIGKILL);
158                        }
159                        break TerminateChildResult::Killed;
160                    }
161                    RunUnitRequest::OtherCancel => {
162                        // Ignore non-signal cancellation requests (most
163                        // likely another test failed). Let the unit finish.
164                    }
165                    RunUnitRequest::Query(RunUnitQuery::GetInfo(sender)) => {
166                        let waiting_snapshot = waiting_stopwatch.snapshot();
167                        _ = sender.send(
168                            cx.info_response(
169                                UnitState::Terminating(UnitTerminatingState {
170                                    pid,
171                                    time_taken: stopwatch.snapshot().active,
172                                    reason: term_reason,
173                                    method: term_method,
174                                    waiting_duration: waiting_snapshot.active,
175                                    remaining: grace_period
176                                        .checked_sub(waiting_snapshot.active)
177                                        .unwrap_or_default(),
178                                }),
179                                child_acc.snapshot_in_progress(cx.packet().kind().waiting_on_message()),
180                            )
181                        );
182                    }
183                }
184            }
185            _ = &mut sleep => {
186                // The process didn't exit -- need to do a hard shutdown.
187                unsafe {
188                    // Send SIGKILL to the entire process group.
189                    libc::kill(-pid_i32, SIGKILL);
190                }
191                break TerminateChildResult::Killed;
192            }
193        }
194    }
195}
196
197fn to_terminate_reason_and_method(
198    reason: &InternalTerminateReason,
199    grace_period: Duration,
200) -> (UnitTerminateReason, UnitTerminateMethod) {
201    match reason {
202        InternalTerminateReason::Timeout => (
203            UnitTerminateReason::Timeout,
204            timeout_terminate_method(grace_period),
205        ),
206        InternalTerminateReason::Signal(req) => (
207            UnitTerminateReason::Signal,
208            shutdown_terminate_method(*req, grace_period),
209        ),
210    }
211}
212
213fn timeout_terminate_method(grace_period: Duration) -> UnitTerminateMethod {
214    if grace_period.is_zero() {
215        UnitTerminateMethod::Signal(UnitTerminateSignal::Kill)
216    } else {
217        UnitTerminateMethod::Signal(UnitTerminateSignal::Term)
218    }
219}
220
221fn shutdown_terminate_method(req: ShutdownRequest, grace_period: Duration) -> UnitTerminateMethod {
222    if grace_period.is_zero() {
223        return UnitTerminateMethod::Signal(UnitTerminateSignal::Kill);
224    }
225
226    match req {
227        ShutdownRequest::Once(ShutdownEvent::Hangup) => {
228            UnitTerminateMethod::Signal(UnitTerminateSignal::Hangup)
229        }
230        ShutdownRequest::Once(ShutdownEvent::Term) => {
231            UnitTerminateMethod::Signal(UnitTerminateSignal::Term)
232        }
233        ShutdownRequest::Once(ShutdownEvent::Quit) => {
234            UnitTerminateMethod::Signal(UnitTerminateSignal::Quit)
235        }
236        ShutdownRequest::Once(ShutdownEvent::Interrupt) => {
237            UnitTerminateMethod::Signal(UnitTerminateSignal::Interrupt)
238        }
239        ShutdownRequest::Twice => UnitTerminateMethod::Signal(UnitTerminateSignal::Kill),
240    }
241}
242
243impl UnitTerminateSignal {
244    fn signal(self) -> libc::c_int {
245        match self {
246            UnitTerminateSignal::Interrupt => SIGINT,
247            UnitTerminateSignal::Term => SIGTERM,
248            UnitTerminateSignal::Hangup => SIGHUP,
249            UnitTerminateSignal::Quit => SIGQUIT,
250            UnitTerminateSignal::Kill => SIGKILL,
251        }
252    }
253}