Skip to main content

solti_exec/subprocess/
task.rs

1//! # Task: resolved subprocess configuration.
2//!
3//! [`SubprocessTaskConfig`] is the **fully resolved** config passed to the subprocess spawn loop.
4//! It is produced by [`SubprocessRunner::build_task_config`] from a [`TaskSpec`](solti_model::TaskSpec) + [`BuildContext`](solti_runner::BuildContext).
5//!
6//! ## How it fits
7//! ```text
8//! TaskSpec (model)
9//!     │
10//!     ├──► SubprocessRunner::build_task_config()
11//!     │     ├──► resolve SubprocessMode → (command, args)
12//!     │     ├──► merge_env(task_env, runner_env)
13//!     │     └──► generate run_id
14//!     │
15//!     └──► SubprocessTaskConfig (this struct)
16//!           ├──► validate(): command not empty
17//!           └──► passed to run_subprocess() for execution
18//! ```
19//!
20//! ## Fields
21//!
22//! | Field              | Source                    | Description                          |
23//! |--------------------|---------------------------|--------------------------------------|
24//! | `run_id`           | generated by runner       | unique execution identifier          |
25//! | `command`          | `SubprocessMode`          | OS command to exec                   |
26//! | `args`             | `SubprocessMode`          | command-line arguments               |
27//! | `env`              | `merge_env(task, runner)` | merged environment variables         |
28//! | `cwd`              | `SubprocessSpec`          | working directory (`None` = inherit) |
29//! | `fail_on_non_zero` | `SubprocessSpec`          | treat non-zero exit as failure       |
30
31use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
32
33use solti_model::Flag;
34
35use crate::ExecError;
36
37/// Subprocess configuration - fully resolved per-task parameters.
38///
39/// ## Also
40///
41/// - [`SubprocessRunner`](super::SubprocessRunner) produces this config in `build_task`.
42/// - [`SubprocessBackendConfig`](super::SubprocessBackendConfig) runner-level settings applied at spawn.
43#[derive(Debug, Clone)]
44pub struct SubprocessTaskConfig {
45    /// End-to-End log identifier.
46    pub(crate) run_id: Arc<str>,
47    /// Raw sequence number from run id generation (used for cgroup naming).
48    pub(crate) seq: u64,
49    /// Command to execute (e.g. `"ls"`, `"/usr/bin/python"`).
50    pub(crate) command: String,
51    /// Command-line arguments passed to the command.
52    pub(crate) args: Vec<String>,
53    /// Merged environment (runner overrides task). Ready for `Command::envs()`.
54    pub(crate) env: BTreeMap<String, String>,
55    /// Working directory for the subprocess.
56    ///
57    /// If `None`, the subprocess inherits the parent process working directory.
58    pub(crate) cwd: Option<PathBuf>,
59    /// Whether non-zero exit codes should be treated as task failures.
60    pub(crate) fail_on_non_zero: Flag,
61}
62
63impl SubprocessTaskConfig {
64    /// Validate the configuration before spawning a subprocess.
65    ///
66    /// Rules:
67    /// - `command` is not empty or whitespace-only.
68    pub fn validate(&self) -> Result<(), ExecError> {
69        if self.command.trim().is_empty() {
70            return Err(ExecError::InvalidSpec("Subprocess command is empty".into()));
71        }
72        Ok(())
73    }
74}
75
76impl fmt::Display for SubprocessTaskConfig {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(
79            f,
80            "SubprocessTaskConfig(cmd='{}', args={}, env={}, cwd={:?}, fail_on_non_zero={})",
81            self.command,
82            self.args.len(),
83            self.env.len(),
84            self.cwd,
85            self.fail_on_non_zero.is_enabled(),
86        )
87    }
88}