Skip to main content

conmonrs/child_reaper/
runtime_call.rs

1//! Running a runtime for a call: its output, its process group and the
2//! cleanup after a failure.
3
4use super::{ChildReaper, InFlight, PendingPid};
5use crate::{
6    container_io::{ContainerIO, READ_BUFFER_SIZE, STDERR_TAIL_SIZE},
7    fs_util, init,
8    mutex::lock,
9    process::{
10        Process, child_pid, is_child, is_subreaper, read_pid_file, signal_name, wait_for_exit_code,
11    },
12    runtime_args::LoggedArgs,
13    tail::Tail,
14    util::{format_duration, with_scratch, with_stderr},
15};
16use anyhow::{Context, Result, bail, format_err};
17use nix::{
18    sys::signal::{Signal, killpg},
19    unistd::Pid,
20};
21use std::{
22    future, io,
23    ops::{Deref, DerefMut},
24    os::unix::process::ExitStatusExt,
25    path::Path,
26    pin::{Pin, pin},
27    process::{ExitStatus, Stdio},
28    sync::{Arc, Mutex},
29    task::{Poll, ready},
30};
31use tokio::{
32    io::{AsyncRead, ReadBuf},
33    process::{Child as RuntimeChild, ChildStderr, ChildStdout, Command},
34    select,
35    task::{self, JoinHandle},
36    time::{self, Duration, Instant},
37};
38use tokio_util::sync::CancellationToken;
39use tracing::{Instrument, debug, info, warn};
40
41/// The stdout and stderr of a runtime with pipes of its own, read in the
42/// background until they end. Only the end of stderr is kept, it holds the
43/// error, stdout is not of interest.
44struct RuntimeOutput {
45    stderr: Arc<Mutex<StderrTail>>,
46    task: JoinHandle<()>,
47}
48
49type StderrTail = Tail<STDERR_TAIL_SIZE>;
50
51impl RuntimeOutput {
52    /// Start reading the pipes of a runtime in the background.
53    fn spawn(stdout: Option<ChildStdout>, stderr: Option<ChildStderr>) -> Self {
54        let buf = Arc::<Mutex<StderrTail>>::default();
55        let stderr_buf = buf.clone();
56        let task = task::spawn(async move {
57            tokio::join!(
58                async {
59                    if let Some(stdout) = stdout {
60                        Self::read(stdout, None).await;
61                    }
62                },
63                async {
64                    if let Some(stderr) = stderr {
65                        Self::read(stderr, Some(&stderr_buf)).await;
66                    }
67                },
68            );
69        });
70        Self { stderr: buf, task }
71    }
72
73    /// Read until the end, keeping the end in `buf` if any. The reads go to
74    /// the scratch buffer of the thread, see [`with_scratch`], so that the
75    /// task of a create or exec with a terminal carries no buffers of its
76    /// own.
77    async fn read(mut reader: impl AsyncRead + Unpin, buf: Option<&Mutex<StderrTail>>) {
78        let mut read = |cx: &mut std::task::Context<'_>| {
79            with_scratch(READ_BUFFER_SIZE, |scratch| {
80                let mut chunk = ReadBuf::new(scratch);
81                match ready!(Pin::new(&mut reader).poll_read(cx, &mut chunk)) {
82                    Ok(()) if !chunk.filled().is_empty() => {
83                        if let Some(buf) = buf {
84                            lock(buf).push(chunk.filled());
85                        }
86                        Poll::Ready(true)
87                    }
88                    Ok(()) | Err(_) => Poll::Ready(false),
89                }
90            })
91        };
92        while future::poll_fn(&mut read).await {}
93    }
94
95    /// Wait for the end of the output for a while, a helper of the runtime
96    /// may hold the pipes open, but only briefly once the shutdown stopped
97    /// the call through `stop`, and return the end of stderr.
98    async fn finish_unless_stopped(output: Option<Self>, stop: &CancellationToken) -> Vec<u8> {
99        let Some(mut output) = output else {
100            return vec![];
101        };
102        let task = &mut output.task;
103        let grace = ChildReaper::RUNTIME_GRACE;
104        if wait_unless_stopped(task, grace, stop, ChildReaper::STOPPED_OUTPUT_WAIT)
105            .await
106            .is_none()
107        {
108            debug!("Runtime output still held open, giving up on it");
109            output.task.abort();
110        }
111        Self::end(&output)
112    }
113
114    fn end(output: &Self) -> Vec<u8> {
115        std::mem::take(&mut *lock(&output.stderr)).into_end()
116    }
117}
118
119/// How [`ChildReaper::create_child`] runs a runtime.
120#[derive(Debug)]
121pub(crate) struct RuntimeCall<'a> {
122    /// The binary of the runtime.
123    pub(crate) runtime: &'a Path,
124    pub(crate) args: &'a [String],
125
126    /// Where the command of an exec starts in `args`, which does not get
127    /// logged, see [`LoggedArgs`].
128    pub(crate) command_start: Option<usize>,
129
130    /// Whether the process gets stdin, through a pipe or its terminal.
131    pub(crate) stdin: bool,
132
133    /// Where the runtime writes the PID of the process it started.
134    pub(crate) pidfile: &'a Path,
135
136    /// When a runtime which did not finish gets killed.
137    pub(crate) deadline: Instant,
138}
139
140impl<'a> RuntimeCall<'a> {
141    /// A call without stdin, whose runtime gets killed at the `deadline`.
142    pub(crate) fn new(
143        runtime: &'a Path,
144        args: &'a [String],
145        pidfile: &'a Path,
146        deadline: Instant,
147    ) -> Self {
148        Self {
149            runtime,
150            args,
151            command_start: None,
152            stdin: false,
153            pidfile,
154            deadline,
155        }
156    }
157}
158
159/// The process a runtime started, see [`ChildReaper::create_child`].
160pub(crate) struct StartedChild {
161    /// The PID the runtime reported, a child of the server.
162    pub(crate) pid: Pid,
163
164    /// Cancelled once the process exited, or once its call failed.
165    pub(crate) token: CancellationToken,
166
167    /// The call, which ends with the registration of the process.
168    pub(crate) call: InFlight,
169}
170
171impl ChildReaper {
172    /// How long the runtime of a create may take before it gets killed, well
173    /// above the time CRI-O gives the creation of a container, so that a
174    /// runtime which hangs does not keep its call in progress forever.
175    pub(crate) const CREATE_RUNTIME_TIMEOUT: Duration = Duration::from_secs(10 * 60);
176
177    /// How long the runtime of an exec may take beyond its timeout before it
178    /// gets killed. It only starts the process, so this bounds an exec
179    /// without a timeout as well.
180    pub(super) const EXEC_RUNTIME_GRACE: Duration = Duration::from_secs(60);
181
182    /// When the runtime of an exec with the `timeout` gets killed. A runtime
183    /// which hangs must not keep its call in progress forever, which defers
184    /// the reaping of orphans and delays the shutdown.
185    pub(crate) fn exec_runtime_deadline(timeout: Option<Instant>) -> Instant {
186        let timeout = timeout.unwrap_or_else(Instant::now);
187        // A timeout too far in the future to add the grace to is never
188        // reached either.
189        timeout
190            .checked_add(Self::EXEC_RUNTIME_GRACE)
191            .unwrap_or(timeout)
192    }
193
194    /// Spawn the runtime, see [`ChildReaper::create_child`].
195    fn spawn_runtime(runtime: &Path, args: &[String], stdin: bool) -> Result<RuntimeChild> {
196        Ok(Self::runtime_command(runtime, args, stdin).spawn()?)
197    }
198
199    /// The command of a runtime, see [`ChildReaper::spawn_runtime`].
200    pub(super) fn runtime_command(runtime: &Path, args: &[String], stdin: bool) -> Command {
201        let mut cmd = Command::new(runtime);
202        // A runtime which gets dropped, for example on a panic, must not keep
203        // running unobserved.
204        cmd.kill_on_drop(true);
205        // In a process group of its own, which a runtime that hangs gets
206        // killed with, see `stop_runtime`. Signals for the group of the
207        // server, like a SIGINT from a terminal, do not reach it, the
208        // server stops it on its own.
209        cmd.process_group(0);
210
211        // Without one it must not inherit the stdin of the server, which the
212        // runtime would pass on to the container.
213        cmd.stdin(if stdin { Stdio::piped() } else { Stdio::null() });
214
215        cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
216        // The sd_notify socket of whoever started the server is not for the
217        // runtime, which would add a notify socket mount of its own and
218        // `NOTIFY_SOCKET` to the container. The server does not talk to
219        // systemd itself and starts no other process, so nothing else sees
220        // the variable.
221        cmd.env_remove("NOTIFY_SOCKET");
222        // Lowers the open files limit to the one the server started with.
223        init::reset_inherited(&mut cmd);
224        cmd
225    }
226
227    /// How long a runtime gets for what follows its exit or its failure: one
228    /// which exited successfully to connect the terminal, one which failed
229    /// to exit on its own and one which got killed to exit. The helpers of a
230    /// runtime which exited get it to close its output, see
231    /// [`RuntimeOutput::finish_unless_stopped`].
232    pub(super) const RUNTIME_GRACE: Duration = Duration::from_secs(5);
233
234    /// How long a call which the shutdown stopped waits for the exit of its
235    /// killed runtime, and then for the output of the runtime, instead of
236    /// the [`ChildReaper::RUNTIME_GRACE`], see [`ChildReaper::STOP_BUDGET`].
237    pub(super) const STOPPED_EXIT_WAIT: Duration = Duration::from_millis(500);
238    pub(super) const STOPPED_OUTPUT_WAIT: Duration = Duration::from_millis(250);
239
240    /// Run the runtime with the I/O of the process it starts, and return the
241    /// process once the runtime exited. The caller registers it through
242    /// [`ChildReaper::watch_grandchild`].
243    pub(crate) async fn create_child(
244        &self,
245        call: RuntimeCall<'_>,
246        container_io: &mut ContainerIO,
247    ) -> Result<StartedChild> {
248        let RuntimeCall {
249            runtime,
250            args,
251            command_start,
252            stdin,
253            pidfile,
254            deadline: runtime_deadline,
255        } = call;
256        let mut in_flight = self.begin_call(runtime_deadline)?;
257        // How long the runtime gets, for the error of one which takes longer.
258        let runtime_timeout = runtime_deadline.saturating_duration_since(Instant::now());
259        // A PID file left from before, like of a container created in the
260        // same bundle earlier, would name a process which the cleanup of a
261        // failed call kills, see `cleanup_failed_grandchild`.
262        fs_util::remove_file(pidfile).context("remove stale PID file")?;
263        let logged_args = LoggedArgs {
264            args,
265            command_start,
266        };
267        debug!("Running: {runtime:?} {logged_args:?}");
268        // With a terminal, the process gets the terminal as its stdin, the
269        // pipe would stay unused.
270        let stdin_pipe = stdin && container_io.terminal().is_none();
271        // A runtime which a package update moved fails with a bare "No such
272        // file or directory" otherwise.
273        let child = Self::spawn_runtime(runtime, args, stdin_pipe)
274            .with_context(|| format!("spawn runtime {}", runtime.display()))?;
275
276        // Not waited for yet, so it has its PID. Dropping it on a failure
277        // kills it.
278        let runtime_pid = child_pid(child.id()).context("get PID of runtime")?;
279        let mut child = Runtime {
280            child,
281            pending: Some(PendingPid::new(&self.pending, runtime_pid)),
282        };
283
284        debug!("Running child on PID: {runtime_pid}");
285
286        let token = CancellationToken::new();
287        let stop = in_flight.stop.clone();
288
289        // With streams, the read loops take the output of the runtime along
290        // with the one of the process. With a terminal, the runtime has pipes
291        // of its own. They get read while waiting for it, a runtime which
292        // writes more than they hold, like with debug logging, would block.
293        let output = container_io
294            .terminal()
295            .is_some()
296            .then(|| RuntimeOutput::spawn(child.stdout.take(), child.stderr.take()));
297
298        let deadline = RuntimeDeadline {
299            at: runtime_deadline,
300            timeout: runtime_timeout,
301        };
302        let connected =
303            Self::connect_io(&mut child, container_io, stdin, &token, deadline, &stop).await;
304        let status = match connected {
305            Ok(()) => Self::wait_runtime(&mut child, deadline, &stop).await,
306            Err(e) => Err(e),
307        };
308        let status = match status {
309            Ok(status) => status,
310            Err(RuntimeFailure { error, grace }) => {
311                token.cancel();
312                let stderr = Self::stop_runtime(child, output, grace, &stop).await;
313                return Err(self.fail_create(pidfile, error, &stderr));
314            }
315        };
316
317        // The container process got reparented to this server once the
318        // runtime exited. Read synchronously and mark it as pending right
319        // away, before waiting for the output of the runtime, which a helper
320        // may hold open for a while. A process which exits before it is
321        // registered must not be taken for an orphan. It is registered by the
322        // caller through `watch_grandchild`, which ends the call and with it
323        // the pending mark, like a caller which gives up before that does.
324        let grandchild_pid = status.success().then(|| read_pid_file(pidfile));
325        if let Some(Ok(pid)) = &grandchild_pid {
326            in_flight.claim(&self.pending, *pid);
327        }
328        let stderr = RuntimeOutput::finish_unless_stopped(output, &stop).await;
329
330        let error = match grandchild_pid {
331            // Claimed above, so the cleanup of the failure below leaves a
332            // foreign PID alone, see `cleanup_failed_grandchild`.
333            Some(Ok(pid)) => match Self::check_reported_pid(pid, is_subreaper()) {
334                Ok(()) => {
335                    return Ok(StartedChild {
336                        pid,
337                        token,
338                        call: in_flight,
339                    });
340                }
341                Err(e) => e,
342            },
343            Some(Err(e)) => e,
344            None => match status.code() {
345                Some(code) => format_err!("runtime exited with status {code}"),
346                // Without an exit code, a signal ended the runtime.
347                None => {
348                    let signal = status.signal().unwrap_or_default();
349                    format_err!("runtime got killed by signal {}", signal_name(signal))
350                }
351            },
352        };
353        // Nothing registers the process, so the stdin loops have to be
354        // stopped here. The caller stops the output once it took the error of
355        // the runtime, see `ContainerIO::runtime_error`.
356        token.cancel();
357        Err(self.fail_create(pidfile, error, &stderr))
358    }
359
360    /// Fail for a PID the runtime reported which is no child of this server,
361    /// see [`is_child`]. Only a child gets registered and signalled later,
362    /// its PID cannot get reused before its watcher reaped it. A runtime
363    /// which reports the PID of another process, by a bug or on purpose,
364    /// must not make the server signal that process. A process which exited
365    /// already is still a child until reaped, like a restored container may
366    /// have. The processes get reparented to the server as their
367    /// `subreaper`, without that flag, like in the tests, they are never its
368    /// children, so there is nothing to check.
369    ///
370    /// The call claims the PID before it checks it, so the cleanup of the
371    /// failed call finds it claimed and does not signal it, see
372    /// [`ChildReaper::cleanup_failed_grandchild`]. The signal through
373    /// [`Process::child`] would refuse a PID which is no child anyway.
374    pub(super) fn check_reported_pid(pid: Pid, subreaper: bool) -> Result<()> {
375        if subreaper && !is_child(pid) {
376            bail!("runtime reported PID {pid}, which is not a child")
377        }
378        Ok(())
379    }
380
381    /// Connect the I/O of the process which the runtime starts: wait until
382    /// the runtime connected the terminal, or read the pipes of the runtime,
383    /// which the process shares. Fails once the `deadline` passed or the
384    /// shutdown cancelled `stop`, like [`ChildReaper::wait_runtime`].
385    async fn connect_io(
386        child: &mut Runtime,
387        container_io: &mut ContainerIO,
388        stdin: bool,
389        token: &CancellationToken,
390        deadline: RuntimeDeadline,
391        stop: &CancellationToken,
392    ) -> Result<(), RuntimeFailure> {
393        if container_io.terminal().is_none() {
394            let (stdin, stdout, stderr) =
395                (child.stdin.take(), child.stdout.take(), child.stderr.take());
396            container_io.connect_streams(stdin, stdout, stderr, token.clone());
397            return Ok(());
398        }
399        // The runtime connects to the console socket before it exits. If it
400        // fails before that, waiting for the connection alone would hang
401        // this call forever.
402        select! {
403            res = container_io.connect_terminal(stdin, token.clone()) => {
404                res.context("wait for terminal socket connection")
405                    .map_err(RuntimeFailure::new)
406            }
407            () = time::sleep_until(deadline.at) => Err(RuntimeFailure::timed_out(deadline.timeout)),
408            () = stop.cancelled() => Err(RuntimeFailure::stopped()),
409            status = child.reap() => match status.context("wait for child") {
410                // The runtime succeeded, so it did connect and the descriptor
411                // is queued already.
412                Ok(status) if status.success() => {
413                    let connect = container_io.connect_terminal(stdin, token.clone());
414                    match wait_unless_stopped(connect, Self::RUNTIME_GRACE, stop, Duration::ZERO)
415                        .await
416                    {
417                        Some(res) => res
418                            .context("wait for terminal socket connection")
419                            .map_err(RuntimeFailure::new),
420                        None if stop.is_cancelled() => Err(RuntimeFailure::stopped()),
421                        None => Err(RuntimeFailure::new(format_err!(
422                            "runtime exited without connecting the terminal"
423                        ))),
424                    }
425                }
426                // A failure is reported later together with its output.
427                Ok(_) => Ok(()),
428                Err(e) => Err(RuntimeFailure::new(e)),
429            },
430        }
431    }
432
433    /// Wait for the exit of the runtime. A runtime which hangs gets killed
434    /// once its deadline passed, and a wait which fails gets cleaned up like
435    /// a runtime which failed. A runtime which still runs once the shutdown
436    /// stops the call through `stop` gets killed right away as well, by
437    /// this call, which holds it until it got reaped, see
438    /// [`ChildReaper::kill_pending`].
439    async fn wait_runtime(
440        child: &mut Runtime,
441        deadline: RuntimeDeadline,
442        stop: &CancellationToken,
443    ) -> Result<ExitStatus, RuntimeFailure> {
444        select! {
445            // A runtime which exited in time succeeded.
446            biased;
447            status = child.reap() => status
448                .context("wait for runtime")
449                .map_err(RuntimeFailure::new),
450            () = time::sleep_until(deadline.at) => Err(RuntimeFailure::timed_out(deadline.timeout)),
451            () = stop.cancelled() => Err(RuntimeFailure::stopped()),
452        }
453    }
454
455    /// Stop a runtime which failed before it exited, or whose call the
456    /// shutdown stopped: give it `grace` to exit on its own, so that it
457    /// still writes the PID file the cleanup needs, kill it otherwise, and
458    /// return its stderr. It gets killed through `child`, which tokio did
459    /// not reap yet, so neither its PID nor its group got reused. Its
460    /// pending mark ends once it got reaped, or once a killed runtime did
461    /// not exit within [`ChildReaper::RUNTIME_GRACE`], before waiting for
462    /// its output. Once the shutdown stopped the call through `stop`, the
463    /// runtime gets killed right away and the waits get shorter, so that
464    /// the call ends within the [`ChildReaper::STOP_BUDGET`].
465    async fn stop_runtime(
466        mut child: Runtime,
467        output: Option<RuntimeOutput>,
468        grace: Duration,
469        stop: &CancellationToken,
470    ) -> Vec<u8> {
471        let exited = wait_unless_stopped(child.reap(), grace, stop, Duration::ZERO).await;
472        if exited.is_none() {
473            info!("Runtime did not exit in time, killing it");
474            // Its helpers in its process group go with it, like hooks or a
475            // runc init which did not set up the container yet, which would
476            // be left running otherwise. A container init which started a
477            // session of its own syncs with its runtime over a socket, and
478            // fails once that closes. The runtime itself gets killed as well,
479            // it may have moved to another group.
480            if let Err(e) = child.kill_group() {
481                debug!("Unable to kill process group of runtime: {e}");
482            }
483            if let Err(e) = child.start_kill() {
484                debug!("Unable to kill runtime: {e}");
485            }
486            // Its container process gets reparented to this server only once
487            // it is gone, the cleanup relies on that. A runtime stuck in the
488            // kernel is not waited for forever.
489            let reaped = child.reap();
490            let (grace, stopped) = (Self::RUNTIME_GRACE, Self::STOPPED_EXIT_WAIT);
491            match wait_unless_stopped(reaped, grace, stop, stopped).await {
492                Some(Ok(_)) => {}
493                Some(Err(e)) => debug!("Unable to wait for runtime: {e}"),
494                // Not unit tested, a killed process which does not exit in
495                // time cannot be built in one.
496                None => warn!("Killed runtime did not exit, cleaning up anyway"),
497            }
498        }
499        // Dropped before its mark: the drop kills the group of a runtime
500        // which did not exit, while the mark still keeps the orphan sweep
501        // from reaping it and its group number from getting reused. Neither
502        // is held while waiting for the output.
503        let pending = child.pending.take();
504        drop(child);
505        drop(pending);
506
507        // Its stderr usually holds the reason.
508        RuntimeOutput::finish_unless_stopped(output, stop).await
509    }
510
511    /// Clean up after a failed runtime call and build its error, with the
512    /// stderr of the runtime appended.
513    fn fail_create(&self, pidfile: &Path, error: anyhow::Error, stderr: &[u8]) -> anyhow::Error {
514        // Do not leave a container process behind which the runtime created
515        // before it failed, and reap what else it left.
516        self.cleanup_failed_grandchild(pidfile);
517        self.sweep.notify_one();
518
519        let error = with_stderr(error, stderr);
520        // The caller logs the error it returns.
521        debug!("Failed: {error:#}");
522        error
523    }
524
525    /// Kill and reap the container process of a failed runtime call, if the
526    /// runtime got as far as writing its PID. It got reparented to this
527    /// subreaper and nobody else waits for it.
528    ///
529    /// Only this PID is waited for. Reaping any exited child instead would take
530    /// the exit status of runtimes of concurrent calls, or of grandchildren
531    /// which are not registered yet.
532    pub(super) fn cleanup_failed_grandchild(&self, pidfile: &Path) {
533        let Ok(pid) = read_pid_file(pidfile) else {
534            return;
535        };
536        // A stale PID file may name a process which got reused by a container
537        // or exec of this server, which must not be killed.
538        if self.is_claimed(pid) {
539            debug!("Not cleaning up PID {pid} of failed runtime call, it is in use");
540            return;
541        }
542
543        // Only a child, the PID may have been reused otherwise. With its
544        // group, like on a timeout, so nothing it started survives.
545        if let Err(e) = Process::child(pid).signal_group(Signal::SIGKILL) {
546            debug!("Not cleaning up PID {pid} of failed runtime call: {e}");
547            return;
548        }
549        debug!("Cleaning up PID {pid} of failed runtime call");
550        task::spawn(wait_for_exit_code(pid).in_current_span());
551    }
552}
553
554/// Wait for `future` for at most `limit`, and only for `stopped` more once
555/// the shutdown stopped the call through `stop`, see
556/// [`ChildReaper::STOP_BUDGET`]. None if it did not finish by then.
557async fn wait_unless_stopped<T>(
558    future: impl Future<Output = T>,
559    limit: Duration,
560    stop: &CancellationToken,
561    stopped: Duration,
562) -> Option<T> {
563    let deadline = Instant::now() + limit;
564    let mut future = pin!(future);
565    select! {
566        biased;
567        res = &mut future => return Some(res),
568        () = time::sleep_until(deadline) => return None,
569        () = stop.cancelled() => {}
570    }
571    time::timeout_at(deadline.min(Instant::now() + stopped), future)
572        .await
573        .ok()
574}
575
576/// When the runtime of a call gets killed, and how long it got for the
577/// error of one which took longer.
578#[derive(Clone, Copy)]
579struct RuntimeDeadline {
580    at: Instant,
581    timeout: Duration,
582}
583
584/// A runtime call which failed before its runtime exited, see
585/// [`ChildReaper::stop_runtime`].
586struct RuntimeFailure {
587    error: anyhow::Error,
588
589    /// How long the runtime may take to exit on its own, none once its
590    /// deadline passed.
591    grace: Duration,
592}
593
594impl RuntimeFailure {
595    fn new(error: anyhow::Error) -> Self {
596        Self {
597            error,
598            grace: ChildReaper::RUNTIME_GRACE,
599        }
600    }
601
602    /// The runtime did not finish within its `timeout`.
603    fn timed_out(timeout: Duration) -> Self {
604        Self {
605            error: format_err!("runtime did not finish within {}", format_duration(timeout)),
606            grace: Duration::ZERO,
607        }
608    }
609
610    /// The shutdown stopped the call, see [`ChildReaper::stop_calls`].
611    fn stopped() -> Self {
612        Self {
613            error: format_err!("runtime got stopped by the shutdown"),
614            grace: Duration::ZERO,
615        }
616    }
617}
618
619/// A runtime whose process group gets killed if it is dropped before it got
620/// reaped, like a call in progress which the shutdown drops once it stops
621/// waiting for it. `kill_on_drop` only kills the runtime, its helpers in its
622/// group, like hooks or a runc init which did not set up the container yet,
623/// would be left running, see [`ChildReaper::stop_runtime`]. Until it got
624/// reaped, the ID of the group is still the one of the runtime.
625#[derive(Debug)]
626struct Runtime {
627    child: RuntimeChild,
628
629    /// Keeps the runtime pending until it got reaped, so that the orphan
630    /// sweep leaves its exit status to tokio. Its PID may belong to another
631    /// process after that, which another mark may keep pending.
632    pending: Option<PendingPid>,
633}
634
635impl Deref for Runtime {
636    type Target = RuntimeChild;
637
638    fn deref(&self) -> &Self::Target {
639        &self.child
640    }
641}
642
643impl DerefMut for Runtime {
644    fn deref_mut(&mut self) -> &mut Self::Target {
645        &mut self.child
646    }
647}
648
649impl Runtime {
650    /// Wait for the exit of the runtime, which reaps it, and end its pending
651    /// mark right away. Returns the status again once it got reaped.
652    async fn reap(&mut self) -> io::Result<ExitStatus> {
653        let status = self.child.wait().await;
654        if status.is_ok() {
655            self.pending = None;
656        }
657        status
658    }
659
660    /// Kill the process group of the runtime, which it leads, see
661    /// `spawn_runtime`. Nothing once it got reaped, the ID of the group may
662    /// belong to another one then, the runtime has no ID anymore.
663    fn kill_group(&self) -> nix::Result<()> {
664        let Ok(pid) = child_pid(self.child.id()) else {
665            return Ok(());
666        };
667        killpg(pid, Signal::SIGKILL)
668    }
669}
670
671impl Drop for Runtime {
672    fn drop(&mut self) {
673        if let Err(e) = self.kill_group() {
674            debug!("Unable to kill process group of dropped runtime: {e}");
675        }
676    }
677}