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