use super::{command_mechanism, ActionSpec};
use sysknife_types::RiskLevel;
const REBOOT_REQUIRED_FILE: &str = "/var/run/reboot-required";
const REBOOT_REQUIRED_PKGS_FILE: &str = "/var/run/reboot-required.pkgs";
pub fn specs() -> Vec<ActionSpec> {
vec![check_pending_reboot()]
}
pub fn check_pending_reboot() -> ActionSpec {
let script = format!(
"if test -f {sentinel}; then cat {sentinel}; cat {pkgs} 2>/dev/null; true; else echo 'No reboot required.'; fi",
sentinel = REBOOT_REQUIRED_FILE,
pkgs = REBOOT_REQUIRED_PKGS_FILE,
);
ActionSpec {
action_name: "CheckPendingReboot",
mechanism: command_mechanism("bash", ["-c", &script]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::ActionMechanism;
fn extract_cmd(spec: &ActionSpec) -> (&'static str, Vec<String>) {
match &spec.mechanism {
ActionMechanism::Command { program, args } => (*program, args.clone()),
_ => panic!("expected Command mechanism"),
}
}
#[test]
fn check_pending_reboot_action_name() {
assert_eq!(check_pending_reboot().action_name, "CheckPendingReboot");
}
#[test]
fn check_pending_reboot_uses_bash() {
let spec = check_pending_reboot();
let (prog, _) = extract_cmd(&spec);
assert_eq!(prog, "bash");
}
#[test]
fn check_pending_reboot_script_references_sentinel_file() {
let spec = check_pending_reboot();
let (_, args) = extract_cmd(&spec);
let joined = args.join(" ");
assert!(
joined.contains(REBOOT_REQUIRED_FILE),
"missing sentinel path in script: {joined}"
);
}
#[test]
fn check_pending_reboot_script_references_pkgs_file() {
let spec = check_pending_reboot();
let (_, args) = extract_cmd(&spec);
let joined = args.join(" ");
assert!(
joined.contains(REBOOT_REQUIRED_PKGS_FILE),
"missing pkgs path in script: {joined}"
);
}
#[test]
fn check_pending_reboot_script_uses_ubuntu_dot_pkgs_filename() {
let spec = check_pending_reboot();
let (_, args) = extract_cmd(&spec);
let joined = args.join(" ");
assert!(
joined.contains("/var/run/reboot-required.pkgs"),
"Ubuntu writes reboot-required.pkgs, not the hyphen form: {joined}"
);
assert!(
!joined.contains("reboot-required-pkgs"),
"hyphen form is not a file Ubuntu writes: {joined}"
);
}
fn run_pending_reboot_script(sentinel: Option<&str>, pkgs: Option<&str>) -> (i32, String) {
let dir = tempfile::tempdir().unwrap();
let sentinel_path = dir.path().join("reboot-required");
let pkgs_path = dir.path().join("reboot-required.pkgs");
if let Some(body) = sentinel {
std::fs::write(&sentinel_path, body).unwrap();
}
if let Some(body) = pkgs {
std::fs::write(&pkgs_path, body).unwrap();
}
let spec = check_pending_reboot();
let (_, args) = extract_cmd(&spec);
assert_eq!(args.first().map(String::as_str), Some("-c"));
let script = args[1]
.replace(REBOOT_REQUIRED_PKGS_FILE, &pkgs_path.to_string_lossy())
.replace(REBOOT_REQUIRED_FILE, &sentinel_path.to_string_lossy());
let output = std::process::Command::new("bash")
.args(["-c", &script])
.output()
.expect("bash");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
)
}
#[test]
fn pending_reboot_without_sentinel_reports_no_reboot() {
let (code, stdout) = run_pending_reboot_script(None, None);
assert_eq!(code, 0, "stdout={stdout:?}");
assert_eq!(stdout.trim(), "No reboot required.");
}
#[test]
fn pending_reboot_with_sentinel_and_pkgs_lists_both() {
let (code, stdout) = run_pending_reboot_script(
Some("*** System restart required ***\n"),
Some("linux-image-6.8.0-40-generic\n"),
);
assert_eq!(code, 0, "stdout={stdout:?}");
assert!(
stdout.contains("*** System restart required ***"),
"stdout={stdout:?}"
);
assert!(
stdout.contains("linux-image-6.8.0-40-generic"),
"stdout={stdout:?}"
);
}
#[test]
fn pending_reboot_with_sentinel_only_is_not_an_error() {
let (code, stdout) =
run_pending_reboot_script(Some("*** System restart required ***\n"), None);
assert_eq!(
code, 0,
"a missing packages file must not fail the action; stdout={stdout:?}"
);
assert!(
stdout.contains("*** System restart required ***"),
"stdout={stdout:?}"
);
}
#[test]
fn check_pending_reboot_risk_is_low() {
assert_eq!(check_pending_reboot().risk_level, RiskLevel::Low);
}
#[test]
fn check_pending_reboot_no_reboot_no_rollback() {
let spec = check_pending_reboot();
assert!(!spec.reboot_required);
assert!(!spec.rollback_available);
}
#[test]
fn specs_covers_check_pending_reboot() {
let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
assert!(
spec_names.contains(&"CheckPendingReboot"),
"specs() missing CheckPendingReboot"
);
}
}