Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257use anyhow::{Context, Result, bail};use std::path::PathBuf;use std::process::Command;
#[derive(Debug)]pub struct Output { pub status: i32, pub stdout: String, pub stderr: String,}
impl Output { pub fn success(&self) -> bool { self.status == 0 }}
pub fn run(prog: &str, args: &[&str]) -> Result<Output> { run_with_env(prog, args, &[])}
pub fn run_with_env(prog: &str, args: &[&str], env: &[(&str, &str)]) -> Result<Output> { let mut cmd = Command::new(prog); cmd.args(args); for (k, v) in env { cmd.env(k, v); } let output = cmd .output() .with_context(|| format!("failed to execute {prog}"))?;
let status = output.status.code().unwrap_or(128); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Ok(Output { status, stdout, stderr, })}
pub fn generate_unit_id() -> String { use rand::RngExt; let mut rng = rand::rng(); let bytes: [u8; 4] = rng.random(); hex_encode(&bytes)}
fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect()}
pub fn find_executable(name: &str) -> Option<PathBuf> { std::env::var_os("PATH").and_then(|paths| { std::env::split_paths(&paths) .map(|dir| dir.join(name)) .find(|path| path.is_file()) })}
pub fn require_binary(name: &str, package_hint: &str) -> Result<()> { match find_executable(name) { Some(_) => Ok(()), None => bail!("{name} not found in PATH — install {package_hint} to continue"), }}
pub fn escape_unit_name(name: &str) -> Result<String> { let out = run("systemd-escape", &[name])?; if !out.success() { bail!("systemd-escape failed: {}", out.stderr); } Ok(out.stdout)}
pub fn systemd_run_bin() -> String { std::env::var("EPI_SYSTEMD_RUN_BIN").unwrap_or_else(|_| "systemd-run".to_string())}
pub fn systemctl_bin() -> String { std::env::var("EPI_SYSTEMCTL_BIN") .unwrap_or_else(|_| "/run/current-system/sw/bin/systemctl".to_string())}
pub fn run_helper( unit_name: &str, slice: &str, vm_unit: Option<&str>, prog: &str, args: &[&str],) -> Result<Output> { let unit_arg = format!("--unit={unit_name}"); let slice_arg = format!("--slice={slice}"); let mut cmd_args: Vec<String> = vec![ "--user".to_string(), "--collect".to_string(), unit_arg, slice_arg, ];
if let Some(vm_unit) = vm_unit { // systemd-run --property= interprets backslash as an escape character, // so we must double them to preserve literal backslashes in escaped unit names let escaped = vm_unit.replace('\\', "\\\\"); cmd_args.push(format!("--property=PartOf={escaped}")); }
cmd_args.push("--".to_string()); cmd_args.push(prog.to_string()); cmd_args.extend(args.iter().map(|s| s.to_string()));
let refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect(); run(&systemd_run_bin(), &refs)}
pub fn run_service( unit_name: &str, slice: &str, properties: &[String], prog: &str, args: &[&str],) -> Result<Output> { let unit_arg = format!("--unit={unit_name}"); let slice_arg = format!("--slice={slice}"); let mut cmd_args: Vec<String> = vec![ "--user".to_string(), "--collect".to_string(), unit_arg, slice_arg, "--property=Type=exec".to_string(), ];
for prop in properties { cmd_args.push(format!("--property={prop}")); }
cmd_args.push("--".to_string()); cmd_args.push(prog.to_string()); cmd_args.extend(args.iter().map(|s| s.to_string()));
let refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect(); run(&systemd_run_bin(), &refs)}
pub fn journal_for_unit(unit_name: &str) -> Result<String> { let out = run( "journalctl", &["--user", "--unit", unit_name, "--no-pager", "--output=cat"], )?; Ok(out.stdout)}
pub fn unit_active_enter_timestamp(unit_name: &str) -> Result<Option<String>> { let out = run( &systemctl_bin(), &[ "--user", "show", unit_name, "--property=ActiveEnterTimestamp", ], )?; // Output: "ActiveEnterTimestamp=Thu 2026-03-20 10:30:00 UTC" or empty if let Some(value) = out.stdout.strip_prefix("ActiveEnterTimestamp=") { if value.is_empty() { return Ok(None); } return Ok(Some(value.to_string())); } Ok(None)}
pub fn unit_is_active(unit_name: &str) -> Result<bool> { let out = run(&systemctl_bin(), &["--user", "is-active", unit_name])?; Ok(out.stdout == "active")}
pub fn stop_unit(unit_name: &str) -> Result<()> { let out = run(&systemctl_bin(), &["--user", "stop", unit_name])?; if !out.success() { bail!("failed to stop unit {unit_name}: {}", out.stderr); } Ok(())}
/// Send SIGKILL to all processes of a unit, bypassing ExecStop.pub fn kill_unit(unit_name: &str) -> Result<()> { let out = run( &systemctl_bin(), &["--user", "kill", "--signal=SIGKILL", unit_name], )?; if !out.success() { bail!("failed to kill unit {unit_name}: {}", out.stderr); } Ok(())}
#[cfg(test)]mod tests { use super::*;
#[test] fn unit_id_is_8_hex_chars() { let id = generate_unit_id(); assert_eq!(id.len(), 8); assert!(id.chars().all(|c| c.is_ascii_hexdigit())); }
#[test] fn unit_ids_are_unique() { let a = generate_unit_id(); let b = generate_unit_id(); assert_ne!(a, b); }
#[test] fn hex_encode_works() { assert_eq!(hex_encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef"); assert_eq!(hex_encode(&[0x00, 0xff]), "00ff"); }
#[test] fn run_captures_stdout() { let out = run("echo", &["hello"]).unwrap(); assert!(out.success()); assert_eq!(out.stdout, "hello"); }
#[test] fn run_captures_exit_code() { let out = run("false", &[]).unwrap(); assert!(!out.success()); assert_eq!(out.status, 1); }
#[test] fn run_with_env_sets_vars() { let out = run_with_env("sh", &["-c", "echo $TEST_VAR"], &[("TEST_VAR", "works")]).unwrap(); assert_eq!(out.stdout, "works"); }
#[test] fn require_binary_succeeds_for_echo() { require_binary("echo", "coreutils").unwrap(); }
#[test] fn require_binary_fails_for_missing() { let result = require_binary("nonexistent-binary-xyz", "fake-package"); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!(msg.contains("nonexistent-binary-xyz")); assert!(msg.contains("fake-package")); }}