Skip to main content

tokio_process_tools/process/
name.rs

1use std::borrow::Cow;
2use typed_builder::TypedBuilder;
3
4/// Controls how the process name is automatically generated when not explicitly provided.
5///
6/// This determines what information is included in the auto-generated process name
7/// used in public errors, logs, and debugging output. The default captures only
8/// the program name so command arguments and environment variables are not logged accidentally.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum AutoName {
11    /// Capture a name from the command as specified by the provided settings.
12    ///
13    /// Example: `ls "-la"` from `Command::new("ls").arg("-la")` when using
14    /// [`AutoName::program_with_args`].
15    Using(AutoNameSettings),
16
17    /// Capture the full Debug representation of the Command.
18    ///
19    /// Example: `"Command { std: \"ls\" \"-la\", kill_on_drop: false }"`
20    ///
21    /// Note: This includes internal implementation details and may change with tokio updates.
22    /// It may also include command arguments, environment variables, or other sensitive data.
23    Debug,
24}
25
26impl AutoName {
27    /// Capture only the program name.
28    ///
29    /// This is the safe default and excludes command arguments and environment variables.
30    #[must_use]
31    pub fn program_only() -> Self {
32        Self::Using(AutoNameSettings::program_only())
33    }
34
35    /// Capture the program name and all arguments.
36    ///
37    /// Arguments may contain sensitive data. Use this only when arguments are safe to log.
38    #[must_use]
39    pub fn program_with_args() -> Self {
40        Self::Using(AutoNameSettings::program_with_args())
41    }
42
43    /// Capture the program name together with all environment variables and arguments.
44    ///
45    /// Environment variables and arguments often contain credentials. Use this only when all
46    /// captured values are safe to log.
47    #[must_use]
48    pub fn program_with_env_and_args() -> Self {
49        Self::Using(AutoNameSettings::program_with_env_and_args())
50    }
51
52    /// Capture the current directory together with the program name, environment variables, and arguments.
53    ///
54    /// Current directories, environment variables, and arguments may contain sensitive data. Use
55    /// this only when all captured values are safe to log.
56    #[must_use]
57    pub fn full() -> Self {
58        Self::Using(AutoNameSettings::full())
59    }
60}
61
62impl Default for AutoName {
63    fn default() -> Self {
64        Self::program_only()
65    }
66}
67
68/// Controls in detail which parts of the command are automatically captured as the process name.
69///
70/// Process names are used in public error messages and tracing fields. Only capture
71/// arguments, environment variables, or current directories when those values are safe to log.
72/// Use [`AutoNameSettings::builder`] to opt into custom combinations; the builder defaults to
73/// capturing only the program name.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, TypedBuilder)]
75#[expect(
76    clippy::struct_excessive_bools,
77    reason = "each flag controls one optional part of the generated process name"
78)]
79pub struct AutoNameSettings {
80    #[builder(default = false)]
81    include_current_dir: bool,
82    #[builder(default = false)]
83    include_envs: bool,
84    #[builder(default = true, setter(skip))]
85    include_program: bool,
86    #[builder(default = false)]
87    include_args: bool,
88}
89
90impl Default for AutoNameSettings {
91    fn default() -> Self {
92        Self {
93            include_current_dir: false,
94            include_envs: false,
95            include_program: true,
96            include_args: false,
97        }
98    }
99}
100
101impl AutoNameSettings {
102    /// Capture the program name.
103    ///
104    /// Example: `Command::new("ls").arg("-la").env("FOO", "foo)` is captured as `"ls"`.
105    #[must_use]
106    pub fn program_only() -> Self {
107        Self::default()
108    }
109
110    /// Capture the program name and all arguments.
111    ///
112    /// Example: `Command::new("ls").arg("-la").env("FOO", "foo)` is captured as `ls "-la"`.
113    ///
114    /// Arguments may contain tokens, passwords, signed URLs, or headers. Use this only when
115    /// arguments are safe to include in public errors and logs or for debugging purposes.
116    #[must_use]
117    pub fn program_with_args() -> Self {
118        Self::builder().include_args(true).build()
119    }
120
121    /// Capture the program name and all environment variables and arguments.
122    ///
123    /// Example: `Command::new("ls").arg("-la").env("FOO", "foo)` is captured as `FOO=foo ls "-la"`.
124    ///
125    /// Environment variables and arguments often contain credentials. Use this only when all
126    /// captured values are safe to include in public errors and logs or for debugging purposes.
127    #[must_use]
128    pub fn program_with_env_and_args() -> Self {
129        Self::builder()
130            .include_envs(true)
131            .include_args(true)
132            .build()
133    }
134
135    /// Capture the directory and the program name and all environment variables and arguments.
136    ///
137    /// Example: `Command::new("ls").arg("-la").env("FOO", "foo)` is captured as
138    /// `/some/dir % FOO=foo ls "-la"`.
139    ///
140    /// Current directories, environment variables, and arguments may contain sensitive data.
141    /// Use this only when all captured values are safe to include in public errors and logs.
142    #[must_use]
143    pub fn full() -> Self {
144        Self::builder()
145            .include_current_dir(true)
146            .include_envs(true)
147            .include_args(true)
148            .build()
149    }
150
151    fn format_cmd(self, cmd: &std::process::Command) -> String {
152        let mut name = String::new();
153        if self.include_current_dir
154            && let Some(current_dir) = cmd.get_current_dir()
155        {
156            name.push_str(current_dir.to_string_lossy().as_ref());
157            name.push_str(" % ");
158        }
159        if self.include_envs {
160            for (key, value) in cmd
161                .get_envs()
162                .filter_map(|(key, value)| Some((key, value?)))
163            {
164                name.push_str(key.to_string_lossy().as_ref());
165                name.push('=');
166                name.push_str(value.to_string_lossy().as_ref());
167                name.push(' ');
168            }
169        }
170        if self.include_program {
171            name.push_str(cmd.get_program().to_string_lossy().as_ref());
172            name.push(' ');
173        }
174        if self.include_args {
175            for arg in cmd.get_args() {
176                name.push('"');
177                name.push_str(arg.to_string_lossy().as_ref());
178                name.push('"');
179                name.push(' ');
180            }
181        }
182        if name.ends_with(' ') {
183            name.pop();
184        }
185        name
186    }
187}
188
189/// Specifies how a process should be named.
190///
191/// This enum allows you to either provide an explicit name or configure automatic
192/// name generation. Using this type ensures you cannot accidentally set both an
193/// explicit name and an auto-naming mode at the same time.
194///
195/// Process names are used in public error messages and tracing fields. Prefer
196/// [`ProcessName::Explicit`] for stable safe labels when commands may contain secrets.
197#[derive(Debug, Clone)]
198pub enum ProcessName {
199    /// Use an explicit custom name.
200    ///
201    /// Example: `ProcessName::Explicit("my-server")`
202    Explicit(Cow<'static, str>),
203
204    /// Auto-generate the name based on the command.
205    ///
206    /// The default automatic mode captures only the program name. Use argument,
207    /// environment, or debug capture only when those values are safe to log.
208    Auto(AutoName),
209}
210
211impl Default for ProcessName {
212    fn default() -> Self {
213        Self::Auto(AutoName::default())
214    }
215}
216
217impl From<&'static str> for ProcessName {
218    fn from(s: &'static str) -> Self {
219        Self::Explicit(Cow::Borrowed(s))
220    }
221}
222
223impl From<String> for ProcessName {
224    fn from(s: String) -> Self {
225        Self::Explicit(Cow::Owned(s))
226    }
227}
228
229impl From<Cow<'static, str>> for ProcessName {
230    fn from(s: Cow<'static, str>) -> Self {
231        Self::Explicit(s)
232    }
233}
234
235impl From<AutoName> for ProcessName {
236    fn from(mode: AutoName) -> Self {
237        Self::Auto(mode)
238    }
239}
240
241impl From<AutoNameSettings> for AutoName {
242    fn from(settings: AutoNameSettings) -> Self {
243        Self::Using(settings)
244    }
245}
246
247impl From<AutoNameSettings> for ProcessName {
248    fn from(settings: AutoNameSettings) -> Self {
249        Self::Auto(settings.into())
250    }
251}
252
253pub(super) fn generate_name(
254    name: &ProcessName,
255    cmd: &tokio::process::Command,
256) -> Cow<'static, str> {
257    match name {
258        ProcessName::Explicit(name) => name.clone(),
259        ProcessName::Auto(auto_name) => match auto_name {
260            AutoName::Using(settings) => settings.format_cmd(cmd.as_std()).into(),
261            AutoName::Debug => format!("{cmd:?}").into(),
262        },
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use assertr::prelude::*;
270    use std::path::PathBuf;
271    use tokio::process::Command;
272
273    fn command_with_args_env_and_current_dir() -> Command {
274        let mut cmd = Command::new("ls");
275        cmd.arg("-la");
276        cmd.env("FOO", "foo");
277        cmd.current_dir(PathBuf::from("./"));
278        cmd
279    }
280
281    #[test]
282    fn auto_name_defaults_to_safe_program_only_naming() {
283        let mut cmd = command_with_args_env_and_current_dir();
284        let sensitive_arg = "--token=secret-token-should-not-be-logged";
285        cmd.arg(sensitive_arg);
286
287        let default_process_name = generated_name(ProcessName::default(), &cmd);
288        let default_auto_name = generated_name(AutoName::default(), &cmd);
289        let program_only_name = generated_name(AutoName::program_only(), &cmd);
290        let builder_default_name = generated_name(AutoNameSettings::builder().build(), &cmd);
291
292        for name in [
293            default_process_name.as_str(),
294            default_auto_name.as_str(),
295            program_only_name.as_str(),
296            builder_default_name.as_str(),
297        ] {
298            assert_that!(name).is_equal_to("ls");
299            assert_that!(name).does_not_contain(sensitive_arg);
300        }
301    }
302
303    #[test]
304    fn auto_name_presets_match_settings_and_expected_output() {
305        let cmd = command_with_args_env_and_current_dir();
306        let cases = [
307            (
308                AutoName::program_only(),
309                AutoNameSettings::program_only(),
310                "ls",
311            ),
312            (
313                AutoName::program_with_args(),
314                AutoNameSettings::program_with_args(),
315                "ls \"-la\"",
316            ),
317            (
318                AutoName::program_with_env_and_args(),
319                AutoNameSettings::program_with_env_and_args(),
320                "FOO=foo ls \"-la\"",
321            ),
322            (
323                AutoName::full(),
324                AutoNameSettings::full(),
325                "./ % FOO=foo ls \"-la\"",
326            ),
327        ];
328
329        for (auto_name, settings, expected) in cases {
330            assert_that!(auto_name).is_equal_to(AutoName::Using(settings));
331            assert_that!(generated_name(auto_name, &cmd)).is_equal_to(expected);
332            assert_that!(generated_name(settings, &cmd)).is_equal_to(expected);
333        }
334    }
335
336    #[test]
337    fn auto_name_debug_uses_command_debug_string() {
338        let cmd = command_with_args_env_and_current_dir();
339
340        assert_that!(generated_name(ProcessName::Auto(AutoName::Debug), &cmd)).is_equal_to(
341            "Command { std: cd \"./\" && FOO=\"foo\" \"ls\" \"-la\", kill_on_drop: false }",
342        );
343    }
344
345    #[test]
346    fn auto_name_settings_builder_supports_custom_combination() {
347        let cmd = command_with_args_env_and_current_dir();
348
349        assert_that!(generated_name(
350            AutoNameSettings::builder()
351                .include_current_dir(true)
352                .include_args(true)
353                .build(),
354            &cmd,
355        ))
356        .is_equal_to("./ % ls \"-la\"");
357    }
358
359    fn generated_name(name: impl Into<ProcessName>, cmd: &Command) -> String {
360        let name = name.into();
361        generate_name(&name, cmd).into_owned()
362    }
363}