Skip to main content

redis_server_wrapper/
process.rs

1//! OS-level process utilities for robust server shutdown and stale process cleanup.
2//!
3//! This module provides functions for checking process liveness, performing
4//! escalating kills (including process-group kills to handle wrapper scripts),
5//! and cleaning up stale pidfiles from crashed test runs.
6//!
7//! All utilities are intentionally synchronous so they can be used from
8//! [`Drop`] implementations as well as from async startup paths.
9//!
10//! Unix-only: every function here shells out to `kill` or `lsof`. See the
11//! crate-level "Platform Support" docs for details.
12
13use std::path::Path;
14use std::process::Command;
15use std::thread;
16use std::time::Duration;
17
18/// Check if a process is alive via `kill -0`.
19///
20/// Returns `true` if the process exists and is reachable, `false` otherwise.
21///
22/// # Example
23///
24/// ```no_run
25/// use redis_server_wrapper::process;
26///
27/// let alive = process::pid_alive(12345);
28/// println!("process alive: {alive}");
29/// ```
30pub fn pid_alive(pid: u32) -> bool {
31    Command::new("kill")
32        .args(["-0", &pid.to_string()])
33        .output()
34        .map(|o| o.status.success())
35        .unwrap_or(false)
36}
37
38/// Escalating kill: SIGTERM, wait grace period, then SIGKILL process group and individual PID.
39///
40/// Strategy:
41/// 1. Send SIGTERM to give the process a chance to shut down cleanly.
42/// 2. Sleep 500ms.
43/// 3. If still alive, SIGKILL the process group (`kill -9 -$pid`) to catch wrapper
44///    scripts and any children they spawned (e.g. `redis-stack-server`).
45/// 4. SIGKILL the individual PID as a fallback.
46///
47/// Uses synchronous [`std::process::Command`] so this is safe to call from [`Drop`] impls.
48///
49/// # Example
50///
51/// ```no_run
52/// use redis_server_wrapper::process;
53///
54/// process::force_kill(12345);
55/// ```
56pub fn force_kill(pid: u32) {
57    let pid_str = pid.to_string();
58    let pgid_str = format!("-{pid}");
59
60    // Step 1: SIGTERM -- graceful shutdown attempt.
61    let _ = Command::new("kill").args([&pid_str]).output();
62
63    // Step 2: Grace period.
64    thread::sleep(Duration::from_millis(500));
65
66    // Step 3: If still alive, escalate to SIGKILL on process group.
67    if pid_alive(pid) {
68        // Kill the whole process group to catch wrapper script children.
69        let _ = Command::new("kill").args(["-9", &pgid_str]).output();
70        // Also kill the individual PID as fallback.
71        let _ = Command::new("kill").args(["-9", &pid_str]).output();
72    }
73}
74
75/// Read a PID from a pidfile.
76///
77/// Returns `None` if the file does not exist, cannot be read, or its contents
78/// cannot be parsed as a `u32`.
79pub fn read_pidfile(path: &Path) -> Option<u32> {
80    std::fs::read_to_string(path)
81        .ok()
82        .and_then(|s| s.trim().parse::<u32>().ok())
83}
84
85/// Kill any process **listening** on a TCP port via `lsof`.
86///
87/// Uses `-sTCP:LISTEN` to restrict matches to server processes, avoiding
88/// false positives on client connections to the same port. Also filters
89/// out the calling process's own PID as a safeguard.
90///
91/// Best-effort -- all errors are silently ignored. This is intended as a
92/// final safety net to release the port after shutdown, not as a primary
93/// kill mechanism.
94///
95/// # Example
96///
97/// ```no_run
98/// use redis_server_wrapper::process;
99///
100/// process::kill_by_port(6379);
101/// ```
102pub fn kill_by_port(port: u16) {
103    let port_str = format!(":{port}");
104    let Ok(output) = Command::new("lsof")
105        .args(["-ti", &port_str, "-sTCP:LISTEN"])
106        .output()
107    else {
108        return;
109    };
110    if !output.status.success() {
111        return;
112    }
113    let my_pid = std::process::id().to_string();
114    let stdout = String::from_utf8_lossy(&output.stdout);
115    for line in stdout.lines() {
116        let line = line.trim();
117        if !line.is_empty() && line != my_pid {
118            let _ = Command::new("kill").args(["-9", line]).output();
119        }
120    }
121}