testlint_sdk/platform/
mod.rs

1/// Cross-platform process and system operations
2///
3/// This module provides platform-agnostic abstractions for:
4/// - Process management (checking, signaling, terminating)
5/// - Process information (command line, executable path)
6/// - File permissions
7///
8/// Each operation has Unix and Windows implementations where possible,
9/// with graceful degradation when platform features aren't available.
10use std::process::Command;
11
12/// Check if a process with the given PID exists
13pub fn process_exists(pid: u32) -> Result<bool, String> {
14    #[cfg(unix)]
15    {
16        let output = Command::new("ps")
17            .args(["-p", &pid.to_string()])
18            .output()
19            .map_err(|e| format!("Failed to check process: {}", e))?;
20
21        Ok(output.status.success())
22    }
23
24    #[cfg(windows)]
25    {
26        let output = Command::new("tasklist")
27            .args(["/FI", &format!("PID eq {}", pid), "/NH"])
28            .output()
29            .map_err(|e| format!("Failed to check process: {}", e))?;
30
31        let stdout = String::from_utf8_lossy(&output.stdout);
32        Ok(stdout.contains(&pid.to_string()))
33    }
34
35    #[cfg(not(any(unix, windows)))]
36    {
37        Err("Process checking not supported on this platform".to_string())
38    }
39}
40
41/// Get the command line of a running process
42pub fn get_process_cmdline(pid: u32) -> Result<String, String> {
43    #[cfg(target_os = "linux")]
44    {
45        let cmdline_path = format!("/proc/{}/cmdline", pid);
46        std::fs::read_to_string(&cmdline_path)
47            .map(|s| s.replace('\0', " "))
48            .map_err(|e| format!("Failed to read process cmdline: {}", e))
49    }
50
51    #[cfg(target_os = "macos")]
52    {
53        let output = Command::new("ps")
54            .args(["-p", &pid.to_string(), "-o", "command="])
55            .output()
56            .map_err(|e| format!("Failed to get process command: {}", e))?;
57
58        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
59    }
60
61    #[cfg(windows)]
62    {
63        // Use WMIC to get process command line
64        let output = Command::new("wmic")
65            .args([
66                "process",
67                "where",
68                &format!("ProcessId={}", pid),
69                "get",
70                "CommandLine",
71                "/format:list",
72            ])
73            .output()
74            .map_err(|e| format!("Failed to get process command: {}", e))?;
75
76        let stdout = String::from_utf8_lossy(&output.stdout);
77        for line in stdout.lines() {
78            if line.starts_with("CommandLine=") {
79                return Ok(line.trim_start_matches("CommandLine=").to_string());
80            }
81        }
82
83        Err(format!("Could not find command line for PID {}", pid))
84    }
85
86    #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
87    {
88        Err("Getting process command line not supported on this platform".to_string())
89    }
90}
91
92/// Get the executable name/path of a running process
93pub fn get_process_name(pid: u32) -> Result<String, String> {
94    #[cfg(unix)]
95    {
96        let output = Command::new("ps")
97            .args(["-p", &pid.to_string(), "-o", "comm="])
98            .output()
99            .map_err(|e| format!("Failed to check process: {}", e))?;
100
101        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
102    }
103
104    #[cfg(windows)]
105    {
106        let output = Command::new("tasklist")
107            .args(["/FI", &format!("PID eq {}", pid), "/NH", "/FO", "CSV"])
108            .output()
109            .map_err(|e| format!("Failed to get process name: {}", e))?;
110
111        let stdout = String::from_utf8_lossy(&output.stdout);
112        // CSV format: "image name","PID","Session Name","Session#","Mem Usage"
113        if let Some(line) = stdout.lines().next() {
114            if let Some(name) = line.split(',').next() {
115                return Ok(name.trim_matches('"').to_string());
116            }
117        }
118
119        Err(format!("Could not find process name for PID {}", pid))
120    }
121
122    #[cfg(not(any(unix, windows)))]
123    {
124        Err("Getting process name not supported on this platform".to_string())
125    }
126}
127
128/// Signal a process (Unix: actual signals, Windows: graceful alternatives)
129pub fn signal_process(pid: u32, signal: ProcessSignal) -> Result<(), String> {
130    #[cfg(unix)]
131    {
132        let signal_name = match signal {
133            ProcessSignal::Interrupt => "INT",
134            ProcessSignal::Terminate => "TERM",
135            ProcessSignal::User1 => "USR1",
136            ProcessSignal::User2 => "USR2",
137        };
138
139        Command::new("kill")
140            .args([&format!("-{}", signal_name), &pid.to_string()])
141            .output()
142            .map_err(|e| format!("Failed to signal process: {}", e))?;
143
144        Ok(())
145    }
146
147    #[cfg(windows)]
148    {
149        match signal {
150            ProcessSignal::Interrupt | ProcessSignal::Terminate => {
151                // On Windows, we can only forcefully terminate
152                // Use taskkill with /T to kill process tree
153                let output = Command::new("taskkill")
154                    .args(["/PID", &pid.to_string(), "/T"])
155                    .output()
156                    .map_err(|e| format!("Failed to terminate process: {}", e))?;
157
158                if !output.status.success() {
159                    return Err(format!(
160                        "Failed to terminate process: {}",
161                        String::from_utf8_lossy(&output.stderr)
162                    ));
163                }
164                Ok(())
165            }
166            ProcessSignal::User1 | ProcessSignal::User2 => Err(
167                "User-defined signals (SIGUSR1/SIGUSR2) are not available on Windows. \
168                     Consider using a file-based trigger mechanism or named pipes instead."
169                    .to_string(),
170            ),
171        }
172    }
173
174    #[cfg(not(any(unix, windows)))]
175    {
176        Err("Signaling processes not supported on this platform".to_string())
177    }
178}
179
180/// Process signals (cross-platform abstraction)
181#[derive(Debug, Clone, Copy)]
182pub enum ProcessSignal {
183    Interrupt, // SIGINT / Ctrl+C
184    Terminate, // SIGTERM
185    User1,     // SIGUSR1 (Unix only)
186    User2,     // SIGUSR2 (Unix only)
187}
188
189/// Make a file executable (Unix only, no-op on Windows)
190pub fn make_executable(path: &str) -> Result<(), String> {
191    #[cfg(unix)]
192    {
193        Command::new("chmod")
194            .args(["+x", path])
195            .output()
196            .map_err(|e| format!("Failed to make file executable: {}", e))?;
197        Ok(())
198    }
199
200    #[cfg(windows)]
201    {
202        // On Windows, executability is determined by file extension (.exe, .bat, .cmd)
203        // So this is a no-op
204        Ok(())
205    }
206
207    #[cfg(not(any(unix, windows)))]
208    {
209        Ok(()) // No-op on other platforms
210    }
211}
212
213/// Check if a process is of a specific type (e.g., "node", "python", "java")
214pub fn is_process_type(pid: u32, process_type: &str) -> Result<bool, String> {
215    let name = get_process_name(pid)?;
216    let cmdline = get_process_cmdline(pid).unwrap_or_default();
217
218    let name_lower = name.to_lowercase();
219    let cmdline_lower = cmdline.to_lowercase();
220    let type_lower = process_type.to_lowercase();
221
222    Ok(name_lower.contains(&type_lower) || cmdline_lower.contains(&type_lower))
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_process_exists_current() {
231        // Current process should always exist
232        let pid = std::process::id();
233        assert!(process_exists(pid).unwrap());
234    }
235
236    #[test]
237    fn test_process_exists_invalid() {
238        // PID 0 should not exist (reserved)
239        // Very high PID unlikely to exist
240        assert!(!process_exists(999999).unwrap_or(true));
241    }
242
243    #[test]
244    fn test_get_process_name_current() {
245        let pid = std::process::id();
246        let name = get_process_name(pid);
247        assert!(name.is_ok());
248        // Should contain test runner or cargo
249        let name_str = name.unwrap().to_lowercase();
250        assert!(
251            name_str.contains("test")
252                || name_str.contains("cargo")
253                || name_str.contains("testlint")
254        );
255    }
256}