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 -ti :$port`.
83///
84/// Best-effort -- all errors are silently ignored. This is intended as a
85/// final safety net to release the port after shutdown, not as a primary
86/// kill mechanism.
87///
88/// # Example
89///
90/// ```no_run
91/// use redis_server_wrapper::process;
92///
93/// process::kill_by_port(6379);
94/// ```
95pub fn kill_by_port(port: u16) {
96 let port_str = format!(":{port}");
97 let Ok(output) = Command::new("lsof").args(["-ti", &port_str]).output() else {
98 return;
99 };
100 if !output.status.success() {
101 return;
102 }
103 let stdout = String::from_utf8_lossy(&output.stdout);
104 for line in stdout.lines() {
105 let line = line.trim();
106 if !line.is_empty() {
107 let _ = Command::new("kill").args(["-9", line]).output();
108 }
109 }
110}