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}