running_process/observer/
cmdline.rs1pub fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
32 #[cfg(target_os = "windows")]
33 {
34 windows_impl::read_process_cmdline(pid)
35 }
36 #[cfg(target_os = "linux")]
37 {
38 linux_impl::read_process_cmdline(pid)
39 }
40 #[cfg(target_os = "macos")]
41 {
42 macos_impl::read_process_cmdline(pid)
43 }
44 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
45 {
46 let _ = pid;
47 Err(std::io::Error::new(
48 std::io::ErrorKind::Unsupported,
49 "#539: no LaunchedProcessTree cmdline backend planned for this OS",
50 ))
51 }
52}
53
54#[cfg(target_os = "macos")]
55mod macos_impl {
56 const CTL_KERN: libc::c_int = 1;
73 const KERN_PROCARGS2: libc::c_int = 49;
74
75 pub(super) fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
76 if pid == 0 {
77 return Err(std::io::Error::new(
78 std::io::ErrorKind::InvalidInput,
79 "pid 0 is the kernel scheduler — not queryable",
80 ));
81 }
82 let mut name: [libc::c_int; 3] = [CTL_KERN, KERN_PROCARGS2, pid as libc::c_int];
83 let mut len: libc::size_t = 0;
85 let r = unsafe {
86 libc::sysctl(
87 name.as_mut_ptr(),
88 3,
89 std::ptr::null_mut(),
90 &mut len,
91 std::ptr::null_mut(),
92 0,
93 )
94 };
95 if r != 0 {
96 return Err(std::io::Error::last_os_error());
97 }
98 if len < std::mem::size_of::<i32>() {
99 return Err(std::io::Error::other(format!(
100 "KERN_PROCARGS2 returned size={len}, smaller than argc header",
101 )));
102 }
103
104 let mut buf = vec![0u8; len];
105 let r = unsafe {
106 libc::sysctl(
107 name.as_mut_ptr(),
108 3,
109 buf.as_mut_ptr() as *mut libc::c_void,
110 &mut len,
111 std::ptr::null_mut(),
112 0,
113 )
114 };
115 if r != 0 {
116 return Err(std::io::Error::last_os_error());
117 }
118 buf.truncate(len);
119 parse_procargs2(&buf)
120 }
121
122 fn parse_procargs2(buf: &[u8]) -> std::io::Result<String> {
123 if buf.len() < std::mem::size_of::<i32>() {
124 return Ok(String::new());
125 }
126 let argc = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
127 if argc <= 0 {
128 return Ok(String::new());
129 }
130 let mut cursor = std::mem::size_of::<i32>();
131 while cursor < buf.len() && buf[cursor] != 0 {
133 cursor += 1;
134 }
135 while cursor < buf.len() && buf[cursor] == 0 {
138 cursor += 1;
139 }
140 let mut argv: Vec<String> = Vec::with_capacity(argc as usize);
144 for _ in 0..argc {
145 if cursor >= buf.len() {
146 break;
147 }
148 let start = cursor;
149 while cursor < buf.len() && buf[cursor] != 0 {
150 cursor += 1;
151 }
152 argv.push(String::from_utf8_lossy(&buf[start..cursor]).into_owned());
153 cursor = cursor.saturating_add(1);
155 }
156 Ok(argv.join(" "))
157 }
158
159 #[cfg(test)]
160 mod tests {
161 use super::parse_procargs2;
162
163 fn build_procargs2(exec_path: &str, argv: &[&str]) -> Vec<u8> {
165 let mut buf = Vec::new();
166 let argc = argv.len() as i32;
167 buf.extend_from_slice(&argc.to_ne_bytes());
168 buf.extend_from_slice(exec_path.as_bytes());
169 buf.push(0);
170 while buf.len() % 8 != 0 {
173 buf.push(0);
174 }
175 for arg in argv {
176 buf.extend_from_slice(arg.as_bytes());
177 buf.push(0);
178 }
179 buf
181 }
182
183 #[test]
184 fn parses_argv_skipping_exec_path_and_padding() {
185 let buf = build_procargs2("/usr/bin/myprog", &["myprog", "--flag", "value with space"]);
186 let out = parse_procargs2(&buf).expect("parse");
187 assert_eq!(out, "myprog --flag value with space");
188 }
189
190 #[test]
191 fn empty_argv_yields_empty_string() {
192 let buf = build_procargs2("/usr/bin/noop", &[]);
193 let out = parse_procargs2(&buf).expect("parse");
194 assert_eq!(out, "");
195 }
196
197 #[test]
198 fn argc_zero_short_circuits() {
199 let mut buf = 0i32.to_ne_bytes().to_vec();
200 buf.extend_from_slice(b"/usr/bin/noop\0");
201 let out = parse_procargs2(&buf).expect("parse");
202 assert_eq!(out, "");
203 }
204 }
205}
206
207#[cfg(target_os = "linux")]
208mod linux_impl {
209 pub(super) fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
214 if pid == 0 {
215 return Err(std::io::Error::new(
216 std::io::ErrorKind::InvalidInput,
217 "pid 0 is the kernel scheduler — not queryable",
218 ));
219 }
220 let path = format!("/proc/{pid}/cmdline");
221 let bytes = std::fs::read(&path)?;
222 if bytes.is_empty() {
225 return Ok(String::new());
226 }
227 let mut trimmed = bytes.as_slice();
233 if trimmed.last() == Some(&0) {
234 trimmed = &trimmed[..trimmed.len() - 1];
235 }
236 let joined: Vec<u8> = trimmed
237 .iter()
238 .map(|b| if *b == 0 { b' ' } else { *b })
239 .collect();
240 Ok(String::from_utf8_lossy(&joined).into_owned())
241 }
242}
243
244#[cfg(target_os = "windows")]
245mod windows_impl {
246 use std::ffi::c_void;
251
252 const PROCESS_COMMAND_LINE_INFORMATION: i32 = 60;
256
257 const STATUS_INFO_LENGTH_MISMATCH: i32 = 0xC0000004u32 as i32;
260
261 const STATUS_SUCCESS: i32 = 0;
263
264 #[repr(C)]
265 struct UnicodeString {
266 length: u16,
267 maximum_length: u16,
268 buffer: *mut u16,
269 }
270
271 #[link(name = "ntdll")]
272 extern "system" {
273 fn NtQueryInformationProcess(
274 process_handle: *mut c_void,
275 process_information_class: i32,
276 process_information: *mut c_void,
277 process_information_length: u32,
278 return_length: *mut u32,
279 ) -> i32;
280 }
281
282 pub(super) fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
283 use winapi::um::handleapi::CloseHandle;
284 use winapi::um::processthreadsapi::OpenProcess;
285 use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION;
286
287 if pid == 0 {
288 return Err(std::io::Error::new(
289 std::io::ErrorKind::InvalidInput,
290 "pid 0 is the system idle process — not queryable",
291 ));
292 }
293
294 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
295 if handle.is_null() {
296 return Err(std::io::Error::last_os_error());
297 }
298
299 let result = query_cmdline(handle as *mut c_void);
300 unsafe { CloseHandle(handle) };
301 result
302 }
303
304 fn query_cmdline(handle: *mut c_void) -> std::io::Result<String> {
305 let mut needed: u32 = 0;
309 let status = unsafe {
310 NtQueryInformationProcess(
311 handle,
312 PROCESS_COMMAND_LINE_INFORMATION,
313 std::ptr::null_mut(),
314 0,
315 &mut needed,
316 )
317 };
318 if status != STATUS_INFO_LENGTH_MISMATCH && status != STATUS_SUCCESS {
319 return Err(std::io::Error::other(format!(
320 "NtQueryInformationProcess size probe returned status=0x{:08x}",
321 status as u32,
322 )));
323 }
324 if needed < std::mem::size_of::<UnicodeString>() as u32 {
325 return Err(std::io::Error::other(format!(
326 "NtQueryInformationProcess returned needed={needed}, smaller than UNICODE_STRING header",
327 )));
328 }
329
330 let mut buf = vec![0u8; needed as usize];
331 let mut returned: u32 = 0;
332 let status = unsafe {
333 NtQueryInformationProcess(
334 handle,
335 PROCESS_COMMAND_LINE_INFORMATION,
336 buf.as_mut_ptr() as *mut c_void,
337 needed,
338 &mut returned,
339 )
340 };
341 if status != STATUS_SUCCESS {
342 return Err(std::io::Error::other(format!(
343 "NtQueryInformationProcess returned status=0x{:08x}",
344 status as u32,
345 )));
346 }
347
348 let us = unsafe { std::ptr::read(buf.as_ptr() as *const UnicodeString) };
354 let len_bytes = us.length as usize;
355 if len_bytes == 0 {
356 return Ok(String::new());
357 }
358 let header_size = std::mem::size_of::<UnicodeString>();
364 if header_size + len_bytes > buf.len() {
365 return Err(std::io::Error::other(format!(
366 "NtQueryInformationProcess wrote less than {} bytes for cmdline (returned={returned}, len={len_bytes})",
367 header_size + len_bytes,
368 )));
369 }
370 let wide_slice: &[u16] = unsafe {
371 std::slice::from_raw_parts(buf[header_size..].as_ptr() as *const u16, len_bytes / 2)
372 };
373 Ok(String::from_utf16_lossy(wide_slice))
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
382 #[test]
383 fn read_cmdline_for_pid_zero_returns_invalid_input() {
384 let err = read_process_cmdline(0).expect_err("pid 0 should be rejected");
389 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
390 }
391
392 #[cfg(any(target_os = "linux", target_os = "macos"))]
393 #[test]
394 fn unix_read_cmdline_round_trips_known_args_from_spawned_child() {
395 use crate::observer::ObserverConfig;
396 use crate::{CommandSpec, NativeProcess, ProcessConfig, StderrMode, StdinMode};
397 use std::time::Duration;
398
399 let cfg = ProcessConfig {
404 command: CommandSpec::Argv(vec!["sleep".into(), "30".into()]),
405 cwd: None,
406 env: None,
407 capture: false,
408 stderr_mode: StderrMode::Stdout,
409 creationflags: None,
410 create_process_group: false,
411 stdin_mode: StdinMode::Inherit,
412 nice: None,
413 };
414 let (process, _sub) = NativeProcess::with_observer(cfg, ObserverConfig::lifecycle());
415 process.start().expect("spawn sleep");
416 let pid = process.pid().expect("pid");
417 std::thread::sleep(Duration::from_millis(100));
418
419 let cmdline = read_process_cmdline(pid).expect("read cmdline");
420 process.kill().ok();
421 process.close().ok();
422
423 assert!(
424 cmdline.contains("sleep"),
425 "expected 'sleep' in cmdline, got: {cmdline:?}"
426 );
427 assert!(
428 cmdline.contains("30"),
429 "expected '30' (the sleep duration) in cmdline, got: {cmdline:?}"
430 );
431 }
432
433 #[cfg(target_os = "linux")]
434 #[test]
435 fn linux_read_cmdline_for_nonexistent_pid_returns_not_found() {
436 let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
437 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
439 }
440
441 #[cfg(target_os = "macos")]
442 #[test]
443 fn macos_read_cmdline_for_nonexistent_pid_returns_io_error() {
444 let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
449 assert!(
450 err.raw_os_error().is_some(),
451 "expected an OS-level errno, got: {err}"
452 );
453 }
454
455 #[cfg(target_os = "windows")]
456 #[test]
457 fn read_cmdline_for_unknown_pid_returns_io_error() {
458 let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
462 assert!(
463 err.raw_os_error().is_some(),
464 "expected an OS-level error code, got: {err}"
465 );
466 }
467
468 #[cfg(target_os = "windows")]
469 #[test]
470 fn read_cmdline_round_trips_known_args_from_spawned_child() {
471 use crate::observer::ObserverConfig;
472 use crate::{CommandSpec, NativeProcess, ProcessConfig, StderrMode, StdinMode};
473 use std::time::Duration;
474
475 let cfg = ProcessConfig {
481 command: CommandSpec::Argv(vec![
482 "ping".into(),
483 "127.0.0.1".into(),
484 "-n".into(),
485 "30".into(),
486 ]),
487 cwd: None,
488 env: None,
489 capture: false,
490 stderr_mode: StderrMode::Stdout,
491 creationflags: None,
492 create_process_group: false,
493 stdin_mode: StdinMode::Inherit,
494 nice: None,
495 };
496 let (process, _sub) = NativeProcess::with_observer(cfg, ObserverConfig::lifecycle());
497 process.start().expect("spawn ping");
498 let pid = process.pid().expect("pid");
499 std::thread::sleep(Duration::from_millis(150));
502
503 let cmdline = read_process_cmdline(pid).expect("read cmdline");
504 process.kill().ok();
505 process.close().ok();
506
507 assert!(
510 cmdline.to_lowercase().contains("ping"),
511 "expected 'ping' in cmdline, got: {cmdline:?}"
512 );
513 assert!(
514 cmdline.contains("127.0.0.1"),
515 "expected target IP in cmdline, got: {cmdline:?}"
516 );
517 assert!(
518 cmdline.contains("30"),
519 "expected -n count in cmdline, got: {cmdline:?}"
520 );
521 }
522}