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//! --on-kill-resume-delay <s>  wait a random <min>[-<max>] seconds before
9//!                             each resume (issue #181)
10//! ```
11
12use crate::args_parser::WrapperOptions;
13use crate::recovery_delay::parse_on_kill_resume_delay_value;
14
15fn parse_on_kill_resume_value(value: &str) -> Result<u32, String> {
16    let text = value.trim();
17    match text.parse::<u32>() {
18        Ok(count) if count >= 1 && text.chars().all(|c| c.is_ascii_digit()) => Ok(count),
19        _ => Err(format!(
20            "Invalid --on-kill-resume value: \"{}\". Expected a positive integer.",
21            value
22        )),
23    }
24}
25
26/// Parse one recovery option.
27///
28/// Returns the number of arguments consumed, or `Ok(0)` when `args[index]` is
29/// not a recovery option.
30pub fn parse_docker_recovery_option(
31    args: &[String],
32    index: usize,
33    options: &mut WrapperOptions,
34) -> Result<usize, String> {
35    let arg = args[index].as_str();
36    if arg == "--on-kill-resume" || arg == "--recovery-command" || arg == "--on-kill-resume-delay" {
37        let Some(value) = args.get(index + 1).filter(|value| !value.starts_with('-')) else {
38            let kind = match arg {
39                "--on-kill-resume" => "count",
40                "--recovery-command" => "command",
41                _ => "seconds",
42            };
43            return Err(format!("Option {} requires a {} argument", arg, kind));
44        };
45        match arg {
46            "--on-kill-resume" => options.on_kill_resume = Some(parse_on_kill_resume_value(value)?),
47            "--recovery-command" => options.recovery_command = Some(value.clone()),
48            _ => options.on_kill_resume_delay = Some(parse_on_kill_resume_delay_value(value)?),
49        }
50        return Ok(2);
51    }
52    if let Some(value) = arg.strip_prefix("--on-kill-resume-delay=") {
53        options.on_kill_resume_delay = Some(parse_on_kill_resume_delay_value(value)?);
54        return Ok(1);
55    }
56    if let Some(value) = arg.strip_prefix("--on-kill-resume=") {
57        options.on_kill_resume = Some(parse_on_kill_resume_value(value)?);
58        return Ok(1);
59    }
60    if let Some(value) = arg.strip_prefix("--recovery-command=") {
61        options.recovery_command = Some(value.to_string());
62        return Ok(1);
63    }
64    Ok(0)
65}
66
67/// Recovery is driven by the detached docker completion watcher, so it needs a
68/// detached docker session. A recovery command on its own implies one attempt;
69/// a delay without either has nothing to delay.
70pub fn validate_docker_recovery_options(options: &mut WrapperOptions) -> Result<(), String> {
71    if let Some(command) = options.recovery_command.as_deref() {
72        if command.trim().is_empty() {
73            return Err("--recovery-command requires a non-empty command".to_string());
74        }
75        options.on_kill_resume.get_or_insert(1);
76    }
77    if options.on_kill_resume.is_none() {
78        if options.on_kill_resume_delay.is_some() {
79            return Err(
80                "--on-kill-resume-delay requires --on-kill-resume or --recovery-command"
81                    .to_string(),
82            );
83        }
84        return Ok(());
85    }
86    let flag = if options.recovery_command.is_some() {
87        "--recovery-command"
88    } else {
89        "--on-kill-resume"
90    };
91    if options.isolated.as_deref() != Some("docker") {
92        return Err(format!(
93            "{} option is only valid with --isolated docker as the only isolation level",
94            flag
95        ));
96    }
97    if !options.detached {
98        return Err(format!("{} option requires --detached", flag));
99    }
100    Ok(())
101}