mind-body: mind-control check, the brake gate a unit runs before it starts
Exit 0 only when the one derivation reads Released; every other reading (absent, held, no dial, dial out of bounds, undecodable store) is an error and exits 2, which systemd's ExecCondition reads as skip. A pure read through FileSource: no writer, no created file or directory. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bcda08b8cd
commit
f1f36d5ff1
2 changed files with 128 additions and 2 deletions
|
|
@ -7,8 +7,12 @@
|
|||
//! mind-control --instance <slug> brake hold (also stops her running units)
|
||||
//! mind-control --instance <slug> brake release
|
||||
//! mind-control --instance <slug> dial set --heat H --base-cooldown-s S --base-run-usd U
|
||||
//! mind-control [--root <dir>] --instance <slug> check
|
||||
//! ```
|
||||
//!
|
||||
//! `check` exits 0 only when the brake is released and the dial in bounds; 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.
|
||||
|
|
@ -22,11 +26,11 @@ use chrono::{DateTime, Utc};
|
|||
use eureka_pipeline::{RunTurn, Slug};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::control::{BurnRate, ControlWriter, FileSource, control_path, derive, load_state};
|
||||
use crate::control::{BurnRate, ControlWriter, Effective, 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";
|
||||
"usage: mind-control [--root <dir>] --instance <slug> show | check | brake hold | brake release | dial set --heat H --base-cooldown-s S --base-run-usd U";
|
||||
|
||||
/// Who made the change: `SUDO_USER`, else `USER`, else `unknown`. An empty
|
||||
/// variable counts as unset.
|
||||
|
|
@ -59,6 +63,7 @@ pub fn run(
|
|||
let words: Vec<&str> = command.iter().map(String::as_str).collect();
|
||||
match words.as_slice() {
|
||||
["show"] => show(root, &instance, out),
|
||||
["check"] => check(&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);
|
||||
|
|
@ -109,6 +114,20 @@ fn need<'a>(value: Option<&'a str>, name: &str) -> Result<&'a str> {
|
|||
value.ok_or_else(|| anyhow!("{name} is required"))
|
||||
}
|
||||
|
||||
/// The gate a unit runs before it starts: Ok only when the one derivation
|
||||
/// (`derive`, the reader behind `read_effective`) says Released. It is a pure
|
||||
/// read through `FileSource`: it opens no writer and creates no file or
|
||||
/// directory. Any other reading is an error, which `main` turns into exit 2,
|
||||
/// and systemd's `ExecCondition` skips the unit on 1-254.
|
||||
fn check(path: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
|
||||
let effective = derive(&FileSource::new(path), instance);
|
||||
writeln!(out, "effective: {effective:?}")?;
|
||||
match effective {
|
||||
Effective::Released(_) => Ok(()),
|
||||
Effective::Held { reason } => bail!("held: {reason:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn show(root: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
|
||||
let source = FileSource::new(control_path(root, instance)?);
|
||||
writeln!(out, "instance: {}", instance.0)?;
|
||||
|
|
|
|||
|
|
@ -128,6 +128,43 @@ fn the_binary_exits_nonzero_with_the_usage_when_refused() {
|
|||
assert!(output.stdout.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_binary_exits_2_for_check_while_held_and_0_after_release_with_a_dial() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let run = |args: &[&str]| {
|
||||
std::process::Command::new(env!("CARGO_BIN_EXE_mind-control"))
|
||||
.arg("--root")
|
||||
.arg(root.path())
|
||||
.args(["--instance", "eureka"])
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap()
|
||||
};
|
||||
// Absent store: refused, and the gate wrote nothing.
|
||||
let output = run(&["check"]);
|
||||
assert_eq!(output.status.code(), Some(2));
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("held: BrakeAbsent"));
|
||||
assert_eq!(std::fs::read_dir(root.path()).unwrap().count(), 0);
|
||||
|
||||
// Released with no dial is still refused; with a dial it opens.
|
||||
assert!(run(&["brake", "release"]).status.success());
|
||||
assert_eq!(run(&["check"]).status.code(), Some(2));
|
||||
assert!(run(&["dial", "set", "--heat", "0.5", "--base-cooldown-s", "3600", "--base-run-usd", "5"]).status.success());
|
||||
let open = run(&["check"]);
|
||||
assert_eq!(open.status.code(), Some(0));
|
||||
assert!(String::from_utf8_lossy(&open.stdout).contains("Released"));
|
||||
|
||||
// Holding closes it again. The brake is written through the library: the
|
||||
// binary's `brake hold` would also ask systemd to stop units.
|
||||
mind_body::control::ControlWriter::open(root.path().join("eureka").join("control.cc"))
|
||||
.unwrap()
|
||||
.set_brake(false, Utc.timestamp_opt(1_800_000_009, 0).unwrap(), "op")
|
||||
.unwrap();
|
||||
let closed = run(&["check"]);
|
||||
assert_eq!(closed.status.code(), Some(2));
|
||||
assert!(String::from_utf8_lossy(&closed.stderr).contains("held: BrakeHeld"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_leading_root_flag_replaces_the_default_root() {
|
||||
let default = tempfile::tempdir().unwrap();
|
||||
|
|
@ -152,6 +189,76 @@ fn a_root_flag_without_a_directory_or_an_instance_is_refused_and_writes_nothing(
|
|||
assert!(!std::path::Path::new("/nonexistent-root-for-test").exists());
|
||||
}
|
||||
|
||||
fn check_out(root: &std::path::Path) -> (anyhow::Result<()>, String) {
|
||||
go(root, &["--instance", "eureka", "check"], "op", 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_refuses_every_reading_but_released() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let held = |root: &std::path::Path, reason: &str| {
|
||||
let (result, out) = check_out(root);
|
||||
let error = format!("{:#}", result.unwrap_err());
|
||||
assert!(error.contains(reason), "{reason}: {error}");
|
||||
assert!(out.contains(reason), "{reason}: {out}");
|
||||
};
|
||||
held(root.path(), "BrakeAbsent");
|
||||
go(root.path(), &["--instance", "eureka", "brake", "hold"], "op", 1).0.unwrap();
|
||||
held(root.path(), "BrakeHeld");
|
||||
go(root.path(), &["--instance", "eureka", "brake", "release"], "op", 2).0.unwrap();
|
||||
held(root.path(), "DialAbsent");
|
||||
go(root.path(), &["--instance", "eureka", "dial", "set", "--heat", "1000", "--base-cooldown-s", "3600", "--base-run-usd", "5"], "op", 3)
|
||||
.0
|
||||
.unwrap();
|
||||
held(root.path(), "DialOutOfBounds");
|
||||
// An undecodable store is a held reading too.
|
||||
let broken = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir(broken.path().join("eureka")).unwrap();
|
||||
std::fs::write(broken.path().join("eureka").join("control.cc"), b"not a store").unwrap();
|
||||
held(broken.path(), "Undecodable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_opens_on_release_with_a_dial_and_is_per_instance() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
go(root.path(), &["--instance", "eureka", "brake", "release"], "op", 0).0.unwrap();
|
||||
go(root.path(), &["--instance", "eureka", "dial", "set", "--heat", "0.5", "--base-cooldown-s", "3600", "--base-run-usd", "5"], "op", 1)
|
||||
.0
|
||||
.unwrap();
|
||||
let (result, out) = check_out(root.path());
|
||||
result.unwrap();
|
||||
assert!(out.contains("Released"), "{out}");
|
||||
// Another instance's store is a different file: still absent, still refused.
|
||||
let (other, _) = go(root.path(), &["--instance", "other", "check"], "op", 2);
|
||||
assert!(format!("{:#}", other.unwrap_err()).contains("BrakeAbsent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_writes_nothing_and_creates_no_directory() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
assert!(check_out(root.path()).0.is_err());
|
||||
assert_eq!(std::fs::read_dir(root.path()).unwrap().count(), 0);
|
||||
|
||||
go(root.path(), &["--instance", "eureka", "brake", "release"], "op", 0).0.unwrap();
|
||||
let file = root.path().join("eureka").join("control.cc");
|
||||
let before = (std::fs::read(&file).unwrap(), std::fs::metadata(&file).unwrap().modified().unwrap());
|
||||
let _ = check_out(root.path());
|
||||
let _ = check_out(root.path());
|
||||
assert_eq!((std::fs::read(&file).unwrap(), std::fs::metadata(&file).unwrap().modified().unwrap()), before);
|
||||
assert_eq!(std::fs::read_dir(root.path().join("eureka")).unwrap().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_takes_no_extra_word_and_needs_an_instance() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
go(root.path(), &["--instance", "eureka", "brake", "release"], "op", 0).0.unwrap();
|
||||
for args in [&["--instance", "eureka", "check", "now"][..], &["--instance", "eureka", "check", "show"][..], &["check"][..]] {
|
||||
let (result, out) = go(root.path(), args, "op", 1);
|
||||
assert!(format!("{:#}", result.unwrap_err()).contains("usage: mind-control"), "{args:?}");
|
||||
assert!(out.is_empty(), "{args:?}: {out}");
|
||||
}
|
||||
}
|
||||
|
||||
fn shown(root: &std::path::Path) -> String {
|
||||
go(root, &["--instance", "eureka", "show"], "op", 0).1
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue