Skip to main content

start_command/
execution_env.rs

1//! START_* variables that tell a launched command which execution it belongs
2//! to (issue #230). Every backend exports the same three variables, next to
3//! the kill-recovery `START_COMMAND_RECOVERY_ATTEMPT`:
4//!
5//! - START_EXECUTION_ID: the execution UUID (`--session-id`)
6//! - START_SESSION_NAME: the isolation session/container name (`--session`);
7//!   empty for direct runs, which have no session
8//! - START_ATTEMPT: 1 for the first run, n for the n-th attempt (`--resume`,
9//!   kill recovery or `--append-attempt`), the same number `--status` shows
10use crate::isolation::isolation_log::shell_quote;
11
12pub const EXECUTION_ID_ENV: &str = "START_EXECUTION_ID";
13pub const SESSION_NAME_ENV: &str = "START_SESSION_NAME";
14pub const ATTEMPT_ENV: &str = "START_ATTEMPT";
15
16/// Variables of one attempt, in a stable order; empty without an execution id.
17pub type ExecutionEnv = Vec<(String, String)>;
18
19/// Build the START_* variables for one attempt.
20pub fn build_execution_env(
21    execution_id: Option<&str>,
22    session_name: Option<&str>,
23    attempt: u64,
24) -> ExecutionEnv {
25    let Some(execution_id) = execution_id.filter(|id| !id.is_empty()) else {
26        return Vec::new();
27    };
28    vec![
29        (EXECUTION_ID_ENV.into(), execution_id.into()),
30        (SESSION_NAME_ENV.into(), session_name.unwrap_or("").into()),
31        (ATTEMPT_ENV.into(), attempt.max(1).to_string()),
32    ]
33}
34
35/// Variables of an isolated launch: its execution UUID, the session or
36/// container name and the attempt number.
37pub fn execution_env_from_options(options: &crate::isolation::IsolationOptions) -> ExecutionEnv {
38    build_execution_env(
39        options.uuid.as_deref().or(options.execution_id.as_deref()),
40        options.session.as_deref(),
41        options.attempt_number,
42    )
43}
44
45/// `KEY=value` assignments, e.g. for `docker run -e`.
46pub fn execution_env_assignments(env: &ExecutionEnv) -> Vec<String> {
47    env.iter()
48        .map(|(key, value)| format!("{}={}", key, value))
49        .collect()
50}
51
52/// POSIX `export` statement for the variables, without a trailing separator.
53pub fn build_execution_env_export(env: &ExecutionEnv) -> String {
54    if env.is_empty() {
55        return String::new();
56    }
57    let assignments: Vec<String> = env
58        .iter()
59        .map(|(key, value)| format!("{}={}", key, shell_quote(value)))
60        .collect();
61    format!("export {}", assignments.join(" "))
62}
63
64/// Prefix a shell command with the export statement. Used by the backends
65/// whose command runs through a shell that does not inherit our environment:
66/// a running tmux server, ssh, and `sudo` (which resets the environment).
67pub fn prefix_with_execution_env(command: &str, env: &ExecutionEnv) -> String {
68    let statement = build_execution_env_export(env);
69    if statement.is_empty() {
70        command.to_string()
71    } else {
72        format!("{}; {}", statement, command)
73    }
74}
75
76/// True when this process was itself started by execution `execution_id`,
77/// i.e. it is the next level of a stacked isolation that passes
78/// `--session-id` down.
79pub fn is_nested_launch_of(execution_id: &str, env_execution_id: Option<&str>) -> bool {
80    !execution_id.is_empty() && env_execution_id == Some(execution_id)
81}
82
83/// `START_ATTEMPT` inherited by a nested level, when it is a positive number.
84pub fn inherited_attempt(value: Option<&str>) -> u64 {
85    value
86        .and_then(|v| v.trim().parse::<u64>().ok())
87        .filter(|n| *n > 0)
88        .unwrap_or(1)
89}