Skip to main content

start_command/
docker_recovery_options.rs

1//! Parse and validate the launch-time recovery options (issue #176):
2//!
3//! ```text
4//! --on-kill-resume <N>        resume up to N times when the main process is
5//!                             killed (exit 137 / OOMKilled)
6//! --recovery-command <cmd>    command to run in the same container on resume
7//!                             (without it, the original command is re-run)
8//! ```
9
10use crate::args_parser::WrapperOptions;
11
12fn parse_on_kill_resume_value(value: &str) -> Result<u32, String> {
13    let text = value.trim();
14    match text.parse::<u32>() {
15        Ok(count) if count >= 1 && text.chars().all(|c| c.is_ascii_digit()) => Ok(count),
16        _ => Err(format!(
17            "Invalid --on-kill-resume value: \"{}\". Expected a positive integer.",
18            value
19        )),
20    }
21}
22
23/// Parse one recovery option.
24///
25/// Returns the number of arguments consumed, or `Ok(0)` when `args[index]` is
26/// not a recovery option.
27pub fn parse_docker_recovery_option(
28    args: &[String],
29    index: usize,
30    options: &mut WrapperOptions,
31) -> Result<usize, String> {
32    let arg = args[index].as_str();
33    if arg == "--on-kill-resume" || arg == "--recovery-command" {
34        let Some(value) = args.get(index + 1).filter(|value| !value.starts_with('-')) else {
35            let kind = if arg == "--on-kill-resume" {
36                "count"
37            } else {
38                "command"
39            };
40            return Err(format!("Option {} requires a {} argument", arg, kind));
41        };
42        if arg == "--on-kill-resume" {
43            options.on_kill_resume = Some(parse_on_kill_resume_value(value)?);
44        } else {
45            options.recovery_command = Some(value.clone());
46        }
47        return Ok(2);
48    }
49    if let Some(value) = arg.strip_prefix("--on-kill-resume=") {
50        options.on_kill_resume = Some(parse_on_kill_resume_value(value)?);
51        return Ok(1);
52    }
53    if let Some(value) = arg.strip_prefix("--recovery-command=") {
54        options.recovery_command = Some(value.to_string());
55        return Ok(1);
56    }
57    Ok(0)
58}
59
60/// Recovery is driven by the detached docker completion watcher, so it needs a
61/// detached docker session. A recovery command on its own implies one attempt.
62pub fn validate_docker_recovery_options(options: &mut WrapperOptions) -> Result<(), String> {
63    if let Some(command) = options.recovery_command.as_deref() {
64        if command.trim().is_empty() {
65            return Err("--recovery-command requires a non-empty command".to_string());
66        }
67        options.on_kill_resume.get_or_insert(1);
68    }
69    if options.on_kill_resume.is_none() {
70        return Ok(());
71    }
72    let flag = if options.recovery_command.is_some() {
73        "--recovery-command"
74    } else {
75        "--on-kill-resume"
76    };
77    if options.isolated.as_deref() != Some("docker") {
78        return Err(format!(
79            "{} option is only valid with --isolated docker as the only isolation level",
80            flag
81        ));
82    }
83    if !options.detached {
84        return Err(format!("{} option requires --detached", flag));
85    }
86    Ok(())
87}