1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//! **The last stop, kept where it survives the stop** (2026-10-01, the real test2 run 9:
//! vm1 stopped cleanly, but its power-off steps were lost — the narration and the disk log
//! die with the read-only remount of /data — and UpCloud reported `stopped` 46 s after the
//! soft stop, so nobody could tell the guest's power-off from the provider's reporting).
//! After the quiesce and just before the power goes, ONE line is written on the ESP (its
//! own filesystem, flipped read-write for that write); the next boot says it at INFO and
//! removes it.
/// Where the record lives on the ESP.
pub const RECORD: &str = "/boot/gunnar-last-stop.txt";
/// The words the next boot says the record in — what the installer's kit looks for.
pub const SAID: &str = "the previous stop (kept on the ESP):";
/// **The record**: `power_off_epoch_ms=<ms> <the stop's line>` — one line, no newline in it.
pub fn render(power_off_epoch_ms: u128, line: &str) -> String {
format!(
"power_off_epoch_ms={power_off_epoch_ms} {}\n",
line.replace('\n', " ")
)
}
/// The power-off time out of a record (or out of the line the next boot said it in).
pub fn power_off_epoch_ms(text: &str) -> Option<u128> {
text.split("power_off_epoch_ms=")
.nth(1)?
.split_whitespace()
.next()?
.parse()
.ok()
}
/// Write the record for a stop said as `line`, stamped now — the last write before the power.
pub fn keep(line: &str) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
crate::narrate::write_esp_file(std::path::Path::new(RECORD), render(now, line).as_bytes())
}
/// At boot: say the previous stop's record (if any) at INFO, then remove it.
pub fn say_previous() {
let path = std::path::Path::new(RECORD);
if let Ok(text) = std::fs::read_to_string(path) {
tracing::info!("{SAID} {}", text.trim());
crate::narrate::remove_esp_file(path);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// One line, the power-off time first and readable back, the stop's own words after it.
#[test]
fn a_stop_record_is_one_line_whose_power_off_time_reads_back() {
let r = render(1_790_850_910_705, "front: stopped cleanly on ACPI power button — power-off: sync(2) done in 3 ms; 2 writer(s) released;\n/data remounted read-only in 41 ms");
assert_eq!(r.matches('\n').count(), 1, "{r}");
assert!(
r.starts_with("power_off_epoch_ms=1790850910705 front: stopped cleanly"),
"{r}"
);
assert_eq!(power_off_epoch_ms(&r), Some(1_790_850_910_705));
assert_eq!(
power_off_epoch_ms(&format!("INFO {SAID} {r}")),
Some(1_790_850_910_705),
"read back out of the next boot's line too"
);
}
}