Skip to main content

mermaid_cli/utils/
proc.rs

1//! Process-tree termination — the single place that knows how to take a spawned
2//! child *and its descendants* down.
3//!
4//! Every tree we manage is spawned as a process-group leader (`process_group(0)`
5//! on Unix; `mode=background` uses `setsid`; Windows kills the tree by pid via
6//! `taskkill /T`). Terminating the whole group reaches a grandchild that a bare
7//! per-pid signal would orphan — the bug this module centralizes the fix for: the
8//! Esc-cancel path already group-killed, but the foreground timeout, the
9//! Ctrl+B-detached cleanup, and the daemon's `/stop`/`/restart` each signalled a
10//! single pid.
11//!
12//! Unix sends to BOTH the group (`-pid`) and the bare pid, so a process that
13//! turned out not to be a group leader (e.g. a `mode=background` launch on a host
14//! without `setsid`) is still killed directly rather than missed entirely.
15//!
16//! Callers pick the grace: `Immediate` (Esc-cancel / timeout, which want the
17//! fastest possible teardown) or `Graceful` (`/stop`, background cleanup, which
18//! give the tree a beat to exit cleanly before the SIGKILL).
19
20use std::process::Stdio;
21use std::time::Duration;
22
23/// How aggressively to tear a tree down.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Grace {
26    /// SIGKILL the group immediately.
27    Immediate,
28    /// SIGTERM the group, brief grace, then SIGKILL — lets it clean up first.
29    Graceful,
30}
31
32/// How long `Graceful` waits between SIGTERM and the SIGKILL backstop.
33const GRACE_PERIOD: Duration = Duration::from_millis(400);
34
35/// Terminate `pid`'s process tree (async). Safe to call on a pid that has
36/// already exited — signals are best-effort and every error is swallowed.
37pub async fn terminate_tree(pid: u32, grace: Grace) {
38    #[cfg(not(target_os = "windows"))]
39    {
40        if grace == Grace::Graceful {
41            unix_kill("-TERM", pid).await;
42            tokio::time::sleep(GRACE_PERIOD).await;
43        }
44        unix_kill("-KILL", pid).await;
45    }
46    #[cfg(target_os = "windows")]
47    {
48        let _ = grace; // taskkill /F is always forceful.
49        taskkill_tree(pid).await;
50    }
51}
52
53/// Blocking sibling for sync call sites (the daemon's `/stop` / `/restart` run
54/// off a sync runtime client and can't await).
55pub fn terminate_tree_blocking(pid: u32, grace: Grace) {
56    #[cfg(not(target_os = "windows"))]
57    {
58        if grace == Grace::Graceful {
59            unix_kill_blocking("-TERM", pid);
60            std::thread::sleep(GRACE_PERIOD);
61        }
62        unix_kill_blocking("-KILL", pid);
63    }
64    #[cfg(target_os = "windows")]
65    {
66        let _ = grace;
67        taskkill_tree_blocking(pid);
68    }
69}
70
71#[cfg(not(target_os = "windows"))]
72async fn unix_kill(signal: &str, pid: u32) {
73    let _ = tokio::process::Command::new("kill")
74        .args([signal, "--", &format!("-{pid}"), &pid.to_string()])
75        .stdin(Stdio::null())
76        .stdout(Stdio::null())
77        .stderr(Stdio::null())
78        .status()
79        .await;
80}
81
82#[cfg(not(target_os = "windows"))]
83fn unix_kill_blocking(signal: &str, pid: u32) {
84    let _ = std::process::Command::new("kill")
85        .args([signal, "--", &format!("-{pid}"), &pid.to_string()])
86        .stdin(Stdio::null())
87        .stdout(Stdio::null())
88        .stderr(Stdio::null())
89        .status();
90}
91
92#[cfg(target_os = "windows")]
93async fn taskkill_tree(pid: u32) {
94    let _ = tokio::process::Command::new("taskkill")
95        .args(["/PID", &pid.to_string(), "/T", "/F"])
96        .stdin(Stdio::null())
97        .stdout(Stdio::null())
98        .stderr(Stdio::null())
99        .status()
100        .await;
101}
102
103#[cfg(target_os = "windows")]
104fn taskkill_tree_blocking(pid: u32) {
105    let _ = std::process::Command::new("taskkill")
106        .args(["/PID", &pid.to_string(), "/T", "/F"])
107        .stdin(Stdio::null())
108        .stdout(Stdio::null())
109        .stderr(Stdio::null())
110        .status();
111}