mind-control: swarm brake hold stops every Mind's units, through the one stopper
stop_all becomes SystemdLauncher::stop(StopScope): a Mind's two exact units, or the swarm's mind-self@*.service and mind-persona@*.service globs. Both holds write first, then stop whatever the write's outcome, with the same four outcomes. Soul s1 swarm-hold-leaves-live-turn-running: a held swarm brake left a live Persona turn running with its shell until the turn ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
14e7838813
commit
edfbf9ce25
5 changed files with 184 additions and 68 deletions
|
|
@ -8,18 +8,19 @@
|
|||
//! mind-control --instance <slug> brake release
|
||||
//! mind-control [--root <dir>] --instance <slug> check
|
||||
//! mind-control [--root <dir>] swarm show
|
||||
//! mind-control swarm brake hold | release
|
||||
//! mind-control swarm brake hold | release (hold also stops every Mind's units)
|
||||
//! ```
|
||||
//!
|
||||
//! The `swarm` verbs write `<root>/swarm.cc` and stop no unit: the swarm's
|
||||
//! brake is read by the organs that act on the swarm's behalf.
|
||||
//! The `swarm` verbs write `<root>/swarm.cc`; `swarm brake hold` also stops
|
||||
//! every Mind's units, and `swarm brake release` stops nothing.
|
||||
//!
|
||||
//! `check` exits 0 only when the instance brake and the swarm brake are released; a
|
||||
//! systemd unit runs it as its `ExecCondition`. It writes nothing.
|
||||
//!
|
||||
//! `brake hold` writes the brake held and then, whatever the write's outcome,
|
||||
//! stops the instance's running units through the `stop_units` parameter. It
|
||||
//! fails if either step failed. No other command stops anything.
|
||||
//! Either `brake hold` writes its brake held and then, whatever the write's
|
||||
//! outcome, stops the units it covers through the one `stop_units` parameter
|
||||
//! (a Mind's two units, or every Mind's). It fails if either step failed. No
|
||||
//! other command stops anything.
|
||||
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
|
|
@ -32,7 +33,7 @@ use eureka_pipeline::{RunTurn, Slug};
|
|||
use crate::control::{
|
||||
ControlWriter, Effective, FileSource, SwarmWriter, control_path, derive, derive_swarm, load_state, load_swarm_state, swarm_path,
|
||||
};
|
||||
use crate::launch::unit;
|
||||
use crate::launch::{StopScope, unit};
|
||||
|
||||
const USAGE: &str =
|
||||
"usage: mind-control [--root <dir>] --instance <slug> show | check | brake hold | brake release | mind-control [--root <dir>] swarm show | brake hold | brake release";
|
||||
|
|
@ -77,14 +78,14 @@ pub fn operator_name(sudo_user: Option<String>, user: Option<String>) -> String
|
|||
/// default; a leading `--root <dir>` argument replaces it (argv, not environment,
|
||||
/// because `sudo -E` carries the environment). The operator's name, the clock, the output and the stopper are parameters so a test drives
|
||||
/// the whole command without touching `/etc` or systemd. `stop_units` is
|
||||
/// called only by `brake hold`, after the write, whatever the write's outcome.
|
||||
/// called only by a `brake hold`, after the write, whatever the write's outcome.
|
||||
pub fn run(
|
||||
args: &[String],
|
||||
root: &Path,
|
||||
set_by: &str,
|
||||
now: DateTime<Utc>,
|
||||
out: &mut impl Write,
|
||||
stop_units: &mut dyn FnMut(&Slug) -> Result<()>,
|
||||
stop_units: &mut dyn FnMut(StopScope<'_>) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let (root, args) = match args {
|
||||
[flag, dir, rest @ ..] if flag == "--root" => (Path::new(dir), rest),
|
||||
|
|
@ -93,7 +94,7 @@ pub fn run(
|
|||
if let [word, rest @ ..] = args
|
||||
&& word == "swarm"
|
||||
{
|
||||
return swarm(root, rest, set_by, now, out);
|
||||
return swarm(root, rest, set_by, now, out, stop_units);
|
||||
}
|
||||
let (instance, command) = match args {
|
||||
[flag, instance, command @ ..] if flag == "--instance" => (Slug(instance.clone()), command),
|
||||
|
|
@ -106,15 +107,13 @@ pub fn run(
|
|||
["check"] => check(root, &path, &instance, out),
|
||||
["brake", "hold"] => {
|
||||
let written = ControlWriter::open(&path).and_then(|mut writer| writer.set_brake(false, now, set_by));
|
||||
let stopped = stop_units(&instance);
|
||||
let stopped = stop_units(StopScope::Mind(&instance));
|
||||
match (written, stopped) {
|
||||
(Ok(()), Ok(())) => {
|
||||
writeln!(out, "units: stopped {} {}", unit(&instance, RunTurn::SelfRun)?, unit(&instance, RunTurn::PersonaTurn)?)?;
|
||||
Ok(())
|
||||
}
|
||||
(Err(write), Ok(())) => Err(write.context("the brake was not written; the units were stopped")),
|
||||
(Ok(()), Err(stop)) => Err(stop.context("the brake is held; the stop failed")),
|
||||
(Err(write), Err(stop)) => Err(anyhow!("the brake was not written ({write:#}) and the stop failed ({stop:#})")),
|
||||
outcome => held(outcome),
|
||||
}
|
||||
}
|
||||
["brake", "release"] => ControlWriter::open(&path)?.set_brake(true, now, set_by),
|
||||
|
|
@ -122,14 +121,40 @@ pub fn run(
|
|||
}
|
||||
}
|
||||
|
||||
/// A hold whose write or stop failed: what was written, what was stopped, and
|
||||
/// which step failed, the same words for a Mind's hold and the swarm's.
|
||||
fn held(outcome: (Result<()>, Result<()>)) -> Result<()> {
|
||||
match outcome {
|
||||
(Ok(()), Ok(())) => Ok(()),
|
||||
(Err(write), Ok(())) => Err(write.context("the brake was not written; the units were stopped")),
|
||||
(Ok(()), Err(stop)) => Err(stop.context("the brake is held; the stop failed")),
|
||||
(Err(write), Err(stop)) => Err(anyhow!("the brake was not written ({write:#}) and the stop failed ({stop:#})")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `swarm` verbs over `<root>/swarm.cc`. Every refusal happens before the
|
||||
/// writer is opened, so a refused command creates nothing.
|
||||
fn swarm(root: &Path, args: &[String], set_by: &str, now: DateTime<Utc>, out: &mut impl Write) -> Result<()> {
|
||||
/// writer is opened, so a refused command creates nothing. `brake hold` then
|
||||
/// stops every Mind's units, whatever the write's outcome.
|
||||
fn swarm(
|
||||
root: &Path,
|
||||
args: &[String],
|
||||
set_by: &str,
|
||||
now: DateTime<Utc>,
|
||||
out: &mut impl Write,
|
||||
stop_units: &mut dyn FnMut(StopScope<'_>) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let path = swarm_path(root);
|
||||
let words: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
match words.as_slice() {
|
||||
["show"] => show_swarm(&path, out),
|
||||
["brake", "hold"] => SwarmWriter::open(&path)?.set_brake(false, now, set_by),
|
||||
["brake", "hold"] => {
|
||||
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")?;
|
||||
}
|
||||
held((written, stopped))
|
||||
}
|
||||
["brake", "release"] => SwarmWriter::open(&path)?.set_brake(true, now, set_by),
|
||||
_ => bail!("{USAGE}"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
//! Persona turn's run opens with no claims, so a request that carries any is
|
||||
//! refused. The primitive
|
||||
//! owns no file and stops no unit through the `Launcher` port: stopping her
|
||||
//! units is the operator's brake hold, through `SystemdLauncher::stop_all`.
|
||||
//! units is either brake's hold (one Mind's, or the swarm's for every Mind),
|
||||
//! through `SystemdLauncher::stop`.
|
||||
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
|
|
@ -125,45 +126,63 @@ pub(crate) fn unit(instance: &Slug, turn: RunTurn) -> Result<String> {
|
|||
Ok(format!("{}@{}.service", template(turn), instance.0))
|
||||
}
|
||||
|
||||
/// The arguments of the one synchronous stop: the exact unit `unit()` names
|
||||
/// for each turn of this instance, the same names start and alive use. No
|
||||
/// glob: a prefix glob also matches another instance (`eureka*` matches
|
||||
/// `eureka-two`) and a glob that matches no loaded unit exits 0. No
|
||||
/// What one stop reaches: the two units of one Mind, or every Mind's.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum StopScope<'a> {
|
||||
Mind(&'a Slug),
|
||||
Swarm,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// `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(instance: &Slug) -> Result<Vec<String>> {
|
||||
pub(crate) fn stop_args(scope: StopScope<'_>) -> Result<Vec<String>> {
|
||||
let mut args = vec!["stop".to_string(), "--".to_string()];
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
args.push(unit(instance, turn)?);
|
||||
args.push(match scope {
|
||||
StopScope::Mind(instance) => unit(instance, turn)?,
|
||||
StopScope::Swarm => format!("{}@*.service", template(turn)),
|
||||
});
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
impl SystemdLauncher {
|
||||
/// Stops every unit of both turns of one instance and waits for them. An
|
||||
/// inherent method, not on `Launcher`: only the operator's brake hold
|
||||
/// calls it. A unit that is not installed is systemctl's exit 5, and the
|
||||
/// others still stop; when the stop fails the error names each unit's
|
||||
/// state, because systemd may have stopped one and not the other.
|
||||
pub fn stop_all(&self, instance: &Slug) -> Result<()> {
|
||||
let args = stop_args(instance)?;
|
||||
/// 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
|
||||
/// `mind-control`. A unit that is not installed is systemctl's exit 5, and
|
||||
/// the others still stop; when a Mind's stop fails the error names each
|
||||
/// unit's state, because systemd may have stopped one and not the other,
|
||||
/// and a failed swarm stop names systemctl's exit status only.
|
||||
pub fn stop(&self, scope: StopScope<'_>) -> Result<()> {
|
||||
let args = stop_args(scope)?;
|
||||
let args: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
let reply = self.systemctl.run(&args)?;
|
||||
if reply.success {
|
||||
return Ok(());
|
||||
}
|
||||
let states = [RunTurn::SelfRun, RunTurn::PersonaTurn]
|
||||
.into_iter()
|
||||
.map(|turn| {
|
||||
let unit = unit(instance, turn)?;
|
||||
let state = self.unit_state(&unit);
|
||||
Ok(format!("{unit} is {state}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.join(", ");
|
||||
let states = match scope {
|
||||
StopScope::Mind(instance) => {
|
||||
let states = [RunTurn::SelfRun, RunTurn::PersonaTurn]
|
||||
.into_iter()
|
||||
.map(|turn| {
|
||||
let unit = unit(instance, turn)?;
|
||||
let state = self.unit_state(&unit);
|
||||
Ok(format!("{unit} is {state}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.join(", ");
|
||||
format!("; {states}")
|
||||
}
|
||||
StopScope::Swarm => String::new(),
|
||||
};
|
||||
match reply.code {
|
||||
Some(code) => Err(anyhow!("systemctl stop failed with exit status {code}; {states}")),
|
||||
None => Err(anyhow!("systemctl stop was ended by a signal; {states}")),
|
||||
Some(code) => Err(anyhow!("systemctl stop failed with exit status {code}{states}")),
|
||||
None => Err(anyhow!("systemctl stop was ended by a signal{states}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1223,7 +1242,7 @@ mod tests {
|
|||
fn stop_args_is_one_synchronous_stop_of_exactly_the_units_start_names() {
|
||||
for name in ["eureka", "eureka.test", "eureka-two"] {
|
||||
let instance = Slug(name.into());
|
||||
let args = stop_args(&instance).unwrap();
|
||||
let args = stop_args(StopScope::Mind(&instance)).unwrap();
|
||||
assert_eq!(args[..2], ["stop", "--"], "one synchronous stop, no --no-block");
|
||||
let (launcher, spy) = spied();
|
||||
let mut started = Vec::new();
|
||||
|
|
@ -1234,21 +1253,42 @@ mod tests {
|
|||
assert_eq!(args[2..], started[..], "stop names exactly the units start names, for both turns");
|
||||
assert!(args[2..].iter().all(|unit| !unit.contains(['*', '?', '['])), "a glob can match another instance or none: {args:?}");
|
||||
}
|
||||
assert_eq!(stop_args(&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"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_all_runs_the_one_stop_and_waits() {
|
||||
fn stop_runs_the_one_stop_and_waits() {
|
||||
let (launcher, spy) = spied();
|
||||
launcher.stop_all(&eureka()).unwrap();
|
||||
assert_eq!(*spy.calls.borrow(), vec![stop_args(&eureka()).unwrap()]);
|
||||
launcher.stop(StopScope::Mind(&eureka())).unwrap();
|
||||
assert_eq!(*spy.calls.borrow(), vec![stop_args(StopScope::Mind(&eureka())).unwrap()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_all_with_an_invalid_instance_is_an_error_and_runs_no_systemctl() {
|
||||
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"]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_swarm_stop_names_the_exit_status_and_asks_no_state() {
|
||||
let (launcher, spy) = spied();
|
||||
spy.success.set(false);
|
||||
spy.code.set(Some(5));
|
||||
*spy.stdout.borrow_mut() = "canary-output-7f3a91
|
||||
".to_string();
|
||||
let shown = format!("{:#}", launcher.stop(StopScope::Swarm).unwrap_err());
|
||||
assert!(shown.contains("exit status 5") && !shown.contains("canary"), "{shown}");
|
||||
assert_eq!(spy.calls.borrow().len(), 1, "no per-unit query for the swarm");
|
||||
spy.code.set(None);
|
||||
assert!(format!("{:#}", launcher.stop(StopScope::Swarm).unwrap_err()).contains("signal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_with_an_invalid_instance_is_an_error_and_runs_no_systemctl() {
|
||||
let (launcher, spy) = spied();
|
||||
for bad in ["canary-../evil-7f3a91", "canary instance", "", "canary*"] {
|
||||
let error = launcher.stop_all(&Slug(bad.into())).unwrap_err();
|
||||
let error = launcher.stop(StopScope::Mind(&Slug(bad.into()))).unwrap_err();
|
||||
assert!(!format!("{error:#}").contains("canary"), "{error:#}");
|
||||
}
|
||||
assert!(spy.calls.borrow().is_empty());
|
||||
|
|
@ -1260,7 +1300,7 @@ mod tests {
|
|||
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")]);
|
||||
let shown = format!("{:#}", launcher.stop_all(&eureka()).unwrap_err());
|
||||
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}");
|
||||
let asked: Vec<Vec<String>> = spy.calls.borrow().iter().skip(1).cloned().collect();
|
||||
|
|
@ -1269,10 +1309,10 @@ mod tests {
|
|||
spy.states.borrow_mut().clear();
|
||||
*spy.stdout.borrow_mut() = "canary-output-7f3a91\n".to_string();
|
||||
spy.code.set(None);
|
||||
let shown = format!("{:#}", launcher.stop_all(&eureka()).unwrap_err());
|
||||
let shown = format!("{:#}", launcher.stop(StopScope::Mind(&eureka())).unwrap_err());
|
||||
assert!(shown.contains("signal") && shown.contains("is unknown") && !shown.contains("canary"), "{shown}");
|
||||
spy.states.borrow_mut().push(("mind-self@eureka.service", "failed\n"));
|
||||
let shown = format!("{:#}", launcher.stop_all(&eureka()).unwrap_err());
|
||||
let shown = format!("{:#}", launcher.stop(StopScope::Mind(&eureka())).unwrap_err());
|
||||
assert!(shown.contains("mind-self@eureka.service is failed"), "{shown}");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ fn main() {
|
|||
&set_by,
|
||||
chrono::Utc::now(),
|
||||
&mut std::io::stdout(),
|
||||
&mut |instance| mind_body::launch::SystemdLauncher::new().stop_all(instance),
|
||||
&mut |scope| mind_body::launch::SystemdLauncher::new().stop(scope),
|
||||
);
|
||||
if let Err(error) = result {
|
||||
eprintln!("mind-control: {error:#}");
|
||||
|
|
|
|||
|
|
@ -3,29 +3,34 @@
|
|||
//! status. Nothing touches `/etc`.
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use eureka_pipeline::Slug;
|
||||
use mind_body::cli::{operator_name, run};
|
||||
use mind_body::launch::StopScope;
|
||||
|
||||
fn go(root: &std::path::Path, args: &[&str], set_by: &str, secs: i64) -> (anyhow::Result<()>, String) {
|
||||
let (result, out, _) = go_stopping(root, args, set_by, secs, |_| Ok(()));
|
||||
(result, out)
|
||||
}
|
||||
|
||||
/// Runs one command with a stopper that records the instance of every call and
|
||||
/// answers with `stop`. `stop` also sees the call before it returns.
|
||||
/// Runs one command with a stopper that records every call (the instance of a
|
||||
/// Mind's stop, None for the swarm's) and answers with `stop`. `stop` also
|
||||
/// sees the call before it returns.
|
||||
fn go_stopping(
|
||||
root: &std::path::Path,
|
||||
args: &[&str],
|
||||
set_by: &str,
|
||||
secs: i64,
|
||||
mut stop: impl FnMut(&Slug) -> anyhow::Result<()>,
|
||||
) -> (anyhow::Result<()>, String, Vec<String>) {
|
||||
mut stop: impl FnMut(Option<&str>) -> anyhow::Result<()>,
|
||||
) -> (anyhow::Result<()>, String, Vec<Option<String>>) {
|
||||
let args: Vec<String> = args.iter().map(|a| a.to_string()).collect();
|
||||
let mut out = Vec::new();
|
||||
let mut calls = Vec::new();
|
||||
let now = Utc.timestamp_opt(1_800_000_000 + secs, 0).unwrap();
|
||||
let result = run(&args, root, set_by, now, &mut out, &mut |instance| {
|
||||
calls.push(instance.0.clone());
|
||||
let result = run(&args, root, set_by, now, &mut out, &mut |scope| {
|
||||
let instance = match scope {
|
||||
StopScope::Mind(instance) => Some(instance.0.as_str()),
|
||||
StopScope::Swarm => None,
|
||||
};
|
||||
calls.push(instance.map(str::to_string));
|
||||
stop(instance)
|
||||
});
|
||||
(result, String::from_utf8(out).unwrap(), calls)
|
||||
|
|
@ -268,7 +273,7 @@ fn brake_hold_stops_once_with_the_instance_and_the_store_already_held() {
|
|||
Ok(())
|
||||
});
|
||||
result.unwrap();
|
||||
assert_eq!(calls, ["eureka"]);
|
||||
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}");
|
||||
}
|
||||
|
|
@ -314,7 +319,7 @@ fn a_failing_write_still_stops_and_the_hold_fails() {
|
|||
std::fs::create_dir_all(root.path().join("eureka").join("control.cc")).unwrap();
|
||||
let (result, out, calls) = go_stopping(root.path(), &["--instance", "eureka", "brake", "hold"], "op", 1, |_| Ok(()));
|
||||
let error = format!("{:#}", result.unwrap_err());
|
||||
assert_eq!(calls, ["eureka"], "stopping does not wait on the write");
|
||||
assert_eq!(calls, [Some("eureka".to_string())], "stopping does not wait on the write");
|
||||
assert!(error.contains("brake was not written"), "{error}");
|
||||
assert!(!out.contains("stopped"), "{out}");
|
||||
|
||||
|
|
@ -451,15 +456,60 @@ fn swarm_verbs_round_trip() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_verbs_write_only_the_swarm_file_and_never_call_the_stopper() {
|
||||
fn swarm_verbs_write_only_the_swarm_file() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let (result, _, calls) = go_stopping(root.path(), &["swarm", "brake", "hold"], "op", 0, |_| Ok(()));
|
||||
result.unwrap();
|
||||
assert!(calls.is_empty(), "the swarm hold has no instance to stop: {calls:?}");
|
||||
go(root.path(), &["swarm", "brake", "hold"], "op", 0).0.unwrap();
|
||||
let names = listing(root.path());
|
||||
assert!(names.iter().all(|name| name.starts_with("swarm.cc")), "{names:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_brake_hold_stops_once_with_the_swarm_scope_and_the_swarm_store_already_held() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
go(root.path(), &["swarm", "brake", "release"], "op", 0).0.unwrap();
|
||||
let mut seen_at_stop = String::new();
|
||||
let (result, out, calls) = go_stopping(root.path(), &["swarm", "brake", "hold"], "alice", 1, |_| {
|
||||
seen_at_stop = swarm_show(root.path());
|
||||
Ok(())
|
||||
});
|
||||
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}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_swarm_stopper_fails_the_hold_and_leaves_it_held() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let (result, out, calls) =
|
||||
go_stopping(root.path(), &["swarm", "brake", "hold"], "op", 1, |_| Err(anyhow::anyhow!("systemctl stop failed with exit status 5")));
|
||||
let error = format!("{:#}", result.unwrap_err());
|
||||
assert_eq!(calls, [None]);
|
||||
assert!(error.contains("the brake is held") && error.contains("exit status 5"), "{error}");
|
||||
assert!(!out.contains("stopped"), "a failed stop printed success: {out}");
|
||||
assert!(swarm_show(root.path()).contains("BrakeHeld"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_swarm_write_still_stops_and_the_hold_fails() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(root.path().join("swarm.cc")).unwrap();
|
||||
let (result, out, calls) = go_stopping(root.path(), &["swarm", "brake", "hold"], "op", 1, |_| Ok(()));
|
||||
let error = format!("{:#}", result.unwrap_err());
|
||||
assert_eq!(calls, [None], "stopping does not wait on the write");
|
||||
assert!(error.contains("brake was not written"), "{error}");
|
||||
assert!(!out.contains("stopped"), "{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_show_and_release_never_call_the_stopper() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
for args in [&["swarm", "show"][..], &["swarm", "brake", "release"], &["swarm", "brake", "release", "now"], &["swarm", "brake"]] {
|
||||
let (_, _, calls) = go_stopping(root.path(), args, "op", 0, |_| Ok(()));
|
||||
assert!(calls.is_empty(), "{args:?} called the stopper");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_commands_refuse_bad_input_and_write_nothing() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ mind_body::chat_rudp: impl RudpChat: fn new (remote : SocketAddr , connection_id
|
|||
mind_body::chat_rudp: struct RudpChat pub-fields[]
|
||||
mind_body::cli: fn instance_and_endpoint (args : & [String] , env_endpoint : Option < String > , usage : & str) -> Result < (Slug , SocketAddr) >
|
||||
mind_body::cli: fn operator_name (sudo_user : Option < String > , user : Option < String >) -> String
|
||||
mind_body::cli: fn run (args : & [String] , root : & Path , set_by : & str , now : DateTime < Utc > , out : & mut impl Write , stop_units : & mut dyn FnMut (& Slug) -> Result < () > ,) -> Result < () >
|
||||
mind_body::cli: fn run (args : & [String] , root : & Path , set_by : & str , now : DateTime < Utc > , out : & mut impl Write , stop_units : & mut dyn FnMut (StopScope < '_ >) -> Result < () > ,) -> Result < () >
|
||||
mind_body::connector: fn render_plan (windows : & [PlanQuota] , now_ms : u64) -> String
|
||||
mind_body::control: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum HeldReason { BrakeAbsent , BrakeHeld , Undecodable , }
|
||||
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum Effective { Held { reason : HeldReason } , Released (Grant) , }
|
||||
|
|
@ -107,6 +107,7 @@ mind_body::heartbeat: struct PersonaId # [derive (Clone , Debug , PartialEq , Eq
|
|||
mind_body::heartbeat: struct PollTrace # [derive (Clone , Debug , PartialEq , Eq , DatabaseEntry)] pub-fields[at: DateTime < Utc >, tempo_s: u32, instances: Vec < InstanceTrace >, launches: Vec < String >]
|
||||
mind_body::heartbeat: struct Seat< 'a > pub-fields[who: PersonaId, mind: & 'a dyn MindPort, chat: & 'a dyn Chat]
|
||||
mind_body::launch: # [derive (Clone , Debug , PartialEq , Eq)] pub enum Declined { Admission (MindRefusal) , Conflict , AlreadyAdmitted , StartFailed , GrantForOtherInstance , PersonaRunTakesNoClaims , }
|
||||
mind_body::launch: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum StopScope < 'a > { Mind (& 'a Slug) , Swarm }
|
||||
mind_body::launch: # [derive (Clone , Debug , PartialEq , Eq)] pub enum LaunchOutcome { Launched { run : PipelineRef , } , NotQueued { item : PipelineRef , } , Tripped { item : PipelineRef , question : PipelineRef , } , Busy { run : PipelineRef , } , Refused (Declined) , }
|
||||
mind_body::launch: const START_GRACE_S : i64
|
||||
mind_body::launch: fn open_and_launch (ports : & Ports , request : LaunchRequest) -> Result < LaunchOutcome >
|
||||
|
|
@ -117,7 +118,7 @@ mind_body::launch: impl Launcher for SystemdLauncher
|
|||
mind_body::launch: impl Systemctl for RealSystemctl
|
||||
mind_body::launch: impl SystemdLauncher
|
||||
mind_body::launch: impl SystemdLauncher: fn new () -> Self
|
||||
mind_body::launch: impl SystemdLauncher: fn stop_all (& self , instance : & Slug) -> Result < () >
|
||||
mind_body::launch: impl SystemdLauncher: fn stop (& self , scope : StopScope < '_ >) -> Result < () >
|
||||
mind_body::launch: pub trait Clock { fn now (& self) -> DateTime < Utc > ; }
|
||||
mind_body::launch: pub trait Launcher { fn start (& self , instance : & Slug , turn : RunTurn) -> Result < () > ; fn alive (& self , instance : & Slug , turn : RunTurn) -> bool ; }
|
||||
mind_body::launch: pub trait Systemctl { fn run (& self , args : & [& str]) -> Result < SystemctlReply > ; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue