tcrm-task 0.4.2

Task execution unit for TCRM project
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
use std::{collections::HashMap, sync::Arc};

use crate::tasks::{error::TaskError, validator::ConfigValidator};

/// Configuration for a task to be executed.
///
/// `TaskConfig` defines all parameters needed to execute a process.
///
/// # Examples
///
/// ## Basic Command
/// ```rust
/// use tcrm_task::tasks::config::TaskConfig;
///
/// let config = TaskConfig::new("cmd")
///     .args(["/C", "dir", "C:\\"]);
/// ```
///
/// ## Complex Configuration
/// ```rust
/// use tcrm_task::tasks::config::{TaskConfig, StreamSource};
/// use std::collections::HashMap;
///
/// let mut env = HashMap::new();
/// env.insert("PATH".to_string(), "C:\\Windows\\System32".to_string());
///
/// let config = TaskConfig::new("cmd")
///     .args(["/C", "echo", "Server started"])
///     .working_dir("C:\\")
///     .env(env)
///     .timeout_ms(30000)
///     .enable_stdin(true)
///     .ready_indicator("Server started")
///     .ready_indicator_source(StreamSource::Stdout);
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct TaskConfig {
    /// The unique identifier for the task
    pub task_id: Option<String>,

    /// The command or executable to run
    pub command: String,

    /// Arguments to pass to the command
    pub args: Option<Vec<String>>,

    /// Working directory for the command
    pub working_dir: Option<String>,

    /// Environment variables for the command
    pub env: Option<HashMap<String, String>>,

    /// Maximum allowed runtime in milliseconds
    pub timeout_ms: Option<u64>,

    /// Allow providing input to the task via stdin
    pub enable_stdin: Option<bool>,

    /// Optional string to indicate the task is ready (for long-running processes like servers)
    pub ready_indicator: Option<String>,

    /// Source of the ready indicator string (stdout/stderr)
    pub ready_indicator_source: Option<StreamSource>,

    /// Enable process group management for child process termination (default: true)
    ///
    /// When enabled, creates process groups (Unix) or Job Objects (Windows) to ensure
    /// all child processes and their descendants are terminated when the main process is killed.
    #[cfg(feature = "process-group")]
    pub use_process_group: Option<bool>,
}

pub type SharedTaskConfig = Arc<TaskConfig>;
impl Default for TaskConfig {
    fn default() -> Self {
        TaskConfig {
            task_id: None,
            command: String::new(),
            args: None,
            working_dir: None,
            env: None,
            timeout_ms: None,
            enable_stdin: Some(false),
            ready_indicator: None,
            ready_indicator_source: Some(StreamSource::Stdout),
            #[cfg(feature = "process-group")]
            use_process_group: Some(true),
        }
    }
}

impl TaskConfig {
    /// Create a new task configuration with the given command
    ///
    /// # Arguments
    ///
    /// * `command` - The executable command to run (e.g., "ls", "node", "python")
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// let config1 = TaskConfig::new("echo");
    /// let config2 = TaskConfig::new("Powershell").args(["-Command", "echo"]);
    /// ```
    pub fn new(command: impl Into<String>) -> Self {
        TaskConfig {
            command: command.into(),
            ..Default::default()
        }
    }
    /// Set the unique identifier for the task
    ///
    /// # Arguments
    ///
    /// * `id` - The unique identifier for the task
    pub fn task_id(mut self, id: impl Into<String>) -> Self {
        self.task_id = Some(id.into());
        self
    }
    /// Set the arguments for the command
    ///
    /// # Arguments
    ///
    /// * `args` - Iterator of arguments to pass to the command
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// let config = TaskConfig::new("ls")
    ///     .args(["-la", "/tmp"]);
    ///     
    /// let config2 = TaskConfig::new("cargo")
    ///     .args(vec!["build", "--release"]);
    /// ```
    #[must_use]
    pub fn args<I, S>(mut self, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.args = Some(args.into_iter().map(Into::into).collect());
        self
    }

