use std::time::Duration;
use serde::{Deserialize, Serialize};
pub const DEFAULT_SHELL_TIMEOUT_SECS: u32 = 120;
pub const DEFAULT_SHELL_TIMEOUT_MAX_SECS: u32 = 600;
pub const LONGEST_SHELL_TIMEOUT_SECS: u32 = 86_400;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ShellTimeoutsError {
#[error("timeouts must be whole seconds from 1 to {LONGEST_SHELL_TIMEOUT_SECS}")]
OutOfRange,
#[error("the default timeout ({default} s) must not exceed the maximum ({max} s)")]
DefaultAboveMax { default: u32, max: u32 },
}
#[derive(Serialize, Deserialize)]
struct RawShellTimeouts {
default_secs: u32,
max_secs: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RawShellTimeouts", into = "RawShellTimeouts")]
pub struct ShellTimeouts {
default_secs: u32,
max_secs: u32,
}
impl ShellTimeouts {
pub const DEFAULT: Self = Self {
default_secs: DEFAULT_SHELL_TIMEOUT_SECS,
max_secs: DEFAULT_SHELL_TIMEOUT_MAX_SECS,
};
pub fn new(default_secs: u32, max_secs: u32) -> Result<Self, ShellTimeoutsError> {
let range = 1..=LONGEST_SHELL_TIMEOUT_SECS;
if !range.contains(&default_secs) || !range.contains(&max_secs) {
Err(ShellTimeoutsError::OutOfRange)
} else if default_secs > max_secs {
Err(ShellTimeoutsError::DefaultAboveMax {
default: default_secs,
max: max_secs,
})
} else {
Ok(Self {
default_secs,
max_secs,
})
}
}
#[must_use]
pub fn effective(self, requested_secs: Option<u64>) -> Duration {
let secs = requested_secs
.unwrap_or(self.default_secs.into())
.clamp(1, self.max_secs.into());
Duration::from_secs(secs)
}
#[must_use]
pub fn max(self) -> Duration {
Duration::from_secs(self.max_secs.into())
}
}
impl Default for ShellTimeouts {
fn default() -> Self {
Self::DEFAULT
}
}
impl TryFrom<RawShellTimeouts> for ShellTimeouts {
type Error = ShellTimeoutsError;
fn try_from(raw: RawShellTimeouts) -> Result<Self, Self::Error> {
Self::new(raw.default_secs, raw.max_secs)
}
}
impl From<ShellTimeouts> for RawShellTimeouts {
fn from(timeouts: ShellTimeouts) -> Self {
Self {
default_secs: timeouts.default_secs,
max_secs: timeouts.max_secs,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShellRequest {
pub command: String,
pub timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ShellOutcome {
Exited {
code: i32,
},
Signaled {
signal: i32,
},
Cancelled,
TimedOut {
after_secs: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShellReply {
pub outcome: ShellOutcome,
pub duration_ms: u64,
pub stdout: String,
pub stderr: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetCwdRequest {
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetCwdReply {
pub cwd: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_timeouts_are_held_between_one_second_and_the_maximum() {
let timeouts = ShellTimeouts::new(120, 600).unwrap();
let secs = |requested| timeouts.effective(requested).as_secs();
assert_eq!(secs(None), 120);
assert_eq!(secs(Some(30)), 30);
assert_eq!(secs(Some(0)), 1);
assert_eq!(secs(Some(100_000)), 600);
}
#[test]
fn timeout_settings_must_be_in_range_and_the_default_within_the_maximum() {
assert!(ShellTimeouts::new(600, 600).is_ok());
assert_eq!(
ShellTimeouts::new(0, 600),
Err(ShellTimeoutsError::OutOfRange)
);
assert_eq!(
ShellTimeouts::new(1, LONGEST_SHELL_TIMEOUT_SECS + 1),
Err(ShellTimeoutsError::OutOfRange)
);
assert_eq!(
ShellTimeouts::new(601, 600),
Err(ShellTimeoutsError::DefaultAboveMax {
default: 601,
max: 600
})
);
let bad = serde_json::json!({ "default_secs": 700, "max_secs": 600 });
assert!(serde_json::from_value::<ShellTimeouts>(bad).is_err());
}
}