diff --git a/crates/mind-body/src/cli.rs b/crates/mind-body/src/cli.rs index 35b048b..9087478 100644 --- a/crates/mind-body/src/cli.rs +++ b/crates/mind-body/src/cli.rs @@ -28,12 +28,12 @@ use std::path::Path; use anyhow::{Result, anyhow, bail}; use chrono::{DateTime, Utc}; -use eureka_pipeline::{RunTurn, Slug}; +use eureka_pipeline::Slug; use crate::control::{ ControlWriter, Effective, FileSource, SwarmWriter, control_path, derive, derive_swarm, load_state, load_swarm_state, swarm_path, }; -use crate::launch::{StopScope, unit}; +use crate::launch::{StopScope, stop_args}; const USAGE: &str = "usage: mind-control [--root ] --instance show | check | brake hold | brake release | mind-control [--root ] swarm show | brake hold | brake release"; @@ -110,7 +110,7 @@ pub fn run( let stopped = stop_units(StopScope::Mind(&instance)); match (written, stopped) { (Ok(()), Ok(())) => { - writeln!(out, "units: stopped {} {}", unit(&instance, RunTurn::SelfRun)?, unit(&instance, RunTurn::PersonaTurn)?)?; + writeln!(out, "units: stopped {}", stop_args(StopScope::Mind(&instance))?[2..].join(" "))?; Ok(()) } outcome => held(outcome), @@ -151,7 +151,7 @@ fn swarm( let written = SwarmWriter::open(&path).and_then(|mut writer| writer.set_brake(false, now, set_by)); let stopped = stop_units(StopScope::Swarm); if written.is_ok() && stopped.is_ok() { - writeln!(out, "units: stopped every mind-self@ and mind-persona@ unit")?; + writeln!(out, "units: stopped every mind-self@, mind-persona@ and mind-stream@ unit")?; } held((written, stopped)) } diff --git a/crates/mind-body/src/launch.rs b/crates/mind-body/src/launch.rs index b954134..281815e 100644 --- a/crates/mind-body/src/launch.rs +++ b/crates/mind-body/src/launch.rs @@ -124,15 +124,23 @@ fn template(turn: RunTurn) -> &'static str { } } -/// The unit of one turn: the template by turn, then the instance, held to the -/// leaf's grammar first so it cannot escape the name. The error names nothing -/// of the value. Start and alive both name the unit here. -pub(crate) fn unit(instance: &Slug, turn: RunTurn) -> Result { +/// The stream's unit template: not a run slot, so only a brake hold names it. +const STREAM_TEMPLATE: &str = "mind-stream"; + +/// A template's unit for one instance, the instance held to the leaf's grammar +/// first so it cannot escape the name. The error names nothing of the value. +fn named(template: &str, instance: &Slug) -> Result { instance.validate_slug().map_err(|_| anyhow!("the instance is not a unit name"))?; - Ok(format!("{}@{}.service", template(turn), instance.0)) + Ok(format!("{template}@{}.service", instance.0)) } -/// What one stop reaches: the two units of one Mind, or every Mind's. +/// The unit of one turn: the template by turn, then the instance. Start and +/// alive both name the unit here. +pub(crate) fn unit(instance: &Slug, turn: RunTurn) -> Result { + named(template(turn), instance) +} + +/// What one stop reaches: the three units of one Mind, or every Mind's. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StopScope<'a> { Mind(&'a Slug), @@ -141,22 +149,25 @@ pub enum StopScope<'a> { /// The arguments of the one synchronous stop. A Mind's: the exact unit `unit()` /// names for each turn of this instance, the same names start and alive use, -/// with no glob (a prefix glob also matches another instance, `eureka*` matches +/// and its stream's, with no glob (a prefix glob also matches another instance, `eureka*` matches /// `eureka-two`). The swarm's: each template's glob over the loaded units, /// which is exactly every running Mind and exits 0 when none is loaded. No /// `--no-block`: `systemctl stop` returns when the units are down. The /// instance is validated by `unit()`. pub(crate) fn stop_args(scope: StopScope<'_>) -> Result> { let mut args = vec!["stop".to_string(), "--".to_string()]; - for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] { - args.push(match scope { - StopScope::Mind(instance) => unit(instance, turn)?, - StopScope::Swarm => format!("{}@*.service", template(turn)), - }); - } + args.extend(held_templates().into_iter().map(|template| match scope { + StopScope::Mind(instance) => named(template, instance), + StopScope::Swarm => Ok(format!("{template}@*.service")), + }).collect::>>()?); Ok(args) } +/// The templates a hold stops: both run slots and the stream. +fn held_templates() -> [&'static str; 3] { + [template(RunTurn::SelfRun), template(RunTurn::PersonaTurn), STREAM_TEMPLATE] +} + impl SystemdLauncher { /// Stops the units of one Mind, or of every Mind, and waits for them. An /// inherent method, not on `Launcher`: only a brake hold calls it, through @@ -173,10 +184,10 @@ impl SystemdLauncher { } let states = match scope { StopScope::Mind(instance) => { - let states = [RunTurn::SelfRun, RunTurn::PersonaTurn] + let states = held_templates() .into_iter() - .map(|turn| { - let unit = unit(instance, turn)?; + .map(|template| { + let unit = named(template, instance)?; let state = self.unit_state(&unit); Ok(format!("{unit} is {state}")) }) @@ -1486,10 +1497,12 @@ mod tests { launcher.start(&instance, turn).unwrap(); started.push(spy.calls.borrow().last().unwrap().last().unwrap().clone()); } - assert_eq!(args[2..], started[..], "stop names exactly the units start names, for both turns"); + assert_eq!(args[2..4], started[..], "stop names exactly the units start names, for both turns"); + assert_eq!(args[4], format!("mind-stream@{name}.service"), "and the stream of that instance"); + assert_eq!(args.len(), 5); assert!(args[2..].iter().all(|unit| !unit.contains(['*', '?', '['])), "a glob can match another instance or none: {args:?}"); } - assert_eq!(stop_args(StopScope::Mind(&eureka())).unwrap(), ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service"]); + assert_eq!(stop_args(StopScope::Mind(&eureka())).unwrap(), ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service", "mind-stream@eureka.service"]); } #[test] @@ -1503,7 +1516,7 @@ mod tests { fn the_swarm_stop_is_one_stop_of_both_templates_globs() { let (launcher, spy) = spied(); launcher.stop(StopScope::Swarm).unwrap(); - assert_eq!(*spy.calls.borrow(), vec![vec!["stop", "--", "mind-self@*.service", "mind-persona@*.service"]]); + assert_eq!(*spy.calls.borrow(), vec![vec!["stop", "--", "mind-self@*.service", "mind-persona@*.service", "mind-stream@*.service"]]); } #[test] @@ -1535,12 +1548,12 @@ mod tests { let (launcher, spy) = spied(); spy.success.set(false); spy.code.set(Some(5)); - spy.states.borrow_mut().extend([("mind-self@eureka.service", "inactive\n"), ("mind-persona@eureka.service", "active\n")]); + spy.states.borrow_mut().extend([("mind-self@eureka.service", "inactive\n"), ("mind-persona@eureka.service", "active\n"), ("mind-stream@eureka.service", "failed\n")]); let shown = format!("{:#}", launcher.stop(StopScope::Mind(&eureka())).unwrap_err()); assert!(shown.contains("exit status 5"), "{shown}"); - assert!(shown.contains("mind-self@eureka.service is inactive") && shown.contains("mind-persona@eureka.service is active"), "{shown}"); + assert!(shown.contains("mind-self@eureka.service is inactive") && shown.contains("mind-persona@eureka.service is active") && shown.contains("mind-stream@eureka.service is failed"), "{shown}"); let asked: Vec> = spy.calls.borrow().iter().skip(1).cloned().collect(); - assert_eq!(asked, [["is-active", "mind-self@eureka.service"], ["is-active", "mind-persona@eureka.service"]], "one query per unit, after the one stop"); + assert_eq!(asked, [["is-active", "mind-self@eureka.service"], ["is-active", "mind-persona@eureka.service"], ["is-active", "mind-stream@eureka.service"]], "one query per unit, after the one stop"); // A state word outside the three, and anything systemctl prints with it, is not carried. spy.states.borrow_mut().clear(); *spy.stdout.borrow_mut() = "canary-output-7f3a91\n".to_string(); diff --git a/crates/mind-body/tests/cli.rs b/crates/mind-body/tests/cli.rs index fa838a4..f23495f 100644 --- a/crates/mind-body/tests/cli.rs +++ b/crates/mind-body/tests/cli.rs @@ -275,7 +275,7 @@ fn brake_hold_stops_once_with_the_instance_and_the_store_already_held() { result.unwrap(); assert_eq!(calls, [Some("eureka".to_string())]); assert!(seen_at_stop.contains("BrakeHeld"), "the write comes before the stop: {seen_at_stop}"); - assert!(out.contains("units: stopped mind-self@eureka.service mind-persona@eureka.service"), "{out}"); + assert!(out.contains("units: stopped mind-self@eureka.service mind-persona@eureka.service mind-stream@eureka.service"), "{out}"); } #[test] @@ -362,8 +362,8 @@ fn hold_through_the_binary(exit: i32) -> (std::process::Output, Vec, tem fn the_binary_hold_runs_systemctl_stop_on_exactly_the_units_it_starts() { let (output, recorded, _root) = hold_through_the_binary(0); assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr)); - assert_eq!(recorded, ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service"], "a stop that succeeds asks no state"); - assert!(String::from_utf8_lossy(&output.stdout).contains("units: stopped mind-self@eureka.service mind-persona@eureka.service")); + assert_eq!(recorded, ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service", "mind-stream@eureka.service"], "a stop that succeeds asks no state"); + assert!(String::from_utf8_lossy(&output.stdout).contains("units: stopped mind-self@eureka.service mind-persona@eureka.service mind-stream@eureka.service")); } #[cfg(unix)] @@ -373,11 +373,11 @@ fn a_partial_stop_names_each_units_state_and_keeps_the_brake_held_with_exit_2() assert_eq!(output.status.code(), Some(2)); assert_eq!( recorded, - ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service", "is-active", "mind-self@eureka.service", "is-active", "mind-persona@eureka.service"] + ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service", "mind-stream@eureka.service", "is-active", "mind-self@eureka.service", "is-active", "mind-persona@eureka.service", "is-active", "mind-stream@eureka.service"] ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("exit status 5") && stderr.contains("the brake is held"), "{stderr}"); - assert!(stderr.contains("mind-self@eureka.service is inactive") && stderr.contains("mind-persona@eureka.service is active"), "{stderr}"); + assert!(stderr.contains("mind-self@eureka.service is inactive") && stderr.contains("mind-persona@eureka.service is active") && stderr.contains("mind-stream@eureka.service is active"), "{stderr}"); assert!(!stderr.contains("were not stopped"), "{stderr}"); assert!(!String::from_utf8_lossy(&output.stdout).contains("stopped")); assert!(shown(root.path()).contains("BrakeHeld")); @@ -475,7 +475,7 @@ fn swarm_brake_hold_stops_once_with_the_swarm_scope_and_the_swarm_store_already_ result.unwrap(); assert_eq!(calls, [None], "one stop, for the swarm"); assert!(seen_at_stop.contains("BrakeHeld"), "the write comes before the stop: {seen_at_stop}"); - assert!(out.contains("units: stopped every mind-self@ and mind-persona@ unit"), "{out}"); + assert!(out.contains("units: stopped every mind-self@, mind-persona@ and mind-stream@ unit"), "{out}"); } #[test]