use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
pub struct HwWatchdog {
file: File,
disarm_on_drop: AtomicBool,
}
impl HwWatchdog {
pub fn open(path: &Path) -> std::io::Result<Self> {
let file = OpenOptions::new().write(true).open(path)?;
Ok(Self {
file,
disarm_on_drop: AtomicBool::new(false),
})
}
pub fn kick(&mut self) {
let _ = self.file.write_all(&[0u8]);
}
pub fn arm_disarm_on_drop(&self) {
self.disarm_on_drop.store(true, Ordering::Release);
}
}
impl Drop for HwWatchdog {
fn drop(&mut self) {
if self.disarm_on_drop.load(Ordering::Acquire) {
let _ = self.file.write_all(b"V");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_path(tag: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("varta-hw-wdt-{}-{}", std::process::id(), tag));
let _ = std::fs::remove_file(&p);
p
}
#[test]
fn kick_writes_byte_to_device() {
let path = tmp_path("kick");
std::fs::write(&path, b"").unwrap();
let mut w = HwWatchdog::open(&path).expect("open");
w.kick();
drop(w); let contents = std::fs::read(&path).unwrap();
assert_eq!(contents, &[0u8], "kick must write NUL byte");
let _ = std::fs::remove_file(&path);
}
#[test]
fn magic_close_writes_v_on_clean_shutdown() {
let path = tmp_path("magic");
std::fs::write(&path, b"").unwrap();
let mut w = HwWatchdog::open(&path).expect("open");
w.kick();
w.arm_disarm_on_drop(); drop(w); let contents = std::fs::read(&path).unwrap();
assert_eq!(
contents.last().copied(),
Some(b'V'),
"clean shutdown must write magic-close byte V"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn no_magic_close_without_arm() {
let path = tmp_path("nomagic");
std::fs::write(&path, b"").unwrap();
let mut w = HwWatchdog::open(&path).expect("open");
w.kick();
drop(w);
let contents = std::fs::read(&path).unwrap();
assert_ne!(
contents.last().copied(),
Some(b'V'),
"crash path must not write magic-close byte"
);
let _ = std::fs::remove_file(&path);
}
}