mind-body: brake hold stops the exact units unit() names, and the stop path is pinned
stop_args now builds each name through unit(), the one name path start and alive use, so the stop argv cannot disagree with the units that run (the :* glob matched none of them on real systemd). A launch test ties stop_args to what start passes systemctl for several instances; two binary tests run mind-control brake hold with a fake systemctl first on PATH, so main's stopper wiring is pinned. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5323167555
commit
bcda08b8cd
3 changed files with 76 additions and 22 deletions
|
|
@ -19,10 +19,11 @@ use std::str::FromStr;
|
|||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use eureka_pipeline::Slug;
|
||||
use eureka_pipeline::{RunTurn, Slug};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::control::{BurnRate, ControlWriter, FileSource, control_path, derive, load_state};
|
||||
use crate::launch::unit;
|
||||
|
||||
const USAGE: &str =
|
||||
"usage: mind-control [--root <dir>] --instance <slug> show | brake hold | brake release | dial set --heat H --base-cooldown-s S --base-run-usd U";
|
||||
|
|
@ -63,7 +64,7 @@ pub fn run(
|
|||
let stopped = stop_units(&instance);
|
||||
match (written, stopped) {
|
||||
(Ok(()), Ok(())) => {
|
||||
writeln!(out, "units: stopped mind-self@{0}:* mind-persona@{0}:*", instance.0)?;
|
||||
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")),
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ impl Default for SystemdLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
/// The unit template of one turn. Start, alive and stop all name units from
|
||||
/// these two.
|
||||
/// The unit template of one turn, named only through `unit()`, which start,
|
||||
/// alive and stop all call.
|
||||
fn template(turn: RunTurn) -> &'static str {
|
||||
match turn {
|
||||
RunTurn::PersonaTurn => "mind-persona",
|
||||
|
|
@ -124,23 +124,27 @@ pub(crate) fn unit(instance: &Slug, turn: RunTurn) -> Result<String> {
|
|||
Ok(format!("{}@{}.service", template(turn), instance.0))
|
||||
}
|
||||
|
||||
/// The arguments of the one synchronous stop: every unit of both turns of this
|
||||
/// instance, matched by glob. The `:` after the instance keeps `eureka` from
|
||||
/// matching `eureka-two`. No `--no-block`: `systemctl stop` returns when the
|
||||
/// units are down. The instance is not validated here; `stop_all` does.
|
||||
pub(crate) fn stop_args(instance: &Slug) -> Vec<String> {
|
||||
/// 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
|
||||
/// `--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>> {
|
||||
let mut args = vec!["stop".to_string(), "--".to_string()];
|
||||
args.extend([RunTurn::SelfRun, RunTurn::PersonaTurn].map(|turn| format!("{}@{}:*.service", template(turn), instance.0)));
|
||||
args
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
args.push(unit(instance, 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 glob that matches no loaded unit is a success.
|
||||
/// calls it. A unit that is not installed is systemctl's exit 5, and the
|
||||
/// others still stop.
|
||||
pub fn stop_all(&self, instance: &Slug) -> Result<()> {
|
||||
instance.validate_slug().map_err(|_| anyhow!("the instance is not a unit name"))?;
|
||||
let args = stop_args(instance);
|
||||
let args = stop_args(instance)?;
|
||||
let args: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
let reply = self.systemctl.run(&args)?;
|
||||
if reply.success {
|
||||
|
|
@ -1169,20 +1173,28 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn stop_args_is_one_synchronous_stop_scoped_to_one_instance() {
|
||||
let args = stop_args(&eureka());
|
||||
assert_eq!(args, ["stop", "--", "mind-self@eureka:*.service", "mind-persona@eureka:*.service"]);
|
||||
// The text before each glob star must not be a prefix of another instance's unit.
|
||||
for (pattern, other) in [(&args[2], "mind-self@eureka-two:x.service"), (&args[3], "mind-persona@eureka-two:x.service")] {
|
||||
assert!(!other.starts_with(pattern.split('*').next().unwrap()), "{pattern}");
|
||||
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();
|
||||
assert_eq!(args[..2], ["stop", "--"], "one synchronous stop, no --no-block");
|
||||
let (launcher, spy) = spied();
|
||||
let mut started = Vec::new();
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
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!(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"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_all_runs_the_one_stop_and_waits() {
|
||||
let (launcher, spy) = spied();
|
||||
launcher.stop_all(&eureka()).unwrap();
|
||||
assert_eq!(*spy.calls.borrow(), vec![stop_args(&eureka())]);
|
||||
assert_eq!(*spy.calls.borrow(), vec![stop_args(&eureka()).unwrap()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ fn brake_hold_stops_once_with_the_instance_and_the_store_already_held() {
|
|||
result.unwrap();
|
||||
assert_eq!(calls, ["eureka"]);
|
||||
assert!(seen_at_stop.contains("BrakeHeld"), "the write comes before the stop: {seen_at_stop}");
|
||||
assert!(out.contains("units: stopped mind-self@eureka:* mind-persona@eureka:*"), "{out}");
|
||||
assert!(out.contains("units: stopped mind-self@eureka.service mind-persona@eureka.service"), "{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -223,3 +223,44 @@ fn a_failing_write_still_stops_and_the_hold_fails() {
|
|||
assert_eq!(calls.len(), 1);
|
||||
assert!(error.contains("brake was not written") && error.contains("units were not stopped"), "{error}");
|
||||
}
|
||||
|
||||
/// Runs the real binary's `brake hold` with a `systemctl` of ours first on
|
||||
/// PATH, so the stopper main.rs wires is the one under test. The fake records
|
||||
/// its arguments, one per line, and exits with `exit`.
|
||||
#[cfg(unix)]
|
||||
fn hold_through_the_binary(exit: i32) -> (std::process::Output, Vec<String>) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let bin = tempfile::tempdir().unwrap();
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let record = bin.path().join("recorded");
|
||||
let script = bin.path().join("systemctl");
|
||||
std::fs::write(&script, format!("#!/bin/sh\nprintf '%s\n' \"$@\" > '{}'\nexit {exit}\n", record.display())).unwrap();
|
||||
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_mind-control"))
|
||||
.env("PATH", bin.path())
|
||||
.args(["--root", root.path().to_str().unwrap(), "--instance", "eureka", "brake", "hold"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let recorded = std::fs::read_to_string(&record).map(|text| text.lines().map(str::to_string).collect()).unwrap_or_default();
|
||||
(output, recorded)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn the_binary_hold_runs_systemctl_stop_on_exactly_the_units_it_starts() {
|
||||
let (output, recorded) = 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"]);
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("units: stopped mind-self@eureka.service mind-persona@eureka.service"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn the_binary_hold_fails_with_status_2_when_systemctl_does() {
|
||||
let (output, recorded) = hold_through_the_binary(5);
|
||||
assert_eq!(output.status.code(), Some(2));
|
||||
assert_eq!(recorded.len(), 4, "systemctl was run: {recorded:?}");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("units were not stopped") && stderr.contains("exit status 5"), "{stderr}");
|
||||
assert!(!String::from_utf8_lossy(&output.stdout).contains("stopped"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue