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}