testlint_sdk/platform/
mod.rs1use std::process::Command;
11
12pub 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
41pub 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 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
92pub 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 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
128pub 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 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#[derive(Debug, Clone, Copy)]
182pub enum ProcessSignal {
183 Interrupt, Terminate, User1, User2, }
188
189pub 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 Ok(())
205 }
206
207 #[cfg(not(any(unix, windows)))]
208 {
209 Ok(()) }
211}
212
213pub 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 let pid = std::process::id();
233 assert!(process_exists(pid).unwrap());
234 }
235
236 #[test]
237 fn test_process_exists_invalid() {
238 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 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}