tcrm_task/tasks/
config.rs

1use std::{collections::HashMap, sync::Arc};
2
3use crate::tasks::{error::TaskError, validator::ConfigValidator};
4
5/// Configuration for a task to be executed.
6///
7/// `TaskConfig` defines all parameters needed to execute a process.
8///
9/// # Examples
10///
11/// ## Basic Command
12/// ```rust
13/// use tcrm_task::tasks::config::TaskConfig;
14///
15/// let config = TaskConfig::new("cmd")
16///     .args(["/C", "dir", "C:\\"]);
17/// ```
18///
19/// ## Complex Configuration
20/// ```rust
21/// use tcrm_task::tasks::config::{TaskConfig, StreamSource};
22/// use std::collections::HashMap;
23///
24/// let mut env = HashMap::new();
25/// env.insert("PATH".to_string(), "C:\\Windows\\System32".to_string());
26///
27/// let config = TaskConfig::new("cmd")
28///     .args(["/C", "echo", "Server started"])
29///     .working_dir("C:\\")
30///     .env(env)
31///     .timeout_ms(30000)
32///     .enable_stdin(true)
33///     .ready_indicator("Server started")
34///     .ready_indicator_source(StreamSource::Stdout);
35/// ```
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[derive(Debug, Clone, PartialEq)]
38pub struct TaskConfig {
39    /// The command or executable to run
40    pub command: String,
41
42    /// Arguments to pass to the command
43    pub args: Option<Vec<String>>,
44
45    /// Working directory for the command
46    pub working_dir: Option<String>,
47
48    /// Environment variables for the command
49    pub env: Option<HashMap<String, String>>,
50
51    /// Maximum allowed runtime in milliseconds
52    pub timeout_ms: Option<u64>,
53
54    /// Allow providing input to the task via stdin
55    pub enable_stdin: Option<bool>,
56
57    /// Optional string to indicate the task is ready (for long-running processes like servers)
58    pub ready_indicator: Option<String>,
59
60    /// Source of the ready indicator string (stdout/stderr)
61    pub ready_indicator_source: Option<StreamSource>,
62
63    /// Enable process group management for child process termination (default: true)
64    ///
65    /// When enabled, creates process groups (Unix) or Job Objects (Windows) to ensure
66    /// all child processes and their descendants are terminated when the main process is killed.
67    #[cfg(feature = "process-group")]
68    pub use_process_group: Option<bool>,
69}
70
71pub type SharedTaskConfig = Arc<TaskConfig>;
72impl Default for TaskConfig {
73    fn default() -> Self {
74        TaskConfig {
75            command: String::new(),
76            args: None,
77            working_dir: None,
78            env: None,
79            timeout_ms: None,
80            enable_stdin: Some(false),
81            ready_indicator: None,
82            ready_indicator_source: Some(StreamSource::Stdout),
83            #[cfg(feature = "process-group")]
84            use_process_group: Some(true),
85        }
86    }
87}
88
89impl TaskConfig {
90    /// Create a new task configuration with the given command
91    ///
92    /// # Arguments
93    ///
94    /// * `command` - The executable command to run (e.g., "ls", "node", "python")
95    ///
96    /// # Examples
97    /// ```rust
98    /// use tcrm_task::tasks::config::TaskConfig;
99    ///
100    /// let config1 = TaskConfig::new("echo");
101    /// let config2 = TaskConfig::new("Powershell").args(["-Command", "echo"]);
102    /// ```
103    pub fn new(command: impl Into<String>) -> Self {
104        TaskConfig {
105            command: command.into(),
106            ..Default::default()
107        }
108    }
109
110    /// Set the arguments for the command
111    ///
112    /// # Arguments
113    ///
114    /// * `args` - Iterator of arguments to pass to the command
115    ///
116    /// # Examples
117    /// ```rust
118    /// use tcrm_task::tasks::config::TaskConfig;
119    ///
120    /// let config = TaskConfig::new("ls")
121    ///     .args(["-la", "/tmp"]);
122    ///     
123    /// let config2 = TaskConfig::new("cargo")
124    ///     .args(vec!["build", "--release"]);
125    /// ```
126    #[must_use]
127    pub fn args<I, S>(mut self, args: I) -> Self
128    where
129        I: IntoIterator<Item = S>,
130        S: Into<String>,
131    {
132        self.args = Some(args.into_iter().map(Into::into).collect());
133        self
134    }
135
136    /// Set the working directory for the command
137    ///
138    /// The working directory must exist when the task is executed.
139    ///
140    /// # Arguments
141    ///
142    /// * `dir` - Path to the working directory
143    ///
144    /// # Examples
145    /// ```rust
146    /// use tcrm_task::tasks::config::TaskConfig;
147    ///
148    /// let config = TaskConfig::new("ls")
149    ///     .working_dir("/tmp");
150    ///     
151    /// let config2 = TaskConfig::new("cargo")
152    ///     .working_dir("/path/to/project");
153    /// ```
154    #[must_use]
155    pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
156        self.working_dir = Some(dir.into());
157        self
158    }
159
160    /// Set environment variables for the command
161    ///
162    /// # Arguments
163    ///
164    /// * `env` - Iterator of (key, value) pairs for environment variables
165    ///
166    /// # Examples
167    /// ```rust
168    /// use tcrm_task::tasks::config::TaskConfig;
169    /// use std::collections::HashMap;
170    ///
171    /// // Using tuples
172    /// let config = TaskConfig::new("node")
173    ///     .env([("NODE_ENV", "production"), ("PORT", "3000")]);
174    ///
175    /// // Using HashMap
176    /// let mut env = HashMap::new();
177    /// env.insert("RUST_LOG".to_string(), "debug".to_string());
178    /// let config2 = TaskConfig::new("cargo")
179    ///     .env(env);
180    /// ```
181    #[must_use]
182    pub fn env<K, V, I>(mut self, env: I) -> Self
183    where
184        K: Into<String>,
185        V: Into<String>,
186        I: IntoIterator<Item = (K, V)>,
187    {
188        self.env = Some(env.into_iter().map(|(k, v)| (k.into(), v.into())).collect());
189        self
190    }
191
192    /// Set the maximum allowed runtime in milliseconds
193    ///
194    /// If the task runs longer than this timeout, it will be terminated.
195    ///
196    /// # Arguments
197    ///
198    /// * `timeout` - Timeout in milliseconds (must be > 0)
199    ///
200    /// # Examples
201    /// ```rust
202    /// use tcrm_task::tasks::config::TaskConfig;
203    ///
204    /// // 30 second timeout
205    /// let config = TaskConfig::new("long-running-task")
206    ///     .timeout_ms(30000);
207    ///
208    /// // 5 minute timeout
209    /// let config2 = TaskConfig::new("build-script")
210    ///     .timeout_ms(300000);
211    /// ```
212    #[must_use]
213    pub fn timeout_ms(mut self, timeout: u64) -> Self {
214        self.timeout_ms = Some(timeout);
215        self
216    }
217
218    /// Enable or disable stdin for the task
219    ///
220    /// When enabled, you can send input to the process via the stdin channel.
221    ///
222    /// # Arguments
223    ///
224    /// * `b` - Whether to enable stdin input
225    ///
226    /// # Examples
227    /// ```rust
228    /// use tcrm_task::tasks::config::TaskConfig;
229    ///
230    /// // Interactive command that needs input
231    /// let config = TaskConfig::new("python")
232    ///     .args(["-i"])
233    ///     .enable_stdin(true);
234    /// ```
235    #[must_use]
236    pub fn enable_stdin(mut self, b: bool) -> Self {
237        self.enable_stdin = Some(b);
238        self
239    }
240
241    /// Set the ready indicator for the task
242    ///
243    /// For long-running processes (like servers), this string indicates when
244    /// the process is ready to accept requests. When this string appears in
245    /// the process output, a Ready event will be emitted.
246    ///
247    /// # Arguments
248    ///
249    /// * `indicator` - String to look for in process output
250    ///
251    /// # Examples
252    /// ```rust
253    /// use tcrm_task::tasks::config::TaskConfig;
254    ///
255    /// let config = TaskConfig::new("my-server")
256    ///     .ready_indicator("Server listening on port");
257    ///
258    /// let config2 = TaskConfig::new("database")
259    ///     .ready_indicator("Database ready for connections");
260    /// ```
261    #[must_use]
262    pub fn ready_indicator(mut self, indicator: impl Into<String>) -> Self {
263        self.ready_indicator = Some(indicator.into());
264        self
265    }
266
267    /// Set the source of the ready indicator
268    ///
269    /// Specifies whether to look for the ready indicator in stdout or stderr.
270    ///
271    /// # Arguments
272    ///
273    /// * `source` - Stream source (Stdout or Stderr)
274    ///
275    /// # Examples
276    /// ```rust
277    /// use tcrm_task::tasks::config::{TaskConfig, StreamSource};
278    ///
279    /// let config = TaskConfig::new("my-server")
280    ///     .ready_indicator("Ready")
281    ///     .ready_indicator_source(StreamSource::Stderr);
282    /// ```
283    #[must_use]
284    pub fn ready_indicator_source(mut self, source: StreamSource) -> Self {
285        self.ready_indicator_source = Some(source);
286        self
287    }
288
289    /// Enable or disable process group management
290    ///
291    /// When enabled (default), creates process groups on Unix or Job Objects on Windows
292    /// to ensure all child processes and their descendants are terminated when the main
293    /// process is killed. This prevents orphaned processes.
294    ///
295    /// # Arguments
296    ///
297    /// * `enabled` - Whether to use process group management
298    ///
299    /// # Examples
300    /// ```rust
301    /// use tcrm_task::tasks::config::TaskConfig;
302    ///
303    /// // Disable process group management
304    /// let config = TaskConfig::new("cmd")
305    ///     .use_process_group(false);
306    ///     
307    /// // Explicitly enable (though it's enabled by default)
308    /// let config2 = TaskConfig::new("node")
309    ///     .use_process_group(true);
310    /// ```
311    #[must_use]
312    #[cfg(feature = "process-group")]
313    pub fn use_process_group(mut self, enabled: bool) -> Self {
314        self.use_process_group = Some(enabled);
315        self
316    }
317
318    /// Validate the configuration
319    ///
320    /// Validates all configuration parameters.
321    ///
322    /// # Validation Checks
323    /// - all fields length limits
324    /// - **Command**: Must not be empty
325    /// - **Arguments**: Must not contain null bytes or shell injection patterns  
326    /// - **Working Directory**: Must exist and be a valid directory
327    /// - **Environment Variables**: Keys must not contain spaces, '=', or null bytes
328    /// - **Timeout**: Must be greater than 0 if specified
329    /// - **Ready Indicator**: Must not be empty if specified
330    ///
331    /// # Returns
332    ///
333    /// - `Ok(())` if the configuration is valid
334    /// - `Err(TaskError::InvalidConfiguration)` with details if validation fails
335    ///
336    /// # Errors
337    ///
338    /// Returns a [`TaskError`] if any validation check fails:
339    /// - [`TaskError::InvalidConfiguration`] for configuration errors
340    /// - [`TaskError::IO`] for working directory path not found
341    ///
342    /// # Examples
343    ///
344    /// ```rust
345    /// use tcrm_task::tasks::config::TaskConfig;
346    ///
347    /// // Valid config
348    /// let config = TaskConfig::new("echo")
349    ///     .args(["hello", "world"]);
350    /// assert!(config.validate().is_ok());
351    ///
352    /// // Invalid config (empty command)
353    /// let config = TaskConfig::new("");
354    /// assert!(config.validate().is_err());
355    ///
356    /// // Invalid config (zero timeout)
357    /// let config = TaskConfig::new("sleep")
358    ///     .timeout_ms(0);
359    /// assert!(config.validate().is_err());
360    /// ```
361    pub fn validate(&self) -> Result<(), TaskError> {
362        ConfigValidator::validate_command(&self.command)?;
363        if let Some(ready_indicator) = &self.ready_indicator {
364            ConfigValidator::validate_ready_indicator(ready_indicator)?;
365        }
366        if let Some(args) = &self.args {
367            ConfigValidator::validate_args(args)?;
368        }
369        if let Some(dir) = &self.working_dir {
370            ConfigValidator::validate_working_dir(dir)?;
371        }
372        if let Some(env) = &self.env {
373            ConfigValidator::validate_env_vars(env)?;
374        }
375        if let Some(timeout) = &self.timeout_ms {
376            ConfigValidator::validate_timeout(timeout)?;
377        }
378        Ok(())
379    }
380}
381
382/// Specifies the source stream for output monitoring
383///
384/// Used with ready indicators to specify whether to monitor stdout or stderr
385/// for the ready signal from long-running processes.
386///
387/// # Examples
388///
389/// ```rust
390/// use tcrm_task::tasks::config::{TaskConfig, StreamSource};
391///
392/// // Monitor stdout for ready signal
393/// let config = TaskConfig::new("web-server")
394///     .ready_indicator("Server ready")
395///     .ready_indicator_source(StreamSource::Stdout);
396///
397/// // Monitor stderr for ready signal  
398/// let config2 = TaskConfig::new("database")
399///     .ready_indicator("Ready for connections")
400///     .ready_indicator_source(StreamSource::Stderr);
401/// ```
402#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
403#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
404#[derive(Debug, Clone, PartialEq)]
405pub enum StreamSource {
406    /// Standard output stream
407    Stdout = 0,
408    /// Standard error stream  
409    Stderr = 1,
410}
411impl Default for StreamSource {
412    fn default() -> Self {
413        Self::Stdout
414    }
415}