    /// Set the working directory for the command
    ///
    /// The working directory must exist when the task is executed.
    ///
    /// # Arguments
    ///
    /// * `dir` - Path to the working directory
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// let config = TaskConfig::new("ls")
    ///     .working_dir("/tmp");
    ///     
    /// let config2 = TaskConfig::new("cargo")
    ///     .working_dir("/path/to/project");
    /// ```
    #[must_use]
    pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
        self.working_dir = Some(dir.into());
        self
    }

    /// Set environment variables for the command
    ///
    /// # Arguments
    ///
    /// * `env` - Iterator of (key, value) pairs for environment variables
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    /// use std::collections::HashMap;
    ///
    /// // Using tuples
    /// let config = TaskConfig::new("node")
    ///     .env([("NODE_ENV", "production"), ("PORT", "3000")]);
    ///
    /// // Using HashMap
    /// let mut env = HashMap::new();
    /// env.insert("RUST_LOG".to_string(), "debug".to_string());
    /// let config2 = TaskConfig::new("cargo")
    ///     .env(env);
    /// ```
    #[must_use]
    pub fn env<K, V, I>(mut self, env: I) -> Self
    where
        K: Into<String>,
        V: Into<String>,
        I: IntoIterator<Item = (K, V)>,
    {
        self.env = Some(env.into_iter().map(|(k, v)| (k.into(), v.into())).collect());
        self
    }

    /// Set the maximum allowed runtime in milliseconds
    ///
    /// If the task runs longer than this timeout, it will be terminated.
    ///
    /// # Arguments
    ///
    /// * `timeout` - Timeout in milliseconds (must be > 0)
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// // 30 second timeout
    /// let config = TaskConfig::new("long-running-task")
    ///     .timeout_ms(30000);
    ///
    /// // 5 minute timeout
    /// let config2 = TaskConfig::new("build-script")
    ///     .timeout_ms(300000);
    /// ```
    #[must_use]
    pub fn timeout_ms(mut self, timeout: u64) -> Self {
        self.timeout_ms = Some(timeout);
        self
    }

    /// Enable or disable stdin for the task
    ///
    /// When enabled, you can send input to the process via the stdin channel.
    ///
    /// # Arguments
    ///
    /// * `b` - Whether to enable stdin input
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// // Interactive command that needs input
    /// let config = TaskConfig::new("python")
    ///     .args(["-i"])
    ///     .enable_stdin(true);
    /// ```
    #[must_use]
    pub fn enable_stdin(mut self, b: bool) -> Self {
        self.enable_stdin = Some(b);
        self
    }

    /// Set the ready indicator for the task
    ///
    /// For long-running processes (like servers), this string indicates when
    /// the process is ready to accept requests. When this string appears in
    /// the process output, a Ready event will be emitted.
    ///
    /// # Arguments
    ///
    /// * `indicator` - String to look for in process output
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// let config = TaskConfig::new("my-server")
    ///     .ready_indicator("Server listening on port");
    ///
    /// let config2 = TaskConfig::new("database")
    ///     .ready_indicator("Database ready for connections");
    /// ```
    #[must_use]
    pub fn ready_indicator(mut self, indicator: impl Into<String>) -> Self {
        self.ready_indicator = Some(indicator.into());
        self
    }

    /// Set the source of the ready indicator
    ///
    /// Specifies whether to look for the ready indicator in stdout or stderr.
    ///
    /// # Arguments
    ///
    /// * `source` - Stream source (Stdout or Stderr)
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::{TaskConfig, StreamSource};
    ///
    /// let config = TaskConfig::new("my-server")
    ///     .ready_indicator("Ready")
    ///     .ready_indicator_source(StreamSource::Stderr);
    /// ```
    #[must_use]
    pub fn ready_indicator_source(mut self, source: StreamSource) -> Self {
        self.ready_indicator_source = Some(source);
        self
    }

    /// Enable or disable process group management
    ///
    /// When enabled (default), creates process groups on Unix or Job Objects on Windows
    /// to ensure all child processes and their descendants are terminated when the main
    /// process is killed. This prevents orphaned processes.
    ///
    /// # Arguments
    ///
    /// * `enabled` - Whether to use process group management
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// // Disable process group management
    /// let config = TaskConfig::new("cmd")
    ///     .use_process_group(false);
    ///     
    /// // Explicitly enable (though it's enabled by default)
    /// let config2 = TaskConfig::new("node")
    ///     .use_process_group(true);
    /// ```
    #[must_use]
    #[cfg(feature = "process-group")]
    pub fn use_process_group(mut self, enabled: bool) -> Self {
        self.use_process_group = Some(enabled);
        self
    }

    /// Validate the configuration
    ///
    /// Validates all configuration parameters.
    ///
    /// # Validation Checks
    /// - all fields length limits
    /// - **Command**: Must not be empty
    /// - **Arguments**: Must not contain null bytes or shell injection patterns  
    /// - **Working Directory**: Must exist and be a valid directory
    /// - **Environment Variables**: Keys must not contain spaces, '=', or null bytes
    /// - **Timeout**: Must be greater than 0 if specified
    /// - **Ready Indicator**: Must not be empty if specified
    ///
    /// # Returns
    ///
    /// - `Ok(())` if the configuration is valid
    /// - `Err(TaskError::InvalidConfiguration)` with details if validation fails
    ///
    /// # Errors
    ///
    /// Returns a [`TaskError`] if any validation check fails:
    /// - [`TaskError::InvalidConfiguration`] for configuration errors
    /// - [`TaskError::IO`] for working directory path not found
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tcrm_task::tasks::config::TaskConfig;
    ///
    /// // Valid config
    /// let config = TaskConfig::new("echo")
    ///     .args(["hello", "world"]);
    /// assert!(config.validate().is_ok());
    ///
    /// // Invalid config (empty command)
    /// let config = TaskConfig::new("");
    /// assert!(config.validate().is_err());
    ///
    /// // Invalid config (zero timeout)
    /// let config = TaskConfig::new("sleep")
    ///     .timeout_ms(0);
    /// assert!(config.validate().is_err());
    /// ```
    pub fn validate(&self) -> Result<(), TaskError> {
        ConfigValidator::validate_command(&self.command)?;
        if let Some(ready_indicator) = &self.ready_indicator {
            ConfigValidator::validate_ready_indicator(ready_indicator)?;
        }
        if let Some(args) = &self.args {
            ConfigValidator::validate_args(args)?;
        }
        if let Some(dir) = &self.working_dir {
            ConfigValidator::validate_working_dir(dir)?;
        }
        if let Some(env) = &self.env {
            ConfigValidator::validate_env_vars(env)?;
        }
        if let Some(timeout) = &self.timeout_ms {
            ConfigValidator::validate_timeout(timeout)?;
        }
        Ok(())
    }
}

/// Specifies the source stream for output monitoring
///
/// Used with ready indicators to specify whether to monitor stdout or stderr
/// for the ready signal from long-running processes.
///
/// # Examples
///
/// ```rust
/// use tcrm_task::tasks::config::{TaskConfig, StreamSource};
///
/// // Monitor stdout for ready signal
/// let config = TaskConfig::new("web-server")
///     .ready_indicator("Server ready")
///     .ready_indicator_source(StreamSource::Stdout);
///
/// // Monitor stderr for ready signal  
/// let config2 = TaskConfig::new("database")
///     .ready_indicator("Ready for connections")
///     .ready_indicator_source(StreamSource::Stderr);
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
#[derive(Debug, Clone, PartialEq)]
pub enum StreamSource {
    /// Standard output stream
    Stdout = 0,
    /// Standard error stream  
    Stderr = 1,
}
impl Default for StreamSource {
    fn default() -> Self {
        Self::Stdout
    }
}