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