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}