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(&mut self) -> &str {
182 &self.cmd
183 }
184
185 /// Get command arguments.
186 pub fn args(&mut self) -> &[String] {
187 &self.args
188 }
189
190 /// Get command options.
191 pub fn options(&mut 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) -> &HashMap<String, String> {
429 &self.envs
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}