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}