leo_lang/cli/commands/devnet/child_manager.rs
1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use std::{
18 process::Child,
19 thread,
20 time::{Duration, Instant},
21};
22
23#[cfg(unix)]
24use libc::{SIGKILL, SIGTERM, kill};
25
26/// Manages child processes spawned by `snarkos` commands, ensuring they can be gracefully terminated and reaped.
27/// - On Unix, we rely on `setsid()` at spawn + sending signals to the **process group** via `kill(-pid, SIGTERM|SIGKILL)` so helpers die too.
28/// - On Windows, we rely on a **Job Object** (created/managed elsewhere) to ensure the tree is contained and hard-killed as a backstop;
29/// we still explicitly terminate lingering children during shutdown.
30pub struct ChildManager {
31 /// All direct `snarkos` children.
32 /// *Invariant*: a `Child` is inserted **exactly once** and removed **never**.
33 children: Vec<Child>,
34 /// Ensures shutdown is **idempotent** even if called multiple times (explicit + Drop).
35 shut_down: bool,
36}
37
38impl ChildManager {
39 /// Create an empty manager.
40 pub fn new() -> Self {
41 Self { children: Vec::new(), shut_down: false }
42 }
43
44 /// Register a freshly–spawned child so it can be reaped later.
45 pub fn push(&mut self, child: Child) {
46 self.children.push(child);
47 }
48
49 /// Graceful-then-forceful termination sequence (idempotent):
50 ///
51 /// 1) **Polite**: ask everyone to exit.
52 /// - Unix: send `SIGTERM` to the **process group** of each child via `kill(-pid, SIGTERM)`.
53 /// - Windows: no-op by default — rely on app-level handling and Job Objects.
54 /// 2) **Wait** up to `timeout` for children to exit (polling `try_wait`).
55 /// 3) **Hard kill survivors**:
56 /// - Unix: `kill(-pid, SIGKILL)`.
57 /// - Windows: `Child::kill()` (TerminateProcess). Job Object is a safety net.
58 /// 4) **Reap**: call `wait()` on all children to avoid zombies (ignore errors).
59 pub fn shutdown_all(&mut self, timeout: Duration) {
60 if self.shut_down {
61 return;
62 }
63 self.shut_down = true;
64
65 // ── 1) Polite pass ──────────────────────────────────────────────
66 for _child in &mut self.children {
67 #[cfg(unix)]
68 #[allow(unsafe_code)]
69 unsafe {
70 // SAFETY: Each child was created with `setsid()` in pre_exec, so `-pid`
71 // targets precisely that child’s session / process group.
72 let _ = kill(-(_child.id() as i32), SIGTERM);
73 }
74
75 #[cfg(windows)]
76 {
77 // On Windows we skip the "polite" console event unless children were spawned with CREATE_NEW_PROCESS_GROUP (not the default).
78 // We rely on:
79 // * application-level graceful handling (Ctrl+C handler), and
80 // * the Job Object (KILL_ON_JOB_CLOSE) for containment.
81 // Doing nothing here avoids brittle CTRL_BREAK semantics.
82 }
83 }
84
85 // ── 2) Wait for orderly exit ────────────────────────────────────
86 let start = Instant::now();
87 while start.elapsed() < timeout {
88 // If all exited, break early; we'll reap below.
89 if self.children.iter_mut().all(|c| matches!(c.try_wait(), Ok(Some(_)))) {
90 break;
91 }
92 thread::sleep(Duration::from_millis(150));
93 }
94
95 // ── 3) Escalate to hard kill for survivors ──────────────────────
96 for child in &mut self.children {
97 let still_running = matches!(child.try_wait(), Ok(None));
98
99 #[cfg(unix)]
100 if still_running {
101 #[allow(unsafe_code)]
102 unsafe {
103 let _ = kill(-(child.id() as i32), SIGKILL);
104 }
105 }
106
107 #[cfg(windows)]
108 if still_running {
109 // Terminate the process (the Job Object ensures descendants are contained).
110 let _ = child.kill();
111 }
112 }
113
114 // ── 4) Final reap (avoid zombies) ───────────────────────────────
115 for child in &mut self.children {
116 let _ = child.wait(); // ignore errors during teardown
117 }
118 }
119}
120
121impl Drop for ChildManager {
122 fn drop(&mut self) {
123 // Best-effort, panic-free shutdown on drop.
124 // Callers who need a different timeout should call `shutdown_all` explicitly.
125 self.shutdown_all(Duration::from_secs(30));
126 }
127}