1use planarian_changes::render;
15use planarian_schema::*;
16pub use planarian_txn::store::MemStore;
17use planarian_txn::store::Store;
18use std::collections::{BTreeMap, BTreeSet};
19use std::sync::{Arc, Mutex, MutexGuard};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Effect {
23 Break(UnitId),
25 Remove(UnitId),
27 TouchCore,
29 ReplaceProbe(UnitId, ProbeDef),
31 DropProbe(UnitId, String),
33 NeedsReboot,
35 Declare { service: String, file: String },
39 Package { name: String, version: String, insecure: bool },
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum Injected {
50 Partial,
51 Unknown,
52 Fenced,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct Rule {
58 pub option: String,
59 pub value: Value,
60 pub effect: Effect,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum Health {
65 Ok,
66 Http503,
68 Delay(Millis),
71 Stall(Millis),
74}
75
76#[derive(Clone, Debug)]
77pub struct UnitSim {
78 pub observed: Observed,
79 pub ended: Option<Ending>,
81 pub health: Health,
82 pub fail_after: Option<Millis>,
84 pub fail_on_start: bool,
86 pub systemd_restart: Option<Millis>,
88 pub started_at: Timestamp,
89 pub failed_at: Option<Timestamp>,
90 pub restarts: u32,
91 pub auto_restarts: u32,
93}
94
95impl UnitSim {
96 pub fn active(now: Timestamp) -> UnitSim {
97 UnitSim {
98 observed: Observed::Active,
99 ended: None,
100 health: Health::Ok,
101 fail_after: None,
102 fail_on_start: false,
103 systemd_restart: None,
104 started_at: now,
105 failed_at: None,
106 restarts: 0,
107 auto_restarts: 0,
108 }
109 }
110}
111
112#[derive(Clone, Debug)]
113pub struct Gen {
114 pub path: StorePath,
115 pub eval: EvaluatedGeneration,
116 pub broken: BTreeSet<UnitId>,
117 pub created: (u32, Timestamp),
119}
120
121pub const PAYLOAD_DELAY: Millis = Millis::secs(240);
124
125#[derive(Clone, Debug)]
126pub struct Job {
127 pub op: OpId,
128 pub done_at: Timestamp,
129 pub cancellable: bool,
130 pub activates: GenerationId,
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct Call {
135 pub at: Timestamp,
136 pub op: OpId,
137 pub call: String,
138 pub fenced: bool,
139}
140
141const NO_OP: OpId = OpId { txn: TxnId(0), epoch: 0, seq: 0 };
143
144#[derive(Debug, Default)]
145pub struct World {
146 pub now: Timestamp,
147 pub netwatch: planarian_backend_netwatch::Tracker,
150 pub netwatch_failed: Option<String>,
153 pub netwatch_stale: BTreeSet<UnitId>,
155 pub wall_skew: i64,
157 pub base_units: UnitDefs,
160 pub base_user_units: Vec<UnitDef>,
161 pub base_options: BTreeMap<String, Value>,
162 pub base_packages: BTreeMap<String, String>,
164 pub base_insecure: BTreeSet<String>,
165 pub inject: BTreeMap<String, Injected>,
167 pub base_inputs: BTreeMap<String, (String, String)>,
169 pub rebuild_uid: u32,
171 pub payload_at: Option<Timestamp>,
174 pub rules: Vec<Rule>,
175 pub forced: BTreeSet<String>,
177 pub core_units: BTreeMap<String, Hash>,
178 pub owner_files: BTreeMap<String, String>,
180 pub untracked: BTreeSet<String>,
181 pub gens: BTreeMap<GenerationId, Gen>,
182 pub current: GenerationId,
183 pub booted: GenerationId,
184 pub boot_default: GenerationId,
185 pub boot_entries: BTreeSet<GenerationId>,
186 pub overrides: OverrideSet,
187 pub override_store: BTreeMap<Hash, OverrideSet>,
188 pub gen_overrides: BTreeMap<GenerationId, Hash>,
189 pub staged: BTreeMap<String, (OverrideSet, BTreeMap<String, String>)>,
190 pub gc_roots: BTreeSet<String>,
191 pub collected: Vec<GenerationId>,
192 pub fail_publish: bool,
193 pub bad_drivers: BTreeSet<String>,
195 pub edit_on_stage: bool,
199 pub untracked_on_stage: Option<String>,
201 pub nospace_on_build: bool,
202 pub builds: usize,
204 pub stages: usize,
206 pub block_on_activate: BTreeSet<UnitId>,
207 pub fail_activate: BTreeSet<GenerationId>,
208 pub slow_activation: Option<(Millis, bool)>,
209 pub jobs: BTreeMap<JobId, Job>,
210 pub job_spans: BTreeMap<JobId, (Job, Timestamp, Option<Timestamp>)>,
212 pub next_job: u64,
213 pub jobs_tagged_unknown: bool,
214 pub units: BTreeMap<UnitId, UnitSim>,
216 pub devices: BTreeMap<UnitId, Option<DriverRef>>,
217 pub sessions: Vec<SessionInfo>,
218 pub compositors: BTreeMap<String, bool>,
221 pub one_shot: Option<GenerationId>,
224 pub fail_one_shot: bool,
225 pub third_party_on_publish: bool,
228 pub agent_instances: BTreeMap<String, InstanceId>,
231 pub dry_activate_fails: Option<String>,
233 pub hold_on_failure: BTreeSet<UnitId>,
236 pub held: BTreeSet<UnitId>,
238 pub hold_declines: BTreeSet<UnitId>,
241 pub hold_hangs: Option<Millis>,
245 pub lift_hangs: Option<Millis>,
248 pub hold_wedged: BTreeSet<UnitId>,
251 pub hold_released: BTreeMap<UnitId, Option<Timestamp>>,
254 pub hold_fails: Option<String>,
256 pub hold_table_flushed: bool,
259 pub hold_table_unreadable: bool,
262 pub hold_reapply_fails: bool,
265 pub nar_mismatch: bool,
268 pub probe_batch_ms: u64,
270 pub activation_restarts: BTreeSet<UnitId>,
273 pub next_instance: u64,
274 pub third_party_override: Option<Hash>,
275 pub foreign_units: Vec<String>,
277 pub foreground: BTreeSet<String>,
279 pub power: Option<PowerSource>,
280 pub blocked: BTreeSet<UnitId>,
282 pub external_down: BTreeSet<String>,
284 pub external_unreachable: BTreeSet<String>,
287 pub probe_batches: u64,
289 pub fs: BTreeMap<String, (u64, u64)>,
291 pub gc_budget: gc::GcBudget,
294 pub space_report: Option<gc::SpaceReport>,
295 pub runtime: BTreeMap<String, Value>,
296 pub wifi_known: Vec<String>,
298 pub wifi_in_range: BTreeSet<String>,
299 pub alive: BTreeSet<u32>,
301 pub unkillable: BTreeSet<u32>,
302 pub calls: Vec<Call>,
303}
304
305impl World {
306 pub fn nar_of(path: &StorePath) -> Hash {
308 Hash::of(path.0.as_bytes())
309 }
310
311 pub fn dry_plan(&self, g: GenerationId) -> ActivationPlan {
314 let cur = &self.gens[&self.current];
315 let new = &self.gens[&g];
316 let mut plan = new.eval.activation.clone();
317 if g != self.current {
318 plan.restart.extend(self.activation_restarts.iter().cloned());
319 }
320 for u in new.eval.units.keys() {
321 match cur.eval.units.get(u) {
322 None => {
323 plan.start.insert(u.clone());
324 }
325 Some(old) if old != &new.eval.units[u] || new.broken.contains(u) != cur.broken.contains(u) => {
326 plan.restart.insert(u.clone());
327 }
328 _ => {}
329 }
330 }
331 for (k, v) in &new.eval.options {
332 if cur.eval.options.get(k) != Some(v) {
333 if let Some(svc) = k.strip_prefix("services.").and_then(|r| r.split('.').next()) {
335 let id = UnitId::service(svc);
336 if new.eval.units.contains_key(&id) {
337 plan.restart.insert(id);
338 }
339 }
340 }
341 }
342 for u in cur.eval.units.keys() {
343 if !new.eval.units.contains_key(u) {
344 plan.stop.insert(u.clone());
345 }
346 }
347 plan
348 }
349
350 fn key(id: &UnitId) -> UnitId {
351 let mut k = id.clone();
352 if let Scope::User { uid, .. } = k.scope {
353 k.scope = Scope::User { uid, session: None };
354 }
355 k
356 }
357
358 pub fn override_hash_of(set: &OverrideSet) -> Hash {
359 Hash::of(render(set).as_bytes())
360 }
361
362 fn source_hash_of(files: &BTreeMap<String, String>, overrides: &OverrideSet) -> Hash {
363 let mut bytes = serde_json::to_vec(files).expect("files serialize");
364 bytes.extend(render(overrides).as_bytes());
365 Hash::of(&bytes)
366 }
367
368 pub fn evaluate(&self, overrides: &OverrideSet) -> (EvaluatedGeneration, BTreeSet<UnitId>) {
370 let mut options = self.base_options.clone();
371 for (k, v) in &overrides.assignments {
372 if !self.forced.contains(k) {
373 options.insert(k.clone(), v.clone());
374 }
375 }
376 let mut units = self.base_units.clone();
377 let mut core = self.core_units.clone();
378 let mut broken = BTreeSet::new();
379 let mut needs_reboot = false;
380 let mut packages = self.base_packages.clone();
381 let mut insecure = self.base_insecure.clone();
382 let mut declarations: BTreeMap<String, Vec<String>> = self
383 .base_units
384 .keys()
385 .filter(|u| u.kind == UnitKind::Service && u.scope == Scope::System)
386 .map(|u| (u.name.clone(), vec!["/etc/nixos/configuration.nix".to_string()]))
387 .collect();
388 for r in &self.rules {
389 if options.get(&r.option) != Some(&r.value) {
390 continue;
391 }
392 match &r.effect {
393 Effect::Break(u) => {
394 broken.insert(u.clone());
395 }
396 Effect::Remove(u) => {
397 units.remove(u);
398 declarations.remove(&u.name);
399 }
400 Effect::Declare { service, file } => {
401 declarations.entry(service.clone()).or_default().push(file.clone());
402 }
403 Effect::TouchCore => {
404 core.insert("planariad.service".into(), Hash::of(b"changed"));
405 }
406 Effect::ReplaceProbe(u, p) => {
407 if let Some(d) = units.get_mut(u) {
408 d.probes.retain(|x| x.id != p.id);
409 d.probes.push(p.clone());
410 }
411 }
412 Effect::DropProbe(u, id) => {
413 if let Some(d) = units.get_mut(u) {
414 d.probes.retain(|x| x.id.0 != *id);
415 }
416 }
417 Effect::NeedsReboot => needs_reboot = true,
418 Effect::Package { name, version, insecure: bad } => {
419 packages.insert(name.clone(), version.clone());
420 if *bad {
421 insecure.insert(name.clone());
422 } else {
423 insecure.remove(name);
424 }
425 }
426 }
427 }
428 let locked = overrides.assignments.keys().filter(|k| self.forced.contains(*k)).cloned().collect();
429 let eval = EvaluatedGeneration {
430 source: Hash::ZERO,
431 options,
432 units,
433 user_units: self.base_user_units.clone(),
434 activation: ActivationPlan { needs_reboot_to_confirm: needs_reboot, ..Default::default() },
435 locked,
436 core_units: core,
437 packages,
438 insecure,
439 declarations,
440 inputs: self.base_inputs.clone(),
441 services: BTreeMap::new(),
442 };
443 (eval, broken)
444 }
445
446 pub fn add_generation(&mut self, overrides: &OverrideSet) -> GenerationId {
447 let (eval, broken) = self.evaluate(overrides);
448 let id = GenerationId(self.gens.keys().map(|g| g.0).max().unwrap_or(0) + 1);
449 let path = StorePath(format!("/nix/store/{:03}-nixos-system-fake", id.0));
450 let created = (self.rebuild_uid, self.now);
451 self.gens.insert(id, Gen { path, eval, broken, created });
452 id
453 }
454
455 pub fn apply_generation(&mut self, g: GenerationId) {
456 let generation = self.gens[&g].clone();
457 let now = self.now;
458 for id in generation.eval.units.keys() {
459 if id.kind != UnitKind::Service || id.name.starts_with("external:") {
460 continue;
461 }
462 let sim = self.units.entry(id.clone()).or_insert_with(|| UnitSim::active(now));
463 if generation.broken.contains(id) || sim.fail_on_start {
464 sim.observed = Observed::Failed(Reason::ExitCode(1));
465 sim.ended = Some(Ending::Exited(1));
466 sim.failed_at = Some(now);
467 } else if sim.observed != Observed::Active {
468 sim.observed = Observed::Active;
469 sim.started_at = now;
470 }
471 }
472 self.payload_at = if generation.eval.declarations.contains_key("gym-bad-payload") {
474 self.payload_at.or(Some(now.plus(PAYLOAD_DELAY)))
475 } else {
476 if self.payload_at == Some(Timestamp(u64::MAX))
478 && let Some(web) = self.units.get_mut(&UnitId::service("gym-web"))
479 {
480 web.fail_on_start = false;
481 }
482 None
483 };
484 let gone: Vec<UnitId> = self
486 .units
487 .keys()
488 .filter(|u| {
489 u.scope == Scope::System && u.kind == UnitKind::Service && !generation.eval.units.contains_key(u)
490 })
491 .cloned()
492 .collect();
493 for u in gone {
494 self.units.get_mut(&u).expect("listed").observed = Observed::Absent;
495 }
496 self.current = g;
497 }
498
499 pub fn on_failure_holds(&mut self) {
502 let now = self.now;
503 let failed: Vec<UnitId> = self
504 .hold_on_failure
505 .iter()
506 .filter(|u| self.units.get(*u).is_some_and(|s| matches!(s.observed, Observed::Failed(_))))
507 .filter(|u| !self.hold_released.get(*u).is_some_and(|t| t.is_none_or(|t| t > now)))
508 .cloned()
509 .collect();
510 self.held.extend(failed);
511 }
512
513 pub fn advance_to(&mut self, now: Timestamp) {
515 self.now = now;
516 if self.payload_at.is_some_and(|t| t <= now) {
518 self.payload_at = Some(Timestamp(u64::MAX));
519 if let Some(web) = self.units.get_mut(&UnitId::service("gym-web")) {
520 web.fail_on_start = true;
521 web.observed = Observed::Failed(Reason::ExitCode(1));
522 web.ended = Some(Ending::Signal(11));
523 web.failed_at = Some(now);
524 }
525 }
526 let done: Vec<JobId> = self.jobs.iter().filter(|(_, j)| j.done_at <= now).map(|(k, _)| k.clone()).collect();
527 for id in done {
528 let job = self.jobs.remove(&id).expect("listed");
529 if let Some(span) = self.job_spans.get_mut(&id) {
530 span.2 = Some(job.done_at);
531 }
532 self.apply_generation(job.activates);
533 }
534 let broken = self.gens.get(&self.current).map(|g| g.broken.clone()).unwrap_or_default();
535 for (id, sim) in self.units.iter_mut() {
536 if sim.observed == Observed::Active
537 && let Some(after) = sim.fail_after
538 && now.0 >= sim.started_at.0 + after.0
539 {
540 sim.observed = Observed::Failed(Reason::ExitCode(1));
541 sim.ended = Some(Ending::Exited(1));
542 sim.failed_at = Some(Timestamp(sim.started_at.0 + after.0));
543 }
544 if let (Observed::Failed(_), Some(rs), Some(at)) = (&sim.observed, sim.systemd_restart, sim.failed_at)
545 && now.0 >= at.0 + rs.0
546 {
547 sim.restarts += 1;
548 sim.auto_restarts += 1;
549 if sim.fail_on_start || broken.contains(id) {
550 sim.failed_at = Some(now);
551 } else {
552 sim.observed = Observed::Active;
553 sim.started_at = now;
554 }
555 }
556 }
557 self.on_failure_holds();
558 }
559
560 pub fn stop_out_of_band(&mut self, unit: &UnitId) {
563 let mut todo = vec![unit.clone()];
564 while let Some(u) = todo.pop() {
565 if let Some(sim) = self.units.get_mut(&u)
566 && sim.observed == Observed::Active
567 {
568 sim.observed = Observed::Bound;
570 sim.ended = Some(Ending::Signal(15));
571 }
572 for (id, d) in &self.base_units {
573 if d.edges.iter().any(|e| e.to == u && e.kind == EdgeKind::Requires) {
574 todo.push(id.clone());
575 }
576 }
577 }
578 }
579
580 pub fn kill(&mut self, unit: &UnitId) {
582 self.signal(unit, 9);
583 }
584
585 pub fn break_in(&mut self, g: GenerationId, unit: &UnitId) {
591 let now = self.now;
592 if let Some(generation) = self.gens.get_mut(&g) {
593 generation.broken.insert(unit.clone());
594 }
595 if self.current == g
596 && let Some(sim) = self.units.get_mut(unit)
597 {
598 sim.observed = Observed::Failed(Reason::ExitCode(1));
599 sim.ended = Some(Ending::Exited(1));
600 sim.failed_at = Some(now);
601 }
602 }
603
604 pub fn signal(&mut self, unit: &UnitId, sig: i32) {
605 let now = self.now;
606 if let Some(sim) = self.units.get_mut(unit) {
607 sim.ended = Some(Ending::Signal(sig));
608 if matches!(sig, 1 | 2 | 13 | 15) {
609 sim.observed = Observed::Bound;
610 } else {
611 sim.observed = Observed::Failed(Reason::ExitCode(128 + sig));
612 sim.failed_at = Some(now);
613 }
614 }
615 self.on_failure_holds();
616 }
617
618 pub fn oom(&mut self, unit: &UnitId) {
620 let now = self.now;
621 if let Some(sim) = self.units.get_mut(unit) {
622 sim.ended = Some(Ending::OomKill);
623 sim.observed = Observed::Failed(Reason::ExitCode(137));
624 sim.failed_at = Some(now);
625 }
626 }
627
628 pub fn reboot(&mut self) {
629 self.booted = match self.one_shot.take() {
632 Some(g) if !self.fail_one_shot => g,
633 _ => self.boot_default,
634 };
635 let now = self.now;
636 for id in self.jobs.keys() {
637 if let Some(span) = self.job_spans.get_mut(id) {
638 span.2 = Some(now);
639 }
640 }
641 self.jobs.clear();
642 self.sessions.clear();
643 let g = self.booted;
644 for sim in self.units.values_mut() {
645 sim.observed = Observed::Bound;
646 }
647 self.apply_generation(g);
648 }
649
650 fn start(&mut self, id: &UnitId) -> Outcome {
651 let now = self.now;
652 let broken = self.gens.get(&self.current).is_some_and(|g| g.broken.contains(id));
653 let present =
654 self.gens.get(&self.current).is_none_or(|g| g.eval.units.contains_key(id) || id.scope != Scope::System);
655 if !present {
656 return Outcome::Failed(format!("{id} not found"));
657 }
658 let sim = self.units.entry(Self::key(id)).or_insert_with(|| UnitSim::active(now));
659 sim.restarts += 1;
660 if broken || sim.fail_on_start {
661 sim.observed = Observed::Failed(Reason::ExitCode(1));
662 sim.ended = Some(Ending::Exited(1));
663 sim.failed_at = Some(now);
664 return Outcome::Failed(format!("{id} failed to start"));
665 }
666 sim.observed = Observed::Active;
667 sim.health = Health::Ok;
668 sim.started_at = now;
669 Outcome::Succeeded
670 }
671}
672
673#[derive(Clone, Debug)]
676pub struct Fake {
677 pub world: Arc<Mutex<World>>,
678 pub store: MemStore,
679}
680
681impl Fake {
682 pub fn footprint_view(&self, tools: &[UnitId]) -> planarian_backend_netwatch::FootprintView {
686 use planarian_backend_netwatch::{FRESH_SECS, WatchState, file_name};
687 let w = self.w();
688 let now = w.now.0 / 1000;
689 let watch = match &w.netwatch_failed {
690 None => WatchState::Attached { at: now },
691 Some(reason) => WatchState::Failed { reason: reason.clone() },
692 };
693 let mut view = w.netwatch.view(watch, tools, now);
694 for u in &w.netwatch_stale {
695 if let Some(f) = view.units.get_mut(&file_name(u)) {
696 f.at = now.saturating_sub(FRESH_SECS + 1);
697 }
698 }
699 view
700 }
701
702 pub fn new(world: World, store: MemStore) -> Fake {
703 Fake { world: Arc::new(Mutex::new(world)), store }
704 }
705
706 pub fn w(&self) -> MutexGuard<'_, World> {
707 self.world.lock().unwrap_or_else(|e| e.into_inner())
708 }
709
710 fn hung(&self, verb: &str, unit: &UnitId, wait: Millis) -> Option<Outcome> {
715 let wedged = verb == "hold" && self.w().hold_wedged.contains(unit);
716 let h = if wedged {
717 Millis(u64::MAX)
718 } else if verb == "lift_hold" && self.w().lift_hangs.is_some() {
719 self.w().lift_hangs?
720 } else {
721 self.w().hold_hangs?
722 };
723 if h.0 > wait.0 {
724 let now = self.w().now;
725 self.w().calls.push(Call { call: format!("{verb}({unit}) timed out"), op: NO_OP, at: now, fenced: false });
726 self.advance(wait);
727 return Some(Outcome::Unknown(format!("{verb} for {unit} did not finish")));
728 }
729 self.advance(h);
730 None
731 }
732
733 pub fn advance(&self, d: Millis) {
734 let mut w = self.w();
735 let t = w.now.plus(d);
736 w.advance_to(t);
737 }
738
739 fn admit(&self, w: &mut World, op: &OpId, call: String) -> bool {
740 let fenced = self.store.fence(op.txn).ok().flatten() != Some(op.epoch);
741 w.calls.push(Call { at: w.now, op: *op, call, fenced });
742 !fenced
743 }
744
745 fn run(&self, op: &OpId, call: String, f: impl FnOnce(&mut World) -> Outcome) -> Outcome {
749 let mut w = self.w();
750 let name = call.split('(').next().unwrap_or("").to_string();
751 let injected = w.inject.remove(&name);
752 if injected == Some(Injected::Fenced) {
753 let at = w.now;
754 w.calls.push(Call { at, op: *op, call, fenced: true });
755 return Outcome::fenced();
756 }
757 if !self.admit(&mut w, op, call) {
758 return Outcome::fenced();
759 }
760 let out = f(&mut w);
761 match injected {
762 Some(Injected::Partial) => Outcome::Failed("partially applied (injected)".into()),
763 Some(Injected::Unknown) => Outcome::Unknown("no answer (injected)".into()),
764 _ => out,
765 }
766 }
767}
768
769impl UnitBackend for Fake {
770 fn protecting(&self, tools: &[UnitId]) -> Result<BTreeSet<UnitId>, Outcome> {
773 let now = self.w().now.0 / 1000;
774 Ok(self.footprint_view(tools).protecting(tools, now))
775 }
776 fn observe(&self, id: &UnitId) -> Result<Observed, Outcome> {
777 let w = self.w();
778 Ok(w.units.get(&World::key(id)).map(|s| s.observed.clone()).unwrap_or(Observed::Absent))
779 }
780
781 fn ended_by(&self, id: &UnitId) -> Option<Ending> {
782 self.w().units.get(&World::key(id)).and_then(|s| s.ended)
783 }
784
785 fn auto_restarts(&self, id: &UnitId) -> Option<u32> {
786 self.w().units.get(&World::key(id)).map(|s| s.auto_restarts)
787 }
788
789 fn foreign_units(&self) -> Vec<String> {
790 self.w().foreign_units.clone()
791 }
792
793 fn list(&self) -> Result<Vec<UnitId>, Outcome> {
794 Ok(self.w().units.keys().cloned().collect())
795 }
796
797 fn start_agent(
798 &self,
799 name: &str,
800 _argv: &[String],
801 _limits: &planarian_schema::backend::AgentLimits,
802 ) -> Result<InstanceId, Outcome> {
803 let mut w = self.w();
804 let now = w.now;
805 let id = UnitId::service(&format!("planarian-agent-{name}"));
806 w.units.insert(World::key(&id), UnitSim::active(now));
807 w.next_instance += 1;
809 let mut b = [0u8; 16];
810 b[..8].copy_from_slice(&w.next_instance.to_be_bytes());
811 b[8] = 0xa9;
812 let inst = InstanceId(b);
813 w.agent_instances.insert(name.to_string(), inst);
814 Ok(inst)
815 }
816
817 fn agent_running(&self, name: &str) -> bool {
818 let w = self.w();
819 let id = UnitId::service(&format!("planarian-agent-{name}"));
820 w.units.get(&World::key(&id)).is_some_and(|u| matches!(u.observed, Observed::Active))
821 }
822
823 fn hold(&self, unit: &UnitId, wait: Millis) -> Outcome {
824 if let Some(o) = self.hung("hold", unit, wait) {
825 return o;
826 }
827 let mut w = self.w();
828 if let Some(why) = w.hold_fails.take() {
829 return Outcome::Failed(why);
830 }
831 let now = w.now;
832 w.calls.push(Call { call: format!("hold({unit})"), op: NO_OP, at: now, fenced: false });
834 if !w.hold_released.get(unit).is_some_and(|t| t.is_none_or(|t| t > now)) && !w.hold_declines.contains(unit) {
836 w.held.insert(unit.clone());
837 }
838 Outcome::Succeeded
839 }
840
841 fn lift_hold(&self, unit: &UnitId, wait: Millis) -> Outcome {
842 if let Some(o) = self.hung("lift_hold", unit, wait) {
843 return o;
844 }
845 let mut w = self.w();
846 if let Some(why) = w.hold_fails.take() {
847 return Outcome::Failed(why);
848 }
849 let now = w.now;
850 w.calls.push(Call { call: format!("lift_hold({unit})"), op: NO_OP, at: now, fenced: false });
851 w.held.remove(unit);
852 Outcome::Succeeded
853 }
854
855 fn held(&self) -> Result<BTreeSet<UnitId>, Outcome> {
856 Ok(self.w().held.clone())
857 }
858
859 fn release_hold(&self, unit: &UnitId, until: Option<Timestamp>, wait: Millis) -> Outcome {
860 self.w().hold_released.insert(unit.clone(), until);
861 self.lift_hold(unit, wait)
862 }
863
864 fn end_release(&self, unit: &UnitId) -> Outcome {
865 self.w().hold_released.remove(unit);
866 Outcome::Succeeded
867 }
868
869 fn hold_table_present(&self, wait: Millis) -> Result<bool, Outcome> {
870 let hold_unit = UnitId::service("planarian-hold");
871 if let Some(o) = self.hung("nft list", &hold_unit, wait) {
872 return Err(o);
873 }
874 let w = self.w();
875 if w.hold_table_unreadable {
876 return Err(Outcome::Failed("nft: No such file or directory".into()));
877 }
878 Ok(w.held.is_empty() || !w.hold_table_flushed)
879 }
880
881 fn reapply_hold(&self, unit: &UnitId, wait: Millis) -> Outcome {
882 if let Some(o) = self.hung("reapply_hold", unit, wait) {
883 return o;
884 }
885 let mut w = self.w();
886 let now = w.now;
887 w.calls.push(Call { call: format!("reapply_hold({unit})"), op: NO_OP, at: now, fenced: false });
888 if w.hold_reapply_fails {
889 return Outcome::Failed("planarian-hold: nft: the table did not load".into());
890 }
891 w.held.insert(unit.clone());
892 w.hold_table_flushed = false;
893 Outcome::Succeeded
894 }
895
896 fn restart(&self, id: &UnitId, op: &OpId) -> Outcome {
897 self.run(op, format!("restart({id})"), |w| w.start(id))
898 }
899
900 fn stop(&self, id: &UnitId, op: &OpId) -> Outcome {
901 self.run(op, format!("stop({id})"), |w| {
902 let now = w.now;
903 let sim = w.units.entry(World::key(id)).or_insert_with(|| UnitSim::active(now));
904 sim.observed = Observed::Bound;
905 Outcome::Succeeded
906 })
907 }
908}
909
910impl GenerationBackend for Fake {
911 fn current(&self) -> Result<GenerationId, Outcome> {
912 Ok(self.w().current)
913 }
914
915 fn boot_entries(&self) -> Result<Vec<GenerationId>, Outcome> {
916 Ok(self.w().boot_entries.iter().copied().collect())
917 }
918
919 fn boot_default(&self) -> Result<GenerationId, Outcome> {
920 Ok(self.w().boot_default)
921 }
922
923 fn last_booted(&self) -> Result<GenerationId, Outcome> {
924 Ok(self.w().booted)
925 }
926
927 fn created_by(&self, g: GenerationId) -> Option<(u32, Timestamp)> {
928 self.w().gens.get(&g).map(|x| x.created)
929 }
930
931 fn stage(&self, _owner: &PinnedSource, overrides: &OverrideSet) -> Result<StagedSource, Outcome> {
932 let mut w = self.w();
933 w.stages += 1;
934 let dir = format!("/var/lib/planarian/stage/{}", w.staged.len() + 1);
935 let files = w.owner_files.clone();
936 let base = w.overrides.clone();
937 w.staged.insert(dir.clone(), (overrides.clone(), files.clone()));
938 let candidate_override = World::override_hash_of(overrides);
939 w.override_store.insert(candidate_override, overrides.clone());
940 if std::mem::take(&mut w.edit_on_stage) {
941 w.owner_files.insert("configuration.nix".into(), "{ edited-during-build = true; }".into());
942 }
943 let excluded: Vec<String> = w.untracked.iter().cloned().collect();
944 if let Some(f) = w.untracked_on_stage.take() {
945 w.untracked.insert(f);
946 }
947 Ok(StagedSource {
948 dir,
949 base_source: World::source_hash_of(&files, &base),
950 staged_source: World::source_hash_of(&files, overrides),
951 base_override: World::override_hash_of(&base),
952 candidate_override,
953 excluded,
954 })
955 }
956
957 fn source_hash(&self, _owner: &PinnedSource) -> Result<Hash, Outcome> {
958 let w = self.w();
959 Ok(World::source_hash_of(&w.owner_files, &w.overrides))
960 }
961
962 fn untracked(&self, _owner: &PinnedSource) -> Vec<String> {
963 self.w().untracked.iter().cloned().collect()
964 }
965
966 fn evaluate(&self, src: &StagedSource) -> Result<EvaluatedGeneration, Outcome> {
967 let w = self.w();
968 let (overrides, _) = w.staged.get(&src.dir).ok_or_else(|| Outcome::Failed("no such stage".into()))?;
969 let (mut eval, _) = w.evaluate(overrides);
970 eval.source = src.staged_source;
971 Ok(eval)
972 }
973
974 fn build(&self, eval: &EvaluatedGeneration) -> Result<Built, Outcome> {
975 let mut w = self.w();
976 w.builds += 1;
977 if std::mem::take(&mut w.nospace_on_build) {
978 return Err(Outcome::Failed("error: writing to file: No space left on device".into()));
979 }
980 if let Some(why) = w.dry_activate_fails.take() {
983 return Err(Outcome::Failed(why));
984 }
985 let staged = w
986 .staged
987 .values()
988 .find(|(o, files)| World::source_hash_of(files, o) == eval.source)
989 .cloned()
990 .ok_or_else(|| Outcome::Failed("build: unknown evaluation".into()))?;
991 let existing =
993 w.gens.iter().find(|(_, x)| x.eval.source == eval.source && eval.source != Hash::ZERO).map(|(g, _)| *g);
994 let g = match existing {
995 Some(g) => g,
996 None => {
997 let g = w.add_generation(&staged.0);
998 w.gens.get_mut(&g).expect("added").eval.source = eval.source;
999 g
1000 }
1001 };
1002 let new = w.gens[&g].clone();
1003 let plan = w.dry_plan(g);
1004 let root = format!("/nix/var/nix/gcroots/planarian/builds/{}", eval.source.to_hex());
1005 w.gc_roots.insert(root.clone());
1006 let nar = Some(World::nar_of(&new.path));
1007 Ok(Built { generation: g, path: new.path, root, activation: plan, nar })
1008 }
1009
1010 fn dry_activation(&self, path: &StorePath) -> Result<ActivationPlan, Outcome> {
1011 let mut w = self.w();
1012 if let Some(why) = w.dry_activate_fails.take() {
1013 return Err(Outcome::Failed(why));
1014 }
1015 let g = w.gens.iter().find(|(_, x)| &x.path == path).map(|(g, _)| *g);
1016 let g = g.ok_or_else(|| Outcome::Failed("unknown store path".into()))?;
1017 Ok(w.dry_plan(g))
1018 }
1019
1020 fn override_content(&self, h: &Hash) -> Result<OverrideSet, Outcome> {
1021 self.w().override_store.get(h).cloned().ok_or_else(|| Outcome::Failed("no override content".into()))
1022 }
1023
1024 fn verify_candidate(&self, path: &StorePath, nar: Option<&Hash>) -> Result<bool, Outcome> {
1025 let mut w = self.w();
1026 let now = if std::mem::take(&mut w.nar_mismatch) { Hash::of(b"changed") } else { World::nar_of(path) };
1029 if nar.is_some_and(|n| *n != now) {
1030 return Ok(false);
1031 }
1032 Ok(w.gens.values().any(|g| &g.path == path)
1033 && !w.collected.iter().any(|c| w.gens.get(c).is_some_and(|g| &g.path == path)))
1034 }
1035
1036 fn pending_jobs(&self) -> Result<Vec<JobId>, Outcome> {
1037 Ok(self.w().jobs.keys().cloned().collect())
1038 }
1039
1040 fn jobs_tagged(&self, op: &OpId) -> Result<Option<Vec<JobId>>, Outcome> {
1041 let w = self.w();
1042 if w.jobs_tagged_unknown {
1043 return Ok(None);
1044 }
1045 Ok(Some(w.jobs.iter().filter(|(_, j)| j.op == *op).map(|(k, _)| k.clone()).collect()))
1046 }
1047
1048 fn override_hash(&self) -> Result<Hash, Outcome> {
1049 Ok(World::override_hash_of(&self.w().overrides))
1050 }
1051
1052 fn activate_live(&self, g: GenerationId, op: &OpId) -> Outcome {
1053 self.run(op, format!("activate_live({})", g.0), |w| {
1054 if !w.gens.contains_key(&g) {
1055 return Outcome::Failed(format!("no generation {}", g.0));
1056 }
1057 if w.fail_activate.contains(&g) {
1058 return Outcome::Failed(format!("switch-to-configuration test failed for {}", g.0));
1059 }
1060 if g != w.booted && !w.block_on_activate.is_empty() {
1061 let b = std::mem::take(&mut w.block_on_activate);
1062 w.blocked.extend(b);
1063 }
1064 if let Some((delay, cancellable)) = w.slow_activation {
1065 w.next_job += 1;
1066 let id = JobId(format!("job-{}", w.next_job));
1067 let done_at = w.now.plus(delay);
1068 let job = Job { op: *op, done_at, cancellable, activates: g };
1069 let now = w.now;
1070 w.job_spans.insert(id.clone(), (job.clone(), now, None));
1071 w.jobs.insert(id, job);
1072 return Outcome::Unknown("activation job accepted; no reply".into());
1073 }
1074 w.apply_generation(g);
1075 Outcome::Succeeded
1076 })
1077 }
1078
1079 fn publish_boot(&self, g: GenerationId, op: &OpId) -> Outcome {
1080 self.run(op, format!("publish_boot({})", g.0), |w| {
1081 if w.fail_publish && !w.boot_entries.contains(&g) {
1083 if let Some((free, _)) = w.fs.get_mut("/boot") {
1085 *free = 0;
1086 }
1087 return Outcome::Failed("No space left on device (/boot)".into());
1088 }
1089 w.boot_entries.insert(g);
1090 w.boot_default = g;
1091 if std::mem::take(&mut w.third_party_on_publish) {
1092 w.overrides.assignments.insert("gym.thirdParty".into(), Value::Str("edited".into()));
1093 w.third_party_override = Some(World::override_hash_of(&w.overrides));
1094 }
1095 Outcome::Succeeded
1096 })
1097 }
1098
1099 fn publish_boot_once(&self, g: GenerationId, keep: GenerationId, op: &OpId) -> Outcome {
1100 self.run(op, format!("publish_boot_once({})", g.0), |w| {
1101 w.boot_entries.insert(g);
1102 w.boot_default = keep;
1103 w.one_shot = Some(g);
1104 Outcome::Succeeded
1105 })
1106 }
1107
1108 fn one_shot_pending(&self) -> Result<Option<GenerationId>, Outcome> {
1109 Ok(self.w().one_shot)
1110 }
1111
1112 fn protect(&self, g: GenerationId, op: &OpId) -> Outcome {
1113 self.run(op, format!("protect({})", g.0), |_| Outcome::Succeeded)
1114 }
1115
1116 fn cancel(&self, job: &JobId, op: &OpId) -> Outcome {
1117 self.run(op, format!("cancel({})", job.0), |w| match w.jobs.get(job) {
1118 Some(j) if j.cancellable => {
1119 w.jobs.remove(job);
1120 let now = w.now;
1121 if let Some(span) = w.job_spans.get_mut(job) {
1122 span.2 = Some(now);
1123 }
1124 Outcome::Succeeded
1125 }
1126 Some(_) => Outcome::Failed("job cannot be cancelled".into()),
1127 None => Outcome::Succeeded,
1128 })
1129 }
1130
1131 fn swap_override(&self, from: &Hash, to: &Hash, op: &OpId) -> Outcome {
1132 self.run(op, "swap_override".into(), |w| {
1133 let cur = World::override_hash_of(&w.overrides);
1134 if cur == *to {
1135 return Outcome::Succeeded;
1136 }
1137 if cur != *from {
1138 return Outcome::Failed("override file changed by a third party".into());
1139 }
1140 match w.override_store.get(to).cloned() {
1141 Some(set) => {
1142 w.overrides = set;
1143 Outcome::Succeeded
1144 }
1145 None => Outcome::Failed("override content unknown".into()),
1146 }
1147 })
1148 }
1149
1150 fn path_of(&self, g: GenerationId) -> Result<StorePath, Outcome> {
1151 self.w().gens.get(&g).map(|x| x.path.clone()).ok_or_else(|| Outcome::Failed(format!("no generation {}", g.0)))
1152 }
1153
1154 fn evaluate_built(&self, path: &StorePath) -> Result<EvaluatedGeneration, Outcome> {
1155 self.w()
1156 .gens
1157 .values()
1158 .find(|g| &g.path == path)
1159 .map(|g| g.eval.clone())
1160 .ok_or_else(|| Outcome::Failed("unknown store path".into()))
1161 }
1162
1163 fn read_overrides(&self) -> Result<(OverrideSet, Hash), Outcome> {
1164 let w = self.w();
1165 Ok((w.overrides.clone(), World::override_hash_of(&w.overrides)))
1166 }
1167
1168 fn remember_override(&self, set: &OverrideSet) -> Result<Hash, Outcome> {
1169 let h = World::override_hash_of(set);
1170 self.w().override_store.insert(h, set.clone());
1171 Ok(h)
1172 }
1173
1174 fn override_for(&self, g: GenerationId) -> Result<Option<Hash>, Outcome> {
1175 Ok(self.w().gen_overrides.get(&g).copied())
1176 }
1177
1178 fn record_override(&self, g: GenerationId, hash: &Hash) -> Result<(), Outcome> {
1179 self.w().gen_overrides.insert(g, *hash);
1180 Ok(())
1181 }
1182
1183 fn release_root(&self, root: &str) -> bool {
1184 self.w().gc_roots.remove(root)
1185 }
1186
1187 fn collect_garbage(&self, protect: &[GenerationId], emergency: bool, op: &OpId) -> Outcome {
1188 self.run(op, "collect_garbage".into(), |w| {
1189 let plan = w.gc_plan(protect, emergency);
1190 let freed = plan.delete.len() as u64 * GEN_BYTES;
1191 w.collected.extend(plan.delete.iter().copied());
1192 for (free, size) in w.fs.values_mut() {
1193 *free = (*free + freed).min(*size);
1194 }
1195 w.space_report = Some(gc::SpaceReport::of(&plan, w.now));
1196 Outcome::Succeeded
1197 })
1198 }
1199
1200 fn gc_plan(&self, protect: &[GenerationId], emergency: bool) -> Result<gc::GcPlan, Outcome> {
1201 Ok(self.w().gc_plan(protect, emergency))
1202 }
1203
1204 fn space_report(&self) -> Option<gc::SpaceReport> {
1205 self.w().space_report.clone()
1206 }
1207}
1208
1209pub const GEN_BYTES: u64 = 2_000_000_000;
1211
1212impl World {
1213 fn gc_plan(&self, protect: &[GenerationId], emergency: bool) -> gc::GcPlan {
1214 let on_disk: Vec<GenerationId> = self.gens.keys().copied().filter(|g| !self.collected.contains(g)).collect();
1215 let mut keep: Vec<GenerationId> = protect.to_vec();
1216 keep.extend([self.current, self.boot_default]);
1217 keep.extend(on_disk.iter().copied().filter(|g| {
1221 let generation = &self.gens[g];
1222 !self.boot_entries.contains(g)
1223 && self.gc_roots.iter().any(|r| r.ends_with(&generation.eval.source.to_hex()))
1224 }));
1225 let fs_size = self.fs.get("/nix").map(|(_, size)| *size).unwrap_or(u64::MAX);
1226 let b = self.gc_budget;
1227 let count = if emergency { b.keep_emergency } else { b.keep_generations };
1228 gc::plan(&on_disk, &b.protected(&on_disk, &keep), count, b.max_bytes(fs_size), |gs| gs.len() as u64 * GEN_BYTES)
1229 }
1230}
1231
1232impl InventoryBackend for Fake {
1233 fn devices(&self) -> Result<Vec<DeviceInfo>, Outcome> {
1234 let w = self.w();
1236 Ok(w.devices.iter().map(|(id, d)| DeviceInfo { id: id.clone(), driver: d.clone() }).collect())
1237 }
1238
1239 fn resources(&self) -> Result<Resources, Outcome> {
1240 Ok(Resources { mem_total_mb: 16_000, mem_available_mb: 12_000, fs_free_bytes: BTreeMap::new() })
1241 }
1242
1243 fn bound_driver(&self, device: &UnitId) -> Result<Option<DriverRef>, Outcome> {
1244 Ok(self.w().devices.get(device).cloned().flatten())
1245 }
1246
1247 fn bind(&self, device: &UnitId, driver: &DriverRef, op: &OpId) -> Outcome {
1248 self.run(op, format!("bind({device}, {})", driver.name), |w| {
1249 w.devices.insert(device.clone(), Some(driver.clone()));
1250 Outcome::Succeeded
1251 })
1252 }
1253
1254 fn unbind(&self, device: &UnitId, op: &OpId) -> Outcome {
1255 self.run(op, format!("unbind({device})"), |w| {
1256 w.devices.insert(device.clone(), None);
1257 Outcome::Succeeded
1258 })
1259 }
1260}
1261
1262impl ProbeBackend for Fake {
1263 fn run_batch(&self, jobs: &[(UnitId, ProbeDef)]) -> Vec<ProbeResult> {
1264 let mut w = self.w();
1265 w.probe_batches += 1;
1266 let slow = w.probe_batch_ms;
1267 w.now = Timestamp(w.now.0 + slow);
1268 jobs.iter()
1269 .map(|(unit, p)| {
1270 if let ProbeKind::FreeSpace { path, min_bytes, min_permille } = &p.kind {
1271 return match w.fs.get(path) {
1272 Some((free, size)) if *free >= *min_bytes && free * 1000 >= size * *min_permille as u64 => {
1273 ProbeResult::Pass
1274 }
1275 Some((free, _)) => ProbeResult::Fail(format!("{free} bytes free on {path}")),
1276 None => ProbeResult::Error(format!("no filesystem at {path}")),
1277 };
1278 }
1279 if w.blocked.contains(unit) {
1280 return ProbeResult::Fail("connection refused".into());
1281 }
1282 if p.id.0 == "pipewire-has-a-card" {
1284 return match w.devices.get(&UnitId::device("snd-hda0")).cloned().flatten() {
1285 Some(d) if !w.bad_drivers.contains(&d.name) => ProbeResult::Pass,
1286 _ => ProbeResult::Fail("no sound card".into()),
1287 };
1288 }
1289 if unit.kind == UnitKind::Device && matches!(p.kind, ProbeKind::Exec { .. }) {
1291 return match w.devices.get(unit).cloned().flatten() {
1292 Some(d) if !w.bad_drivers.contains(&d.name) => ProbeResult::Pass,
1293 Some(d) => ProbeResult::Fail(format!("{} cannot drive it", d.name)),
1294 None => ProbeResult::Fail("no driver".into()),
1295 };
1296 }
1297 if w.base_units.get(unit).and_then(|d| d.runtime_repair.as_deref()) == Some("wifi.network") {
1299 return match w.runtime.get("wifi.network") {
1300 Some(Value::Str(n)) if w.wifi_in_range.contains(n) => ProbeResult::Pass,
1301 _ => ProbeResult::Fail("not associated".into()),
1302 };
1303 }
1304 let unreachable = |u: &UnitId| {
1305 u.name.strip_prefix("external:").is_some_and(|x| w.external_unreachable.contains(x))
1306 || w.base_units.get(u).is_some_and(|d| {
1307 d.edges.iter().any(|e| {
1308 e.to.name.strip_prefix("external:").is_some_and(|x| w.external_unreachable.contains(x))
1309 })
1310 })
1311 };
1312 if unreachable(unit) {
1313 return ProbeResult::Timeout;
1314 }
1315 if let Some(ext) = unit.name.strip_prefix("external:") {
1316 return if w.external_down.contains(ext) {
1317 ProbeResult::Fail(format!("{ext}: connection refused"))
1318 } else {
1319 ProbeResult::Pass
1320 };
1321 }
1322 if w.base_units.get(unit).is_some_and(|d| {
1324 d.edges
1325 .iter()
1326 .any(|e| e.to.name.strip_prefix("external:").is_some_and(|x| w.external_down.contains(x)))
1327 }) {
1328 return ProbeResult::Fail("upstream unavailable (502)".into());
1329 }
1330 match w.units.get(&World::key(unit)) {
1331 None => ProbeResult::Fail("not running".into()),
1332 Some(sim) if sim.observed != Observed::Active => ProbeResult::Fail("connection refused".into()),
1333 Some(sim) => match (&sim.health, &p.kind) {
1334 (Health::Ok, _) => ProbeResult::Pass,
1335 (Health::Http503, ProbeKind::Http { .. }) => ProbeResult::Fail("HTTP 503".into()),
1336 (Health::Http503, _) => ProbeResult::Pass,
1337 (Health::Delay(d), ProbeKind::Http { .. }) if d.0 > p.timeout.0 => ProbeResult::Timeout,
1339 (Health::Delay(_), _) => ProbeResult::Pass,
1340 (Health::Stall(d), _) if d.0 > p.timeout.0 => ProbeResult::Timeout,
1341 (Health::Stall(_), _) => ProbeResult::Pass,
1342 },
1343 }
1344 })
1345 .collect()
1346 }
1347}
1348
1349impl SessionBackend for Fake {
1350 fn sessions(&self) -> Result<Vec<SessionInfo>, Outcome> {
1351 Ok(self.w().sessions.clone())
1352 }
1353
1354 fn compositor_running(&self, s: &SessionInfo) -> Option<bool> {
1355 self.w().compositors.get(&s.id).copied()
1356 }
1357
1358 fn foreground(&self) -> Vec<String> {
1359 self.w().foreground.iter().cloned().collect()
1360 }
1361
1362 fn power(&self) -> Option<PowerSource> {
1363 self.w().power
1364 }
1365}
1366
1367impl RuntimeOptionBackend for Fake {
1368 fn get(&self, path: &str) -> Option<Value> {
1369 self.w().runtime.get(path).cloned()
1370 }
1371
1372 fn set(&self, path: &str, value: &Value, op: &OpId) -> Outcome {
1373 self.run(op, format!("set({path})"), |w| {
1374 w.runtime.insert(path.into(), value.clone());
1375 Outcome::Succeeded
1376 })
1377 }
1378
1379 fn alternatives(&self, path: &str) -> Vec<Value> {
1380 let w = self.w();
1381 match path {
1382 "wifi.network" => {
1383 let current = w.runtime.get(path);
1384 w.wifi_known
1385 .iter()
1386 .filter(|n| w.wifi_in_range.contains(*n))
1387 .map(|n| Value::Str(n.clone()))
1388 .filter(|v| Some(v) != current)
1389 .collect()
1390 }
1391 _ => vec![],
1392 }
1393 }
1394}
1395
1396impl ProcessBackend for Fake {
1397 fn kill_group(&self, pid: u32) -> bool {
1398 let mut w = self.w();
1399 if w.unkillable.contains(&pid) {
1400 return false;
1401 }
1402 w.alive.remove(&pid);
1403 true
1404 }
1405
1406 fn alive(&self, pid: u32) -> bool {
1407 self.w().alive.contains(&pid)
1408 }
1409}
1410
1411impl Clock for Fake {
1412 fn now(&self) -> Timestamp {
1415 let w = self.w();
1416 Timestamp(w.now.0.saturating_add_signed(w.wall_skew))
1417 }
1418
1419 fn boot_now(&self) -> Timestamp {
1420 self.w().now
1421 }
1422
1423 fn sleep(&self, d: Millis) {
1424 self.advance(d);
1425 }
1426}
1427
1428impl Backends for Fake {
1429 type Units = Fake;
1430 type Gens = Fake;
1431 type Inv = Fake;
1432 type Probes = Fake;
1433 type Sessions = Fake;
1434 type Runtime = Fake;
1435 type Procs = Fake;
1436 type Clock = Fake;
1437
1438 fn units(&self) -> &Fake {
1439 self
1440 }
1441 fn generations(&self) -> &Fake {
1442 self
1443 }
1444 fn inventory(&self) -> &Fake {
1445 self
1446 }
1447 fn probes(&self) -> &Fake {
1448 self
1449 }
1450 fn sessions(&self) -> &Fake {
1451 self
1452 }
1453 fn runtime(&self) -> &Fake {
1454 self
1455 }
1456 fn processes(&self) -> &Fake {
1457 self
1458 }
1459 fn clock(&self) -> &Fake {
1460 self
1461 }
1462}
1463
1464pub fn gym_world(store: MemStore) -> Fake {
1467 let mut w = World { now: Timestamp(1_700_000_000_000), power: Some(PowerSource::Ac), ..Default::default() };
1468 w.base_units = fixtures::gym_units();
1469 w.base_user_units = fixtures::gym_user_units();
1470 w.base_options = [
1471 ("services.gym-web.listenAddress".to_string(), Value::Str("127.0.0.1".into())),
1472 ("services.openssh.enable".to_string(), Value::Bool(true)),
1473 ("planarian.reference.fontScale".to_string(), Value::Str("1".into())),
1474 ("gym.vulnlib".to_string(), Value::Str("1.0".into())),
1475 ("gym.kernelParam".to_string(), Value::Str("".into())),
1476 ("services.gym-bad-payload.enable".to_string(), Value::Bool(false)),
1477 ("gym.modules".to_string(), Value::Str("old".into())),
1478 ]
1479 .into();
1480 w.base_packages = [("gym-vulnlib".to_string(), "1.0".to_string())].into();
1482 w.base_insecure = ["gym-vulnlib".to_string()].into();
1483 w.base_inputs = [("gym-modules".to_string(), ("old".to_string(), "/nix/store/aaa-gym-modules".to_string()))].into();
1484 w.rules = vec![
1485 Rule {
1487 option: "gym.modules".into(),
1488 value: Value::Str("new".into()),
1489 effect: Effect::Declare {
1490 service: "gym-bad-payload".into(),
1491 file: "/nix/store/bbb-gym-modules/module.nix".into(),
1492 },
1493 },
1494 Rule {
1497 option: "services.gym-bad-payload.enable".into(),
1498 value: Value::Bool(true),
1499 effect: Effect::Declare { service: "gym-bad-payload".into(), file: "/etc/nixos/configuration.nix".into() },
1500 },
1501 Rule {
1503 option: "gym.kernelParam".into(),
1504 value: Value::Str("virtio_gpu.modeset=0".into()),
1505 effect: Effect::NeedsReboot,
1506 },
1507 Rule { option: "gym.touchCore".into(), value: Value::Str("on".into()), effect: Effect::TouchCore },
1510 Rule {
1511 option: "gym.dropProbe".into(),
1512 value: Value::Str("sshd-tcp".into()),
1513 effect: Effect::DropProbe(UnitId::service("sshd"), "sshd-tcp".into()),
1514 },
1515 Rule {
1516 option: "gym.vulnlib".into(),
1517 value: Value::Str("1.1".into()),
1518 effect: Effect::Package { name: "gym-vulnlib".into(), version: "1.1".into(), insecure: false },
1519 },
1520 Rule {
1523 option: "gym.vulnlib".into(),
1524 value: Value::Str("1.2".into()),
1525 effect: Effect::Package { name: "gym-vulnlib".into(), version: "1.2".into(), insecure: false },
1526 },
1527 Rule { option: "gym.vulnlib".into(), value: Value::Str("1.2".into()), effect: Effect::NeedsReboot },
1528 Rule {
1531 option: "gym.newlib".into(),
1532 value: Value::Str("1.0".into()),
1533 effect: Effect::Package { name: "gym-newlib".into(), version: "1.0".into(), insecure: false },
1534 },
1535 Rule {
1536 option: "gym.newlib".into(),
1537 value: Value::Str("2.0".into()),
1538 effect: Effect::Package { name: "gym-newlib".into(), version: "2.0".into(), insecure: true },
1539 },
1540 Rule {
1541 option: "services.gym-web.listenAddress".into(),
1542 value: Value::Str("192.0.2.10".into()),
1543 effect: Effect::Break(UnitId::service("gym-web")),
1544 },
1545 Rule {
1546 option: "services.openssh.enable".into(),
1547 value: Value::Bool(false),
1548 effect: Effect::Remove(UnitId::service("sshd")),
1549 },
1550 ];
1551 w.core_units = [
1552 ("planariad.service".to_string(), Hash::of(b"planariad")),
1553 ("planarian-recovery.service".to_string(), Hash::of(b"recovery")),
1554 ]
1555 .into();
1556 w.owner_files = [
1557 ("flake.nix".to_string(), "{ outputs = …; }".to_string()),
1558 ("configuration.nix".to_string(), "{ … }".to_string()),
1559 ]
1560 .into();
1561 w.devices = [
1562 (UnitId::device("snd-hda0"), Some(DriverRef { name: "snd_hda_intel".into(), module: None })),
1563 (UnitId::device("net-gym0"), Some(DriverRef { name: "virtio_net".into(), module: None })),
1564 (UnitId::device("gpu0"), Some(DriverRef { name: "virtio_gpu".into(), module: None })),
1565 (UnitId::device("wlan-hwsim"), Some(DriverRef { name: "mac80211_hwsim".into(), module: None })),
1566 ]
1567 .into();
1568 w.wifi_known = vec!["gym-ap-a".into(), "gym-ap-b".into()];
1569 w.wifi_in_range = ["gym-ap-a".to_string(), "gym-ap-b".to_string()].into();
1570 w.runtime.insert("wifi.network".into(), Value::Str("gym-ap-a".into()));
1571 w.bad_drivers.insert("vfio-pci".into());
1572 w.fs = [
1573 ("/".to_string(), (40_000_000_000, 100_000_000_000)),
1574 ("/nix".to_string(), (40_000_000_000, 100_000_000_000)),
1575 ("/boot".to_string(), (400_000_000, 1_000_000_000)),
1576 ("/var/lib/planarian".to_string(), (40_000_000_000, 100_000_000_000)),
1577 ]
1578 .into();
1579 let g = w.add_generation(&OverrideSet::default());
1580 w.boot_entries.insert(g);
1581 w.boot_default = g;
1582 w.booted = g;
1583 w.override_store.insert(World::override_hash_of(&OverrideSet::default()), OverrideSet::default());
1584 let now = w.now;
1585 for id in w.base_units.keys() {
1586 if id.kind == UnitKind::Service && !id.name.starts_with("external:") {
1587 w.units.insert(id.clone(), UnitSim::active(now));
1588 }
1589 }
1590 if let Some(sim) = w.units.get_mut(&UnitId::service("gym-restartful")) {
1591 sim.systemd_restart = Some(Millis::secs(5));
1592 }
1593 w.apply_generation(g);
1594 Fake::new(w, store)
1595}
1596
1597pub mod fixtures;