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}