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}