pub(crate) const SYSTEMD_UNIT_PATTERN: &str = "opencrabs*.service";
pub(crate) const EVOLVE_UNIT_PREFIX: &str = "opencrabs-evolve-";
pub(crate) struct RestartTargets {
pub(crate) user: bool,
pub(crate) units: Vec<String>,
}
pub(crate) enum RestartPlan {
Targets(RestartTargets),
NoUnitsMatched,
Unresolved,
}
pub(crate) fn build_systemd_restart_command(
pid: u32,
user: bool,
units: &[String],
) -> std::process::Command {
let unit_name = format!("{EVOLVE_UNIT_PREFIX}{pid}");
let mut cmd = std::process::Command::new("systemd-run");
let mut args = vec![];
if user {
args.push("--user".to_string());
}
args.push("--on-active=3".to_string());
args.push(format!("--unit={unit_name}"));
args.push("systemctl".to_string());
if user {
args.push("--user".to_string());
}
args.push("restart".to_string());
let targets = restart_targets_from(units);
if targets.len() != units.len() {
tracing::warn!(
target: "evolve",
dropped = units.len() - targets.len(),
"evolve: dropped a transient evolve unit from the restart target list, \
it is the unit this restart runs inside"
);
}
args.extend(targets);
cmd.args(&args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
cmd
}
pub(crate) fn restart_targets_from(candidates: &[String]) -> Vec<String> {
candidates
.iter()
.filter(|unit| !unit.trim_start().starts_with(EVOLVE_UNIT_PREFIX))
.cloned()
.collect()
}
pub(crate) const EVOLVE_UNIT_GLOB: &str = "opencrabs-evolve-*.service";
pub(crate) fn build_systemd_cleanup_command(user: bool) -> std::process::Command {
let mut cmd = std::process::Command::new("systemctl");
if user {
cmd.arg("--user");
}
cmd.arg("reset-failed")
.arg(EVOLVE_UNIT_GLOB)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
cmd
}
pub(crate) fn list_matching_systemd_units(pattern: &str, user: bool) -> Option<Vec<String>> {
let mut cmd = std::process::Command::new("systemctl");
cmd.args(["list-units", "--no-legend", "--no-pager"]);
if user {
cmd.arg("--user");
}
cmd.arg(pattern);
cmd.stderr(std::process::Stdio::null());
let output = cmd.output().ok()?;
if !output.status.success() {
return None;
}
Some(parse_systemd_unit_names(&String::from_utf8_lossy(
&output.stdout,
)))
}
pub(crate) fn parse_systemd_unit_names(stdout: &str) -> Vec<String> {
stdout
.lines()
.filter_map(|line| {
line.split_whitespace()
.find(|token| token.ends_with(".service"))
})
.map(|name| name.to_string())
.collect()
}
enum BusLookup {
Units(Vec<String>),
Unknown,
Empty,
}
fn lookup_bus(pattern: &str, user: bool) -> BusLookup {
match list_matching_systemd_units(pattern, user) {
None => BusLookup::Unknown,
Some(candidates) => {
let units = restart_targets_from(&candidates);
if units.is_empty() {
BusLookup::Empty
} else {
BusLookup::Units(units)
}
}
}
}
pub(crate) fn select_restart_targets(sid: uuid::Uuid) -> RestartPlan {
match lookup_bus(SYSTEMD_UNIT_PATTERN, false) {
BusLookup::Units(units) => {
tracing::info!(
target: "evolve",
pattern = SYSTEMD_UNIT_PATTERN,
matched_units = units.len(),
use_user_units = false,
session_id = %sid,
"evolve: pre-flight found system-level units, scheduling restart (+3s)"
);
return RestartPlan::Targets(RestartTargets { user: false, units });
}
BusLookup::Unknown => {
tracing::warn!(
target: "evolve",
pattern = SYSTEMD_UNIT_PATTERN,
session_id = %sid,
"evolve: could not list system-level units (systemctl query failed), \
no restart target could be resolved"
);
return RestartPlan::Unresolved;
}
BusLookup::Empty => {}
}
match lookup_bus(SYSTEMD_UNIT_PATTERN, true) {
BusLookup::Units(units) => {
tracing::info!(
target: "evolve",
pattern = SYSTEMD_UNIT_PATTERN,
user_units = units.len(),
session_id = %sid,
"evolve: no system-level units found, using user-level units, \
scheduling restart with --user"
);
return RestartPlan::Targets(RestartTargets { user: true, units });
}
BusLookup::Unknown => {
tracing::warn!(
target: "evolve",
pattern = SYSTEMD_UNIT_PATTERN,
session_id = %sid,
"evolve: could not list user-level units (systemctl query failed), \
no restart target could be resolved"
);
return RestartPlan::Unresolved;
}
BusLookup::Empty => {}
}
tracing::warn!(
target: "evolve",
pattern = SYSTEMD_UNIT_PATTERN,
session_id = %sid,
"evolve: no systemd units matched the pattern (checked system and user level), \
skipping scheduled restart"
);
RestartPlan::NoUnitsMatched
}
pub(crate) fn sweep_stale_evolve_units(use_user_units: bool, sid: uuid::Uuid) {
match build_systemd_cleanup_command(use_user_units).status() {
Ok(status) => tracing::debug!(
target: "evolve",
glob = EVOLVE_UNIT_GLOB,
success = status.success(),
session_id = %sid,
"evolve: reset-failed swept stale evolve units"
),
Err(e) => tracing::warn!(
target: "evolve",
glob = EVOLVE_UNIT_GLOB,
error = %e,
session_id = %sid,
"evolve: could not sweep stale evolve units, the restart still proceeds"
),
}
}