planarian_recovery/
main.rs1use 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 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
86fn 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}