Skip to main content

running_process/observer/
cmdline.rs

1//! Portable facade for native command-line inspection.
2//!
3//! [`read_process_argv`] is the canonical API for policy or execution
4//! decisions. [`read_process_cmdline`] is a stable display string retained
5//! for logs and diagnostics; it is not shell syntax and cannot preserve every
6//! argument boundary on every host.
7
8/// Read a process's argument vector without flattening argument boundaries.
9///
10/// The returned values use [`std::ffi::OsString`] so Unix's opaque argv bytes
11/// and Windows' UTF-16 arguments remain representable. This is the only
12/// command-inspection API suitable for matching an executable or argument.
13pub fn read_process_argv(pid: u32) -> std::io::Result<Vec<std::ffi::OsString>> {
14    running_process_platform_internal::platform::process::read_process_argv(pid)
15}
16
17/// Read a stable human-readable process command display string.
18///
19/// This legacy API deliberately does not promise shell quoting or lossless
20/// argument boundaries. Use [`read_process_argv`] for structured inspection.
21pub fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
22    running_process_platform_internal::platform::process::read_process_cmdline(pid)
23}
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28    use crate::observer::ObserverConfig;
29    use crate::{CommandSpec, NativeProcess, ProcessConfig, StderrMode, StdinMode};
30    use std::time::Duration;
31
32    fn fixture_program() -> String {
33        let exe = std::env::current_exe().expect("test executable path");
34        let dir = exe
35            .parent()
36            .and_then(std::path::Path::parent)
37            .expect("test binary should live in <profile>/deps/");
38        dir.join(format!(
39            "testbin-stdio-scripted{}",
40            std::env::consts::EXE_SUFFIX
41        ))
42        .to_string_lossy()
43        .into_owned()
44    }
45
46    fn config(args: Vec<String>) -> ProcessConfig {
47        ProcessConfig {
48            command: CommandSpec::Argv(args),
49            cwd: None,
50            env: None,
51            capture: false,
52            stderr_mode: StderrMode::Stdout,
53            creationflags: None,
54            create_process_group: false,
55            stdin_mode: StdinMode::Inherit,
56            nice: None,
57            address_space_limit_bytes: None,
58        }
59    }
60
61    #[test]
62    fn read_cmdline_for_pid_zero_returns_invalid_input() {
63        let err = read_process_cmdline(0).expect_err("pid 0 should be rejected");
64        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
65    }
66
67    #[test]
68    fn read_cmdline_round_trips_known_args_from_spawned_child() {
69        let marker = "running-process-cmdline-marker";
70        let (process, _sub) = NativeProcess::with_observer(
71            config(vec![
72                fixture_program(),
73                "sleep-ms:30000".into(),
74                format!("out:{marker}"),
75            ]),
76            ObserverConfig::lifecycle(),
77        );
78        process.start().expect("spawn fixture");
79        let pid = process.pid().expect("pid");
80        std::thread::sleep(Duration::from_millis(150));
81
82        let argv = read_process_argv(pid).expect("read argv");
83        let cmdline = read_process_cmdline(pid).expect("read display string");
84        process.kill().ok();
85        process.close().ok();
86        assert!(
87            argv[0].to_string_lossy().contains("testbin-stdio-scripted"),
88            "expected fixture name in argv, got: {argv:?}"
89        );
90        assert!(
91            argv.iter()
92                .any(|argument| argument.to_string_lossy().contains(marker)),
93            "expected marker in argv, got: {argv:?}"
94        );
95        assert!(
96            cmdline.contains("testbin-stdio-scripted"),
97            "expected fixture name in display string, got: {cmdline:?}"
98        );
99        assert!(
100            cmdline.contains(marker),
101            "expected marker in display string, got: {cmdline:?}"
102        );
103    }
104
105    #[test]
106    fn argv_preserves_ambiguous_argument_boundaries() {
107        let args = vec![
108            fixture_program(),
109            "sleep-ms:30000".into(),
110            "has space".into(),
111            "quote\"".into(),
112            String::new(),
113            r"back\slash".into(),
114        ];
115        let (process, _sub) =
116            NativeProcess::with_observer(config(args.clone()), ObserverConfig::lifecycle());
117        process.start().expect("spawn fixture");
118        let pid = process.pid().expect("pid");
119        std::thread::sleep(Duration::from_millis(150));
120
121        let argv = read_process_argv(pid).expect("read argv");
122        let cmdline = read_process_cmdline(pid).expect("read display string");
123        process.kill().ok();
124        process.close().ok();
125        assert_eq!(
126            argv.iter()
127                .map(|argument| argument.to_string_lossy().into_owned())
128                .collect::<Vec<_>>(),
129            args
130        );
131        assert!(
132            cmdline.contains("has space") && cmdline.contains(r"back\slash"),
133            "native display string unexpectedly changed: {cmdline:?}"
134        );
135    }
136
137    #[test]
138    fn read_cmdline_for_unknown_pid_returns_an_os_error() {
139        let err = read_process_cmdline(0x7FFF_FFFE).expect_err("nonexistent pid");
140        assert!(
141            err.raw_os_error().is_some() || err.kind() == std::io::ErrorKind::NotFound,
142            "expected an OS-level missing-process error, got: {err}"
143        );
144    }
145}