//! 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 ] --instance show //! mind-control --instance brake hold (also stops her running units) //! mind-control --instance brake release //! mind-control [--root ] --instance check //! mind-control [--root ] swarm show //! mind-control swarm brake hold | release (hold also stops every Mind's units) //! ``` //! //! The `swarm` verbs write `/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 ] --instance show | check | brake hold | brake release | mind-control [--root ] swarm show | brake hold | brake release"; /// The arguments every organ binary shares: `--instance ` and an optional /// `--endpoint rudp://:`, 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://:` is refused with `usage` or a fixed message, and no /// value is echoed. pub fn instance_and_endpoint(args: &[String], env_endpoint: Option, 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::().ok()) .ok_or_else(|| anyhow!("the endpoint is not rudp://:"))?; 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, user: Option) -> String { [sudo_user, user].into_iter().flatten().find(|name| !name.is_empty()).unwrap_or_else(|| "unknown".to_string()) } /// Runs one command against `//control.cc`. `root` is the /// default; a leading `--root ` 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, 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 `/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, 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(()) }