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] =
83 [CTL_KERN, KERN_PROCARGS2, pid as libc::c_int];
84 let mut len: libc::size_t = 0;
86 let r = unsafe {
87 libc::sysctl(
88 name.as_mut_ptr(),
89 3,
90 std::ptr::null_mut(),
91 &mut len,
92 std::ptr::null_mut(),
93 0,
94 )
95 };
96 if r != 0 {
97 return Err(std::io::Error::last_os_error());
98 }
99 if len < std::mem::size_of::<i32>() {
100 return Err(std::io::Error::other(format!(
101 "KERN_PROCARGS2 returned size={len}, smaller than argc header",
102 )));
103 }
104
105 let mut buf = vec![0u8; len];
106 let r = unsafe {
107 libc::sysctl(
108 name.as_mut_ptr(),
109 3,
110 buf.as_mut_ptr() as *mut libc::c_void,
111 &mut len,
112 std::ptr::null_mut(),
113 0,
114 )
115 };
116 if r != 0 {
117 return Err(std::io::Error::last_os_error());
118 }
119 buf.truncate(len);
120 parse_procargs2(&buf)
121 }
122
123 fn parse_procargs2(buf: &[u8]) -> std::io::Result<String> {
124 if buf.len() < std::mem::size_of::<i32>() {
125 return Ok(String::new());
126 }
127 let argc = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
128 if argc <= 0 {
129 return Ok(String::new());
130 }
131 let mut cursor = std::mem::size_of::<i32>();
132 while cursor < buf.len() && buf[cursor] != 0 {
134 cursor += 1;
135 }
136 while cursor < buf.len() && buf[cursor] == 0 {
139 cursor += 1;
140 }
141 let mut argv: Vec<String> = Vec::with_capacity(argc as usize);
145 for _ in 0..argc {
146 if cursor >= buf.len() {
147 break;
148 }
149 let start = cursor;
150 while cursor < buf.len() && buf[cursor] != 0 {
151 cursor += 1;
152 }
153 argv.push(String::from_utf8_lossy(&buf[start..cursor]).into_owned());
154 cursor = cursor.saturating_add(1);
156 }
157 Ok(argv.join(" "))
158 }
159
160 #[cfg(test)]
161 mod tests {
162 use super::parse_procargs2;
163
164 fn build_procargs2(exec_path: &str, argv: &[&str]) -> Vec<u8> {
166 let mut buf = Vec::new();
167 let argc = argv.len() as i32;
168 buf.extend_from_slice(&argc.to_ne_bytes());
169 buf.extend_from_slice(exec_path.as_bytes());
170 buf.push(0);
171 while buf.len() % 8 != 0 {
174 buf.push(0);
175 }
176 for arg in argv {
177 buf.extend_from_slice(arg.as_bytes());
178 buf.push(0);
179 }
180 buf
182 }
183
184 #[test]
185 fn parses_argv_skipping_exec_path_and_padding() {
186 let buf = build_procargs2(
187 "/usr/bin/myprog",
188 &["myprog", "--flag", "value with space"],
189 );
190 let out = parse_procargs2(&buf).expect("parse");
191 assert_eq!(out, "myprog --flag value with space");
192 }
193
194 #[test]
195 fn empty_argv_yields_empty_string() {
196 let buf = build_procargs2("/usr/bin/noop", &[]);
197 let out = parse_procargs2(&buf).expect("parse");
198 assert_eq!(out, "");
199 }
200
201 #[test]
202 fn argc_zero_short_circuits() {
203 let mut buf = 0i32.to_ne_bytes().to_vec();
204 buf.extend_from_slice(b"/usr/bin/noop\0");
205 let out = parse_procargs2(&buf).expect("parse");
206 assert_eq!(out, "");
207 }
208 }
209}
210
211#[cfg(target_os = "linux")]
212mod linux_impl {
213 pub(super) fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
218 if pid == 0 {
219 return Err(std::io::Error::new(
220 std::io::ErrorKind::InvalidInput,
221 "pid 0 is the kernel scheduler — not queryable",
222 ));
223 }
224 let path = format!("/proc/{pid}/cmdline");
225 let bytes = std::fs::read(&path)?;
226 if bytes.is_empty() {
229 return Ok(String::new());
230 }
231 let mut trimmed = bytes.as_slice();
237 if trimmed.last() == Some(&0) {
238 trimmed = &trimmed[..trimmed.len() - 1];
239 }
240 let joined: Vec<u8> = trimmed
241 .iter()
242 .map(|b| if *b == 0 { b' ' } else { *b })
243 .collect();
244 Ok(String::from_utf8_lossy(&joined).into_owned())
245 }
246}
247
248#[cfg(target_os = "windows")]
249mod windows_impl {
250 use std::ffi::c_void;
255
256 const PROCESS_COMMAND_LINE_INFORMATION: i32 = 60;
260
261 const STATUS_INFO_LENGTH_MISMATCH: i32 = 0xC0000004u32 as i32;
264
265 const STATUS_SUCCESS: i32 = 0;
267
268 #[repr(C)]
269 struct UnicodeString {
270 length: u16,
271 maximum_length: u16,
272 buffer: *mut u16,
273 }
274
275 #[link(name = "ntdll")]
276 extern "system" {
277 fn NtQueryInformationProcess(
278 process_handle: *mut c_void,
279 process_information_class: i32,
280 process_information: *mut c_void,
281 process_information_length: u32,
282 return_length: *mut u32,
283 ) -> i32;
284 }
285
286 pub(super) fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
287 use winapi::um::handleapi::CloseHandle;
288 use winapi::um::processthreadsapi::OpenProcess;
289 use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION;
290
291 if pid == 0 {
292 return Err(std::io::Error::new(
293 std::io::ErrorKind::InvalidInput,
294 "pid 0 is the system idle process — not queryable",
295 ));
296 }
297
298 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
299 if handle.is_null() {
300 return Err(std::io::Error::last_os_error());
301 }
302
303 let result = query_cmdline(handle as *mut c_void);
304 unsafe { CloseHandle(handle) };
305 result
306 }
307
308 fn query_cmdline(handle: *mut c_void) -> std::io::Result<String> {
309 let mut needed: u32 = 0;
313 let status = unsafe {
314 NtQueryInformationProcess(
315 handle,
316 PROCESS_COMMAND_LINE_INFORMATION,
317 std::ptr::null_mut(),
318 0,
319 &mut needed,
320 )
321 };
322 if status != STATUS_INFO_LENGTH_MISMATCH && status != STATUS_SUCCESS {
323 return Err(std::io::Error::other(format!(
324 "NtQueryInformationProcess size probe returned status=0x{:08x}",
325 status as u32,
326 )));
327 }
328 if needed < std::mem::size_of::<UnicodeString>() as u32 {
329 return Err(std::io::Error::other(format!(
330 "NtQueryInformationProcess returned needed={needed}, smaller than UNICODE_STRING header",
331 )));
332 }
333
334 let mut buf = vec![0u8; needed as usize];
335 let mut returned: u32 = 0;
336 let status = unsafe {
337 NtQueryInformationProcess(
338 handle,
339 PROCESS_COMMAND_LINE_INFORMATION,
340 buf.as_mut_ptr() as *mut c_void,
341 needed,
342 &mut returned,
343 )
344 };
345 if status != STATUS_SUCCESS {
346 return Err(std::io::Error::other(format!(
347 "NtQueryInformationProcess returned status=0x{:08x}",
348 status as u32,
349 )));
350 }
351
352 let us = unsafe { std::ptr::read(buf.as_ptr() as *const UnicodeString) };
358 let len_bytes = us.length as usize;
359 if len_bytes == 0 {
360 return Ok(String::new());
361 }
362 let header_size = std::mem::size_of::<UnicodeString>();
368 if header_size + len_bytes > buf.len() {
369 return Err(std::io::Error::other(format!(
370 "NtQueryInformationProcess wrote less than {} bytes for cmdline (returned={returned}, len={len_bytes})",
371 header_size + len_bytes,
372 )));
373 }
374 let wide_slice: &[u16] = unsafe {
375 std::slice::from_raw_parts(buf[header_size..].as_ptr() as *const u16, len_bytes / 2)
376 };
377 Ok(String::from_utf16_lossy(wide_slice))
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
386 #[test]
387 fn read_cmdline_for_pid_zero_returns_invalid_input() {
388 let err = read_process_cmdline(0).expect_err("pid 0 should be rejected");
393 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
394 }
395
396 #[cfg(any(target_os = "linux", target_os = "macos"))]
397 #[test]
398 fn unix_read_cmdline_round_trips_known_args_from_spawned_child() {
399 use crate::observer::ObserverConfig;
400 use crate::{CommandSpec, NativeProcess, ProcessConfig, StderrMode, StdinMode};
401 use std::time::Duration;
402
403 let cfg = ProcessConfig {
408 command: CommandSpec::Argv(vec!["sleep".into(), "30".into()]),
409 cwd: None,
410 env: None,
411 capture: false,
412 stderr_mode: StderrMode::Stdout,
413 creationflags: None,
414 create_process_group: false,
415 stdin_mode: StdinMode::Inherit,
416 nice: None,
417 };
418 let (process, _sub) = NativeProcess::with_observer(cfg, ObserverConfig::lifecycle());
419 process.start().expect("spawn sleep");
420 let pid = process.pid().expect("pid");
421 std::thread::sleep(Duration::from_millis(100));
422
423 let cmdline = read_process_cmdline(pid).expect("read cmdline");
424 process.kill().ok();
425 process.close().ok();
426
427 assert!(
428 cmdline.contains("sleep"),
429 "expected 'sleep' in cmdline, got: {cmdline:?}"
430 );
431 assert!(
432 cmdline.contains("30"),
433 "expected '30' (the sleep duration) in cmdline, got: {cmdline:?}"
434 );
435 }
436
437 #[cfg(target_os = "linux")]
438 #[test]
439 fn linux_read_cmdline_for_nonexistent_pid_returns_not_found() {
440 let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
441 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
443 }
444
445 #[cfg(target_os = "macos")]
446 #[test]
447 fn macos_read_cmdline_for_nonexistent_pid_returns_io_error() {
448 let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
453 assert!(
454 err.raw_os_error().is_some(),
455 "expected an OS-level errno, got: {err}"
456 );
457 }
458
459 #[cfg(target_os = "windows")]
460 #[test]
461 fn read_cmdline_for_unknown_pid_returns_io_error() {
462 let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
466 assert!(
467 err.raw_os_error().is_some(),
468 "expected an OS-level error code, got: {err}"
469 );
470 }
471
472 #[cfg(target_os = "windows")]
473 #[test]
474 fn read_cmdline_round_trips_known_args_from_spawned_child() {
475 use crate::observer::ObserverConfig;
476 use crate::{CommandSpec, NativeProcess, ProcessConfig, StderrMode, StdinMode};
477 use std::time::Duration;
478
479 let cfg = ProcessConfig {
485 command: CommandSpec::Argv(vec![
486 "ping".into(),
487 "127.0.0.1".into(),
488 "-n".into(),
489 "30".into(),
490 ]),
491 cwd: None,
492 env: None,
493 capture: false,
494 stderr_mode: StderrMode::Stdout,
495 creationflags: None,
496 create_process_group: false,
497 stdin_mode: StdinMode::Inherit,
498 nice: None,
499 };
500 let (process, _sub) = NativeProcess::with_observer(cfg, ObserverConfig::lifecycle());
501 process.start().expect("spawn ping");
502 let pid = process.pid().expect("pid");
503 std::thread::sleep(Duration::from_millis(150));
506
507 let cmdline = read_process_cmdline(pid).expect("read cmdline");
508 process.kill().ok();
509 process.close().ok();
510
511 assert!(
514 cmdline.to_lowercase().contains("ping"),
515 "expected 'ping' in cmdline, got: {cmdline:?}"
516 );
517 assert!(
518 cmdline.contains("127.0.0.1"),
519 "expected target IP in cmdline, got: {cmdline:?}"
520 );
521 assert!(
522 cmdline.contains("30"),
523 "expected -n count in cmdline, got: {cmdline:?}"
524 );
525 }
526}