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>
213 lines
9.3 KiB
Rust
213 lines
9.3 KiB
Rust
//! The `mind-control` command line: the operator's hand on one instance's
|
|
//! control store. It checks no caller identity: the file is
|
|
//! root-owned and the filesystem is the authority.
|
|
//!
|
|
//! ```text
|
|
//! mind-control [--root <dir>] --instance <slug> show
|
|
//! mind-control --instance <slug> brake hold (also stops her running units)
|
|
//! 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 (hold also stops every Mind's units)
|
|
//! ```
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! 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;
|
|
use std::path::Path;
|
|
|
|
use anyhow::{Result, anyhow, bail};
|
|
use chrono::{DateTime, Utc};
|
|
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::{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";
|
|
|
|
/// The arguments every organ binary shares: `--instance <slug>` and an optional
|
|
/// `--endpoint rudp://<ip>:<port>`, which `env_endpoint` (the binary's reading of
|
|
/// `HUGINN_ENDPOINT`) replaces when absent. A flag given twice, an unknown
|
|
/// argument, a name that is not a slug or an endpoint that is not
|
|
/// `rudp://<ip>:<port>` is refused with `usage` or a fixed message, and no
|
|
/// value is echoed.
|
|
pub fn instance_and_endpoint(args: &[String], env_endpoint: Option<String>, usage: &str) -> Result<(Slug, SocketAddr)> {
|
|
let (mut instance, mut endpoint) = (None, None);
|
|
let mut rest = args.iter();
|
|
while let Some(flag) = rest.next() {
|
|
let slot = match flag.as_str() {
|
|
"--instance" => &mut instance,
|
|
"--endpoint" => &mut endpoint,
|
|
_ => bail!("{usage}"),
|
|
};
|
|
let value = rest.next().ok_or_else(|| anyhow!("{usage}"))?;
|
|
if slot.replace(value.clone()).is_some() {
|
|
bail!("{usage}");
|
|
}
|
|
}
|
|
let instance = Slug(instance.ok_or_else(|| anyhow!("{usage}"))?);
|
|
instance.validate_slug().map_err(|_| anyhow!("the instance is not a slug"))?;
|
|
let endpoint = endpoint.or(env_endpoint).ok_or_else(|| anyhow!("no daemon endpoint: --endpoint or HUGINN_ENDPOINT"))?;
|
|
let address = endpoint
|
|
.strip_prefix("rudp://")
|
|
.and_then(|address| address.parse::<SocketAddr>().ok())
|
|
.ok_or_else(|| anyhow!("the endpoint is not rudp://<ip>:<port>"))?;
|
|
Ok((instance, address))
|
|
}
|
|
|
|
/// Who made the change: `SUDO_USER`, else `USER`, else `unknown`. An empty
|
|
/// variable counts as unset.
|
|
pub fn operator_name(sudo_user: Option<String>, user: Option<String>) -> String {
|
|
[sudo_user, user].into_iter().flatten().find(|name| !name.is_empty()).unwrap_or_else(|| "unknown".to_string())
|
|
}
|
|
|
|
/// Runs one command against `<root>/<instance>/control.cc`. `root` is the
|
|
/// 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 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(StopScope<'_>) -> Result<()>,
|
|
) -> Result<()> {
|
|
let (root, args) = match args {
|
|
[flag, dir, rest @ ..] if flag == "--root" => (Path::new(dir), rest),
|
|
_ => (root, args),
|
|
};
|
|
if let [word, rest @ ..] = args
|
|
&& word == "swarm"
|
|
{
|
|
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),
|
|
_ => bail!("{USAGE}"),
|
|
};
|
|
let path = control_path(root, &instance)?;
|
|
let words: Vec<&str> = command.iter().map(String::as_str).collect();
|
|
match words.as_slice() {
|
|
["show"] => show(root, &instance, out),
|
|
["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(StopScope::Mind(&instance));
|
|
match (written, stopped) {
|
|
(Ok(()), Ok(())) => {
|
|
writeln!(out, "units: stopped {} {}", unit(&instance, RunTurn::SelfRun)?, unit(&instance, RunTurn::PersonaTurn)?)?;
|
|
Ok(())
|
|
}
|
|
outcome => held(outcome),
|
|
}
|
|
}
|
|
["brake", "release"] => ControlWriter::open(&path)?.set_brake(true, now, set_by),
|
|
_ => bail!("{USAGE}"),
|
|
}
|
|
}
|
|
|
|
/// 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. `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"] => {
|
|
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}"),
|
|
}
|
|
}
|
|
|
|
fn show_swarm(path: &Path, out: &mut impl Write) -> Result<()> {
|
|
let source = FileSource::new(path);
|
|
writeln!(out, "swarm: {}", path.display())?;
|
|
match load_swarm_state(&source) {
|
|
Ok(state) => {
|
|
match &state.brake {
|
|
Some(brake) => writeln!(out, "brake: {} (set_at {}, set_by {})", if brake.released { "released" } else { "held" }, brake.set_at.to_rfc3339(), brake.set_by)?,
|
|
None => writeln!(out, "brake: absent")?,
|
|
}
|
|
}
|
|
Err(error) => writeln!(out, "store: undecodable ({error:#})")?,
|
|
}
|
|
writeln!(out, "effective: {:?}", derive_swarm(&source))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// 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(root: &Path, path: &Path, instance: &Slug, out: &mut impl Write) -> Result<()> {
|
|
let effective = derive(&FileSource::new(path), &FileSource::new(swarm_path(root)), 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)?;
|
|
match load_state(&source) {
|
|
Ok(state) => {
|
|
match &state.brake {
|
|
Some(brake) => writeln!(
|
|
out,
|
|
"brake: {} (set_at {}, set_by {})",
|
|
if brake.released { "released" } else { "held" },
|
|
brake.set_at.to_rfc3339(),
|
|
brake.set_by
|
|
)?,
|
|
None => writeln!(out, "brake: absent")?,
|
|
}
|
|
}
|
|
Err(error) => writeln!(out, "store: undecodable ({error:#})")?,
|
|
}
|
|
writeln!(out, "effective: {:?}", derive(&source, &FileSource::new(swarm_path(root)), instance))?;
|
|
Ok(())
|
|
}
|