Skip to main content

planarian_recovery/
main.rs

1//! planarian-recovery (§12 recovery watchdog). A separate unit with
2//! `Restart=always`, started before planariad and independent of it. Every
3//! 10 s it reads the journal. A transaction in Activating, Checking,
4//! Published, Promoted or Reverting whose owner is dead, or whose lease went
5//! unrenewed past its deadline, is taken over: fence, confirm quiescence, then
6//! run the crash-recovery table. From Checking that means the entire
7//! authorized commit path, or a revert; never a partial path. When the
8//! daemon is alive and renewing, the watchdog does nothing.
9
10use planarian_backend_linux::{Paths, Real};
11use planarian_daemon::config::{CONFIG, FileConfig, load_canonical};
12use planarian_exec::host::ExecHost;
13use planarian_schema::{Hash, OwnerId};
14use planarian_txn::journal::{Journal, JournalError};
15use planarian_txn::kernel::Kernel;
16use planarian_txn::store::FileStore;
17use std::path::Path;
18use std::time::Duration;
19
20const PERIOD: Duration = Duration::from_secs(10);
21
22fn main() {
23    let cfg_path = std::env::var("PLANARIAN_CONFIG").unwrap_or_else(|_| CONFIG.into());
24    let file = FileConfig::load(Path::new(&cfg_path)).unwrap_or_else(|e| {
25        eprintln!("planarian-recovery: {e}; using defaults");
26        FileConfig::default()
27    });
28    let canonical = load_canonical().unwrap_or_else(|e| {
29        eprintln!("planarian-recovery: canonical probes: {e}");
30        Default::default()
31    });
32    let paths = Paths {
33        state: file.state_dir.clone(),
34        owner: file.owner.clone(),
35        flake_attr: file.flake_attr.clone(),
36        ..Default::default()
37    };
38    let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id").unwrap_or_default();
39    let me = OwnerId::Watchdog { pid: std::process::id(), boot_id: Hash::of(boot.trim().as_bytes()) };
40    core_hash_check();
41    eprintln!("planarian-recovery: watching {}", paths.journal_dir().display());
42    loop {
43        if let Err(e) = pass(&paths, &file, &canonical, &me) {
44            eprintln!("planarian-recovery: {e}");
45        }
46        std::thread::sleep(PERIOD);
47    }
48}
49
50fn pass(
51    paths: &Paths,
52    file: &FileConfig,
53    canonical: &std::collections::BTreeMap<String, Vec<planarian_schema::ProbeDef>>,
54    me: &OwnerId,
55) -> Result<(), String> {
56    let store = FileStore::open(&paths.journal_dir(), &paths.txn_dir(), file.journal_bytes)
57        .map_err(|e| format!("journal: {e}"))?;
58    let journal = match Journal::open(store) {
59        Ok(j) => j,
60        // planarian-recovery's independent check (§12d): report and stop with
61        // the Integrity status; the posture units respond.
62        Err(JournalError::Integrity(why)) => {
63            eprintln!("planarian-recovery: INTEGRITY: the journal's hash chain is broken: {why}");
64            let _ = std::fs::create_dir_all("/run/planarian");
65            let _ = std::fs::write("/run/planarian/integrity", format!("journal: {why}\n"));
66            std::process::exit(78);
67        }
68        Err(e) => return Err(format!("journal: {e:?}")),
69    };
70    let real = Real::new(paths.clone(), file.devices.clone());
71    let mut k = Kernel::new(journal, ExecHost::new(real, canonical.clone()), me.clone());
72    let live: Vec<_> = k.journal.ledger().live.values().map(|l| l.txn.clone()).collect();
73    for txn in live {
74        if !k.watchdog_should_take_over(&txn) {
75            continue;
76        }
77        eprintln!("planarian-recovery: taking over transaction {} (found in {:?})", txn.id.0, txn.state);
78        match k.recover(txn.id, txn.lease.epoch) {
79            Ok(end) => eprintln!("planarian-recovery: transaction {} → {end:?}", txn.id.0),
80            Err(e) => eprintln!("planarian-recovery: transaction {}: {e:?}", txn.id.0),
81        }
82    }
83    Ok(())
84}
85
86/// I2 in Phase A-D (§12d's independent check, B-12): every Planarian
87/// binary and script the generation installs hashes to what was recorded at
88/// build (`<sha256> <path>` per line). A mismatch, a missing file or a
89/// record that is not in that form is an Integrity failure: report it and
90/// stop with status 78; the posture units respond.
91fn core_hash_check() {
92    let Ok(record) = std::env::var("PLANARIAN_CORE_HASH") else { return };
93    let text = std::fs::read_to_string(&record).unwrap_or_default();
94    let mut problems = vec![];
95    let mut checked = 0;
96    for line in text.lines().filter(|l| !l.trim().is_empty()) {
97        let Some((want, path)) = line.split_once(' ') else {
98            problems.push(format!("{record}: malformed line {line:?}"));
99            continue;
100        };
101        let have = std::fs::read(path).map(|b| Hash::of(&b).to_hex()).unwrap_or_default();
102        if have != want {
103            problems.push(format!("{path} hashes to {have:?}, but {record} records {want:?}"));
104        }
105        checked += 1;
106    }
107    if checked == 0 && problems.is_empty() {
108        problems.push(format!("{record} records nothing"));
109    }
110    if !problems.is_empty() {
111        let why = format!("core: {}", problems.join("; "));
112        eprintln!("planarian-recovery: INTEGRITY: {why}");
113        let _ = std::fs::create_dir_all("/run/planarian");
114        let _ = std::fs::write("/run/planarian/integrity", format!("{why}\n"));
115        std::process::exit(78);
116    }
117}