proc_heim/process/model/
command.rs

1//! [`CmdOptionsError::StdoutConfigurationConflict`]: crate::model::command::CmdOptionsError::StdoutConfigurationConflict
2//! [`MessagingType::StandardIo`]: crate::model::command::MessagingType::StandardIo
3use std::{
4    collections::HashMap,
5    path::{Path, PathBuf},
6};
7
8/// Enum returned from fallible `Cmd` methods.
9#[derive(thiserror::Error, Debug)]
10pub enum CmdError {
11    /// No command name was provided.
12    #[error("No command name was provided")]
13    NoCommandNameProvided,
14}
15
16use super::Runnable;
17
18/// `Cmd` represents a single command.
19///
20/// It requires at least to set a command name.
21/// Command's arguments and options are optional.
22///
23/// Note that using input/output redirection symbols (eg. `|`, `>>`, `2>`) as command arguments will fail.
24/// Instead use [`Script`](struct@crate::model::script::Script).
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct Cmd {
28    pub(crate) cmd: String,
29    #[cfg_attr(feature = "serde", serde(default))]
30    pub(crate) args: Vec<String>,
31    #[cfg_attr(feature = "serde", serde(default))]
32    pub(crate) options: CmdOptions,
33}
34
35impl Cmd {
36    /// Creates a new command with given name.
37    /// # Examples
38    /// ```
39    /// # use proc_heim::model::command::Cmd;
40    /// Cmd::new("echo");
41    /// ```
42    pub fn new<S>(cmd: S) -> Self
43    where
44        S: Into<String>,
45    {
46        Self {
47            cmd: cmd.into(),
48            args: Vec::new(),
49            options: CmdOptions::default(),
50        }
51    }
52
53    /// Creates a new command with given name and arguments.
54    /// # Examples
55    /// ```
56    /// # use proc_heim::model::command::Cmd;
57    /// Cmd::with_args("ls", ["-l", "~"]);
58    /// ```
59    pub fn with_args<S, T, I>(cmd: S, args: I) -> Self
60    where
61        S: Into<String>,
62        T: Into<String>,
63        I: IntoIterator<Item = T>,
64    {
65        Self {
66            cmd: cmd.into(),
67            args: args.into_iter().map(Into::into).collect(),
68            options: CmdOptions::default(),
69        }
70    }
71
72    /// Creates a new command with given name and options.
73    /// # Examples
74    /// ```
75    /// # use proc_heim::model::command::*;
76    /// Cmd::with_options("ls", CmdOptions::default());
77    /// ```
78    pub fn with_options<S>(cmd: S, options: CmdOptions) -> Self
79    where
80        S: Into<String>,
81    {
82        Self {
83            cmd: cmd.into(),
84            args: Vec::new(),
85            options,
86        }
87    }
88
89    /// Creates a new command with given name, arguments and options.
90    /// # Examples
91    /// ```
92    /// # use proc_heim::model::command::*;
93    /// Cmd::with_args_and_options("ls", ["-l"], CmdOptions::default());
94    /// ```
95    pub fn with_args_and_options<S, T, I>(cmd: S, args: I, options: CmdOptions) -> Self
96    where
97        S: Into<String>,
98        T: Into<String>,
99        I: IntoIterator<Item = T>,
100    {
101        Self {
102            cmd: cmd.into(),
103            args: args.into_iter().map(Into::into).collect(),
104            options,
105        }
106    }
107
108    /// Try to create a new command from given whitespace separated string.
109    /// Notice that it will trim all whitespace characters.
110    /// # Examples
111    /// ```
112    /// # use proc_heim::model::command::*;
113    /// let cmd = Cmd::parse("ls -l /some/path").unwrap();
114    /// assert_eq!(cmd, Cmd::with_args("ls", ["-l", "/some/path"]));
115    /// # assert_eq!(cmd, Cmd::parse("ls -l    /some/path").unwrap());
116    /// ```
117    pub fn parse(cmd_string: &str) -> Result<Self, CmdError> {
118        let mut parts = cmd_string.split_ascii_whitespace();
119        if let Some(cmd) = parts.next() {
120            Ok(Cmd::with_args(cmd, parts))
121        } else {
122            Err(CmdError::NoCommandNameProvided)
123        }
124    }
125
126    /// Try to create a new command from given whitespace separated string and options.
127    /// Notice that it will trim all whitespace characters.
128    /// # Examples
129    /// ```
130    /// # use proc_heim::model::command::*;
131    /// let cmd = Cmd::parse_with_options("ls -l /some/path", CmdOptions::default());
132    /// ```
133    pub fn parse_with_options(cmd_string: &str, options: CmdOptions) -> Result<Self, CmdError> {
134        let mut cmd = Self::parse(cmd_string)?;
135        cmd.options = options;
136        Ok(cmd)
137    }
138
139    /// Set a command arguments.
140    /// # Examples
141    /// ```
142    /// # use proc_heim::model::command::*;
143    /// let mut cmd = Cmd::new("ls");
144    /// cmd.set_args(["-la", "~"]);
145    /// ```
146    pub fn set_args<S, I>(&mut self, args: I)
147    where
148        S: Into<String>,
149        I: IntoIterator<Item = S>,
150    {
151        self.args = args.into_iter().map(Into::into).collect();
152    }
153
154    /// Set a command options.
155    /// # Examples
156    /// ```
157    /// # use proc_heim::model::command::*;
158    /// let mut cmd = Cmd::new("ls");
159    /// cmd.set_options(CmdOptions::default());
160    /// ```
161    pub fn set_options(&mut self, options: CmdOptions) {
162        self.options = options;
163    }
164
165    /// Add a new argument to the end of argument list.
166    /// # Examples
167    /// ```
168    /// # use proc_heim::model::command::*;
169    /// let mut cmd = Cmd::new("ls");
170    /// cmd.add_arg("-l");
171    /// cmd.add_arg("/some/directory");
172    /// ```
173    pub fn add_arg<S>(&mut self, arg: S)
174    where
175        S: Into<String>,
176    {
177        self.args.push(arg.into());
178    }
179
180    /// Get command name.
181    pub fn cmd(&self) -> &str {
182        &self.cmd
183    }
184
185    /// Get command arguments.
186    pub fn args(&self) -> &[String] {
187        &self.args
188    }
189
190    /// Get command options.
191    pub fn options(&self) -> &CmdOptions {
192        &self.options
193    }
194
195    /// Update command options via mutable reference.
196    /// # Examples
197    /// ```
198    /// # use proc_heim::model::command::*;
199    /// let mut cmd = Cmd::new("env");
200    /// cmd.options_mut().add_env("TEST_ENV_VAR", "value");
201    /// ```
202    pub fn options_mut(&mut self) -> &mut CmdOptions {
203        &mut self.options
204    }
205}
206
207/// Wrapper type used to define buffer capacity.
208///
209/// Capacity must be greater than 0 and less or equal `usize::MAX / 2`.
210/// # Examples
211/// ```
212/// # use proc_heim::model::command::*;
213/// let capacity = BufferCapacity::try_from(16).unwrap();
214/// assert_eq!(16, *capacity.as_ref());
215/// ```
216/// ```
217/// # use proc_heim::model::command::*;
218/// let result = BufferCapacity::try_from(0);
219/// assert!(result.is_err());
220/// ```
221/// ```
222/// # use proc_heim::model::command::*;
223/// let result = BufferCapacity::try_from(usize::MAX / 2 + 1);
224/// assert!(result.is_err());
225/// ```
226#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
227pub struct BufferCapacity {
228    pub(crate) inner: usize,
229}
230
231impl TryFrom<usize> for BufferCapacity {
232    type Error = String;
233
234    fn try_from(value: usize) -> Result<Self, Self::Error> {
235        if value == 0 || value > usize::MAX / 2 {
236            Err("Buffer capacity must be greater than 0 and less or equal usize::MAX / 2".into())
237        } else {
238            Ok(Self { inner: value })
239        }
240    }
241}
242
243impl AsRef<usize> for BufferCapacity {
244    fn as_ref(&self) -> &usize {
245        &self.inner
246    }
247}
248
249/// Default capacity value is 16.
250impl Default for BufferCapacity {
251    fn default() -> Self {
252        Self { inner: 16 }
253    }
254}
255
256/// `CmdOptions` are used to describe command's additional settings.
257///
258/// It allows to configure command's input/outputs, working_directory and environment variables.
259///
260/// Command's input allows to send messages from parent process, and receive them in spawned (child) process.
261/// Whereas the message output of the command is used for communication in the opposite direction.
262/// Communication with a process can be done using standard I/O or named pipes.
263///
264/// It is also possible to set logging in order to allow child process to produce logs
265/// which, unlike messages, are stored permanently and therefore can be read multiple times by parent process.
266#[derive(Debug, Clone, Default, PartialEq, Eq)]
267#[cfg_attr(feature = "serde", derive(serde::Serialize))]
268pub struct CmdOptions {
269    pub(crate) current_dir: Option<PathBuf>,
270    pub(crate) clear_envs: bool,
271    pub(crate) envs: HashMap<String, String>,
272    pub(crate) envs_to_remove: Vec<String>,
273    pub(crate) output_buffer_capacity: BufferCapacity,
274    pub(crate) message_input: Option<MessagingType>,
275    pub(crate) message_output: Option<MessagingType>,
276    pub(crate) logging_type: Option<LoggingType>,
277}
278
279impl CmdOptions {
280    /// Create options with configured messaging input/output via standard I/O.
281    pub fn with_standard_io_messaging() -> CmdOptions {
282        Self::with_same_in_out(MessagingType::StandardIo)
283    }
284
285    /// Create options with configured messaging input/output via named pipes.
286    pub fn with_named_pipe_messaging() -> CmdOptions {
287        Self::with_same_in_out(MessagingType::NamedPipe)
288    }
289
290    fn with_same_in_out(messaging_type: MessagingType) -> CmdOptions {
291        CmdOptions {
292            message_input: messaging_type.clone().into(),
293            message_output: messaging_type.into(),
294            ..Default::default()
295        }
296    }
297
298    /// Create options with configured messaging input type.
299    pub fn with_message_input(message_input: MessagingType) -> Self {
300        Self {
301            message_input: message_input.into(),
302            ..Default::default()
303        }
304    }
305
306    /// Create options with configured messaging output type.
307    pub fn with_message_output(message_output: MessagingType) -> Self {
308        Self {
309            message_output: message_output.into(),
310            ..Default::default()
311        }
312    }
313
314    /// Create options with configured logging type.
315    pub fn with_logging(logging_type: LoggingType) -> Self {
316        Self {
317            logging_type: logging_type.into(),
318            ..Default::default()
319        }
320    }
321
322    /// Set process's working directory.
323    pub fn set_current_dir(&mut self, dir: PathBuf) {
324        self.current_dir = dir.into();
325    }
326
327    /// By default, child process will inherit all environment variables from the parent.
328    /// To prevent this behavior set this value to `true`.
329    pub fn clear_inherited_envs(&mut self, value: bool) {
330        self.clear_envs = value;
331    }
332
333    /// Set environment variables for a process.
334    /// # Examples
335    /// ```
336    /// # use proc_heim::model::command::*;
337    /// # use std::collections::HashMap;
338    /// let mut envs = HashMap::new();
339    /// envs.insert("TEST_ENV_VAR_1", "value1");
340    /// envs.insert("TEST_ENV_VAR_2", "value2");
341    ///
342    /// let mut options = CmdOptions::default();
343    /// options.set_envs(envs);
344    /// ```
345    pub fn set_envs<K, V, I>(&mut self, envs: I)
346    where
347        K: Into<String>,
348        V: Into<String>,
349        I: IntoIterator<Item = (K, V)>,
350    {
351        self.envs = envs
352            .into_iter()
353            .map(|(k, v)| (k.into(), v.into()))
354            .collect();
355    }
356
357    /// Add or update single environment variable.
358    pub fn add_env<K, V>(&mut self, name: K, value: V)
359    where
360        K: Into<String>,
361        V: Into<String>,
362    {
363        self.envs.insert(name.into(), value.into());
364    }
365
366    /// Remove single environment variable (manually set earlier and also inherited from the parent process).
367    pub fn remove_env<S>(&mut self, name: S)
368    where
369        S: Into<String> + AsRef<str>,
370    {
371        self.envs.remove(name.as_ref());
372        self.envs_to_remove.push(name.into());
373    }
374
375    /// Set message input type.
376    pub fn set_message_input(&mut self, messaging_type: MessagingType) {
377        self.message_input = messaging_type.into();
378    }
379
380    /// Set message output type.
381    ///
382    /// This method will return [`CmdOptionsError::StdoutConfigurationConflict`]
383    /// when trying to set [`MessagingType::StandardIo`] and logging to stdout was previously configured.
384    pub fn set_message_output(
385        &mut self,
386        messaging_type: MessagingType,
387    ) -> Result<(), CmdOptionsError> {
388        validate_stdout_config(Some(&messaging_type), self.logging_type.as_ref())?;
389        self.message_output = messaging_type.into();
390        Ok(())
391    }
392
393    /// Set logging type.
394    ///
395    /// This method will return [`CmdOptionsError::StdoutConfigurationConflict`]
396    /// when trying to set logging to stdout and message output was previously configured as [`MessagingType::StandardIo`].
397    pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), CmdOptionsError> {
398        validate_stdout_config(self.message_output.as_ref(), Some(&logging_type))?;
399        self.logging_type = logging_type.into();
400        Ok(())
401    }
402
403    /// Set message output buffer capacity for receiving end (parent process).
404    ///
405    /// When parent process is not reading messages produced by a child process,
406    /// then the messages are buffered up to the given `capacity` value.
407    /// If the buffer limit is reached and a child process sends a new message, the "oldest" buffered message will be removed.
408    pub fn set_message_output_buffer_capacity(&mut self, capacity: BufferCapacity) {
409        self.output_buffer_capacity = capacity;
410    }
411
412    /// Get current directory.
413    pub fn current_dir(&self) -> Option<&PathBuf> {
414        self.current_dir.as_ref()
415    }
416
417    /// Check if inherited environment variables will be cleared.
418    pub fn inherited_envs_cleared(&self) -> bool {
419        self.clear_envs
420    }
421
422    /// Get environment variables.
423    pub fn envs(&self) -> &HashMap<String, String> {
424        &self.envs
425    }
426
427    /// Get inherited environment variables to remove.
428    pub fn inherited_envs_to_remove(&self) -> &[String] {
429        &self.envs_to_remove
430    }
431
432    /// Get message input type.
433    pub fn message_input(&self) -> Option<&MessagingType> {
434        self.message_input.as_ref()
435    }
436
437    /// Get message output type.
438    pub fn message_output(&self) -> Option<&MessagingType> {
439        self.message_output.as_ref()
440    }
441
442    /// Get logging type.
443    pub fn logging_type(&self) -> Option<&LoggingType> {
444        self.logging_type.as_ref()
445    }
446
447    /// Get message output buffer capacity.
448    pub fn message_output_buffer_capacity(&self) -> &BufferCapacity {
449        &self.output_buffer_capacity
450    }
451
452    /// Update this options using values of other `CmdOptions`.
453    /// # Examples
454    /// ```
455    /// # use proc_heim::model::command::*;
456    /// # use std::collections::HashMap;
457    /// let mut options = CmdOptions::default();
458    /// let mut other = CmdOptions::with_standard_io_messaging();
459    /// other.clear_inherited_envs(true);
460    ///
461    /// options.update(other);
462    ///
463    /// let expected = MessagingType::StandardIo;
464    /// assert!(matches!(options.message_input(), expected));
465    /// assert!(matches!(options.message_output(), expected));
466    /// assert!(options.inherited_envs_cleared());
467    /// ```
468    pub fn update(&mut self, other: CmdOptions) {
469        if self.current_dir != other.current_dir {
470            self.current_dir = other.current_dir;
471        }
472        if self.clear_envs != other.clear_envs {
473            self.clear_envs = other.clear_envs;
474        }
475        if self.envs != other.envs {
476            self.envs = other.envs;
477        }
478        if self.envs_to_remove != other.envs_to_remove {
479            for env in other.envs_to_remove {
480                self.remove_env(env);
481            }
482        }
483        if self.message_input != other.message_input {
484            self.message_input = other.message_input;
485        }
486        if self.message_output != other.message_output {
487            self.message_output = other.message_output;
488        }
489        if self.logging_type != other.logging_type {
490            self.logging_type = other.logging_type;
491        }
492        if self.output_buffer_capacity != other.output_buffer_capacity {
493            self.output_buffer_capacity = other.output_buffer_capacity;
494        }
495    }
496}
497
498pub(crate) fn validate_stdout_config(
499    messaging_type: Option<&MessagingType>,
500    logging_type: Option<&LoggingType>,
501) -> Result<(), CmdOptionsError> {
502    if let (Some(messaging_type), Some(logging_type)) = (messaging_type, logging_type) {
503        if messaging_type == &MessagingType::StandardIo && logging_type != &LoggingType::StderrOnly
504        {
505            return Err(CmdOptionsError::StdoutConfigurationConflict(
506                messaging_type.to_owned(),
507                logging_type.to_owned(),
508            ));
509        }
510    }
511    Ok(())
512}
513
514/// Enum returned from fallible `CmdOptions` methods.
515#[derive(thiserror::Error, Debug)]
516pub enum CmdOptionsError {
517    /// Standard output can only be used for logging or messaging, but not both.
518    /// When you need to use both functionalities, then configure message output as [`MessagingType::NamedPipe`].
519    ///
520    /// For more information, see [`CmdOptions::set_message_output`] or [`CmdOptions::set_logging_type`].
521    #[error("Cannot use {0:?} together with {1:?} for stdout configuration")]
522    StdoutConfigurationConflict(MessagingType, LoggingType),
523}
524
525/// Enum representing messaging type of a spawned process.
526#[derive(Debug, Clone, PartialEq, Eq)]
527#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
528pub enum MessagingType {
529    /// Communicate with a spawned process via standard I/O.
530    StandardIo,
531    /// Communicate with a spawned process via named pipes.
532    NamedPipe,
533}
534
535/// Enum representing logging type of a spawned process.
536#[derive(Debug, Clone, PartialEq, Eq)]
537#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
538pub enum LoggingType {
539    /// Collect logs only from standard output stream.
540    StdoutOnly,
541    /// Collect logs only from standard error stream.
542    StderrOnly,
543    /// Collect logs from both: standard output and error streams.
544    StdoutAndStderr,
545    /// Collect logs from one stream, created by merged standard output and error streams.
546    StdoutAndStderrMerged,
547}
548
549impl Runnable for Cmd {
550    fn bootstrap_cmd(&self, _process_dir: &Path) -> Result<Cmd, String> {
551        Ok(self.clone())
552    }
553}