] --instance show
-//! mind-control --instance brake hold
+//! mind-control --instance brake hold (also stops her running units)
//! mind-control --instance brake release
//! mind-control --instance dial set --heat H --base-cooldown-s S --base-run-usd U
//! ```
+//!
+//! `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.
use std::io::Write;
use std::path::Path;
@@ -26,18 +30,22 @@ const USAGE: &str =
/// 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())
+ [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 and the output are parameters so a test drives
-/// the whole command without touching `/etc`.
-pub fn run(args: &[String], root: &Path, set_by: &str, now: DateTime, out: &mut impl Write) -> Result<()> {
+/// 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.
+pub fn run(
+ args: &[String],
+ root: &Path,
+ set_by: &str,
+ now: DateTime,
+ out: &mut impl Write,
+ stop_units: &mut dyn FnMut(&Slug) -> Result<()>,
+) -> Result<()> {
let (root, args) = match args {
[flag, dir, rest @ ..] if flag == "--root" => (Path::new(dir), rest),
_ => (root, args),
@@ -50,7 +58,19 @@ pub fn run(args: &[String], root: &Path, set_by: &str, now: DateTime, out:
let words: Vec<&str> = command.iter().map(String::as_str).collect();
match words.as_slice() {
["show"] => show(root, &instance, out),
- ["brake", "hold"] => ControlWriter::open(&path)?.set_brake(false, now, set_by),
+ ["brake", "hold"] => {
+ let written = ControlWriter::open(&path).and_then(|mut writer| writer.set_brake(false, now, set_by));
+ let stopped = stop_units(&instance);
+ match (written, stopped) {
+ (Ok(()), Ok(())) => {
+ writeln!(out, "units: stopped mind-self@{0}:* mind-persona@{0}:*", instance.0)?;
+ 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 units were not stopped")),
+ (Err(write), Err(stop)) => Err(anyhow!("the brake was not written ({write:#}) and the units were not stopped ({stop:#})")),
+ }
+ }
["brake", "release"] => ControlWriter::open(&path)?.set_brake(true, now, set_by),
["dial", "set", flags @ ..] => {
let dial = parse_dial(flags, now, set_by)?;
@@ -77,9 +97,7 @@ fn parse_dial(flags: &[&str], now: DateTime, set_by: &str) -> Result,
pub stdout: String,
}
@@ -71,7 +73,11 @@ struct RealSystemctl;
impl Systemctl for RealSystemctl {
fn run(&self, args: &[&str]) -> Result {
let output = std::process::Command::new("systemctl").args(args).output().map_err(|_| anyhow!("systemctl could not be run"))?;
- Ok(SystemctlReply { success: output.status.success(), stdout: String::from_utf8_lossy(&output.stdout).into_owned() })
+ Ok(SystemctlReply {
+ success: output.status.success(),
+ code: output.status.code(),
+ stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
+ })
}
}
@@ -101,16 +107,50 @@ impl Default for SystemdLauncher {
}
}
+/// The unit template of one turn. Start, alive and stop all name units from
+/// these two.
+fn template(turn: RunTurn) -> &'static str {
+ match turn {
+ RunTurn::PersonaTurn => "mind-persona",
+ RunTurn::SelfRun => "mind-self",
+ }
+}
+
/// The unit of one turn: the template by turn, then the instance, held to the
/// leaf's grammar first so it cannot escape the name. The error names nothing
/// of the value. Start and alive both name the unit here.
pub(crate) fn unit(instance: &Slug, turn: RunTurn) -> Result {
instance.validate_slug().map_err(|_| anyhow!("the instance is not a unit name"))?;
- let template = match turn {
- RunTurn::PersonaTurn => "mind-persona",
- RunTurn::SelfRun => "mind-self",
- };
- Ok(format!("{template}@{}.service", instance.0))
+ 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 {
+ let mut args = vec!["stop".to_string(), "--".to_string()];
+ args.extend([RunTurn::SelfRun, RunTurn::PersonaTurn].map(|turn| format!("{}@{}:*.service", template(turn), instance.0)));
+ 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.
+ 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: Vec<&str> = args.iter().map(String::as_str).collect();
+ let reply = self.systemctl.run(&args)?;
+ if reply.success {
+ return Ok(());
+ }
+ match reply.code {
+ Some(code) => Err(anyhow!("systemctl stop failed with exit status {code}")),
+ None => Err(anyhow!("systemctl stop was ended by a signal")),
+ }
+ }
}
impl Launcher for SystemdLauncher {
@@ -1082,18 +1122,20 @@ mod tests {
struct Spy {
calls: RefCell>>,
success: Cell,
+ code: Cell