1use planarian_schema::spec::DEFAULT_SETTLE;
8use planarian_schema::*;
9use planarian_state::{Assembler, Canonical, Context};
10use planarian_txn::kernel::{CheckPhase, Host, Verdict};
11use std::collections::BTreeSet;
12use std::sync::{Arc, Mutex};
13
14pub const CHECK_PATIENCE: Millis = Millis::secs(30);
16pub const CHECK_POLL: Millis = Millis::secs(2);
17
18pub type Protect = Arc<Mutex<Vec<GenerationId>>>;
20
21pub struct ExecHost<B: Backends> {
22 pub b: B,
23 pub canonical: Canonical,
24 pub protect: Protect,
25 pub ctx: Arc<Mutex<Context>>,
27}
28
29impl<B: Backends + Clone> Clone for ExecHost<B> {
30 fn clone(&self) -> Self {
31 ExecHost {
32 b: self.b.clone(),
33 canonical: self.canonical.clone(),
34 protect: self.protect.clone(),
35 ctx: self.ctx.clone(),
36 }
37 }
38}
39
40impl<B: Backends> ExecHost<B> {
41 pub fn new(b: B, canonical: Canonical) -> Self {
42 ExecHost { b, canonical, protect: Arc::new(Mutex::new(vec![])), ctx: Arc::new(Mutex::new(Context::default())) }
43 }
44
45 fn ctx(&self) -> Context {
46 self.ctx.lock().unwrap_or_else(|e| e.into_inner()).clone()
47 }
48
49 fn settle(&self, settle: &Settle) {
50 match settle {
51 Settle::Duration(d) => self.b.clock().sleep(*d),
52 Settle::Ready { unit, timeout } => {
53 let deadline = self.b.clock().now().plus(*timeout);
54 while self.b.clock().now() < deadline {
55 if matches!(self.b.units().observe(unit), Ok(Observed::Active)) {
56 break;
57 }
58 self.b.clock().sleep(Millis::secs(1));
59 }
60 }
61 }
62 }
63
64 fn observe(&self, manifest: &Manifest) -> (SystemState, UnitDefs) {
66 let mut a = Assembler::new(manifest.clone(), self.canonical.clone());
67 let s = a.observe(&self.b, &self.ctx());
68 (s, a.defs())
69 }
70
71 fn violations(&self, txn: &Transaction, phase: CheckPhase, s: &SystemState, defs: &UnitDefs) -> Vec<String> {
73 let mut out = vec![];
74 let want_gen = match phase {
75 CheckPhase::Candidate => txn.candidate.as_ref().map(|(g, _)| *g),
76 CheckPhase::Revert => txn.candidate.as_ref().map(|_| txn.revert_to),
77 };
78 if let Some(g) = want_gen
79 && s.generation != g
80 {
81 out.push(format!("running generation is {} (expected {})", s.generation.0, g.0));
82 }
83 if phase == CheckPhase::Candidate {
84 for (u, want) in &txn.expected.units {
85 if !defs.contains_key(u) {
87 continue;
88 }
89 let got = s.status(u, defs);
90 if &got != want {
91 out.push(format!("{u}: expected {want:?}, observed {got:?}"));
92 } else if *want == UnitStatus::Bound {
93 let failing = s.failing_readiness(u, defs);
96 if !failing.is_empty() {
97 out.push(format!(
98 "{u}: bound, but {} failing",
99 failing.iter().map(|p| p.0.as_str()).collect::<Vec<_>>().join(", ")
100 ));
101 }
102 }
103 }
104 }
105 for (u, before) in &txn.check.pre {
111 if *before != UnitStatus::Running || !defs.contains_key(u) || !s.expected(u, defs) {
112 continue;
113 }
114 let now = s.status(u, defs);
115 if now == UnitStatus::Running {
116 continue;
117 }
118 if phase == CheckPhase::Revert && matches!(now, UnitStatus::Degraded(_)) && space_only(u, s, defs) {
123 continue;
124 }
125 out.push(format!("{u} was Running before and is now {now:?}"));
126 }
127 for (u, passing) in &txn.check.pre_passing {
131 if !defs.contains_key(u) || !s.expected(u, defs) {
132 continue;
133 }
134 let failing = s.failing_readiness(u, defs);
135 let lost: Vec<&str> = passing.iter().filter(|p| failing.contains(p)).map(|p| p.0.as_str()).collect();
136 if lost.is_empty() || (phase == CheckPhase::Revert && space_only(u, s, defs)) {
137 continue;
138 }
139 out.push(format!("{u}: {} passed before and fails now", lost.join(", ")));
140 }
141 out
142 }
143}
144
145impl<B: Backends> Host for ExecHost<B> {
146 type Gen = B::Gens;
147 type Inv = B::Inv;
148
149 fn floor_fixes(&self, txn: &Transaction, floor: Option<GenerationId>) -> Result<Vec<String>, Outcome> {
150 let gens = self.b.generations();
151 let Some((cand, _)) = txn.candidate.clone() else { return Ok(vec![]) };
152 let eval = |g: GenerationId| gens.path_of(g).and_then(|p| gens.evaluate_built(&p));
153 let c = eval(cand)?;
154 let mut earlier: Vec<GenerationId> = gens.boot_entries()?;
157 earlier.push(txn.revert_to);
158 earlier.sort();
159 earlier.dedup();
160 earlier.retain(|g| g.0 < cand.0 && floor.is_none_or(|f| g.0 >= f.0));
161 let earlier: Vec<EvaluatedGeneration> = earlier.into_iter().map(eval).collect::<Result<_, _>>()?;
162 Ok(c.packages
163 .keys()
164 .filter(|p| !c.insecure.contains(*p))
165 .filter(|p| earlier.iter().all(|e| e.insecure.contains(*p) || !e.packages.contains_key(*p)))
166 .filter(|p| earlier.iter().any(|e| e.insecure.contains(*p)))
167 .cloned()
168 .collect())
169 }
170
171 fn generations(&self) -> &B::Gens {
172 self.b.generations()
173 }
174
175 fn inventory(&self) -> &B::Inv {
176 self.b.inventory()
177 }
178
179 fn execute_runtime(&self, t: &Transition, op: &OpId) -> Outcome {
180 match t {
181 Transition::RestartUnit(u) => self.b.units().restart(u, op),
182 Transition::StopUnit(u) => self.b.units().stop(u, op),
183 Transition::CollectGarbage { emergency } => {
184 let protect = self.protect.lock().unwrap_or_else(|e| e.into_inner()).clone();
185 self.b.generations().collect_garbage(&protect, *emergency, op)
186 }
187 Transition::SetRuntimeOption { path, value } => self.b.runtime().set(path, value, op),
188 other => Outcome::Failed(format!("not a runtime operation: {other:?}")),
189 }
190 }
191
192 fn reobserve(&self, txn: &Transaction) -> Outcome {
193 self.settle(&txn.expected.settle);
194 let manifest = txn.check.candidate.clone().unwrap_or_else(|| txn.check.base.clone());
195 let (s, defs) = self.observe(&manifest);
196 let missed: Vec<String> = txn
197 .expected
198 .units
199 .iter()
200 .filter(|(u, want)| defs.contains_key(*u) && &s.status(u, &defs) != *want)
201 .map(|(u, want)| format!("{u}: expected {want:?}, observed {:?}", s.status(u, &defs)))
202 .collect();
203 if missed.is_empty() { Outcome::Succeeded } else { Outcome::Partial(missed.join("; ")) }
204 }
205
206 fn check(&self, txn: &Transaction, phase: CheckPhase) -> Verdict {
207 let settle = match phase {
208 CheckPhase::Candidate => txn.expected.settle.clone(),
209 CheckPhase::Revert => Settle::Duration(DEFAULT_SETTLE),
210 };
211 self.settle(&settle);
212 let manifest = match phase {
213 CheckPhase::Candidate => txn.check.candidate.clone().unwrap_or_else(|| txn.check.base.clone()),
214 CheckPhase::Revert => txn.check.base.clone(),
215 };
216 let deadline = self.b.clock().now().plus(CHECK_PATIENCE);
219 loop {
220 let (s, defs) = self.observe(&manifest);
221 let v = self.violations(txn, phase, &s, &defs);
222 if v.is_empty() {
223 return Verdict::Pass;
224 }
225 if self.b.clock().now() >= deadline {
226 return Verdict::Fail(v.join("; "));
227 }
228 self.b.clock().sleep(CHECK_POLL);
229 }
230 }
231
232 fn release_gc_root(&self, path: &str) -> bool {
233 self.b.generations().release_root(path)
234 }
235
236 fn now(&self) -> Timestamp {
237 self.b.clock().now()
238 }
239
240 fn boot_now(&self) -> Timestamp {
241 self.b.clock().boot_now()
242 }
243
244 fn sleep(&self, d: Millis) {
245 self.b.clock().sleep(d)
246 }
247
248 fn kill_worker(&self, pid: u32) -> bool {
249 self.b.processes().kill_group(pid)
250 }
251
252 fn pid_alive(&self, pid: u32) -> bool {
253 self.b.processes().alive(pid)
254 }
255}
256
257fn space_only(u: &UnitId, s: &SystemState, defs: &UnitDefs) -> bool {
259 let failing = s.failing_readiness(u, defs);
260 !failing.is_empty()
261 && defs.get(u).is_some_and(|d| {
262 failing.iter().all(|f| d.probes.iter().any(|p| &p.id == f && matches!(p.kind, ProbeKind::FreeSpace { .. })))
263 })
264}
265
266pub fn declared(d: &UnitDef) -> UnitDef {
274 let UnitDef {
275 id,
276 desired,
277 edges,
278 sensitivity,
279 restart_disruption,
280 probes,
281 synonyms,
282 systemd_restart,
283 startup_grace,
284 runtime_repair,
285 stops_are_faults,
286 hold_allow,
287 hold_interfaces,
288 holds: _,
290 } = d.clone();
291 UnitDef {
292 id,
293 desired,
294 edges,
295 sensitivity,
296 restart_disruption,
297 probes,
298 synonyms,
299 systemd_restart,
300 startup_grace,
301 runtime_repair,
302 stops_are_faults,
303 hold_allow,
304 hold_interfaces,
305 holds: false,
306 }
307}
308
309pub fn changed_units(a: &Manifest, b: &Manifest) -> BTreeSet<UnitId> {
314 let mut out = BTreeSet::new();
315 for (id, d) in &a.units {
316 if b.units.get(id).map(declared) != Some(declared(d)) {
317 out.insert(id.clone());
318 }
319 }
320 for id in b.units.keys() {
321 if !a.units.contains_key(id) {
322 out.insert(id.clone());
323 }
324 }
325 out
326}
327
328#[cfg(test)]
330#[path = "host_props.rs"]
331mod props;