mod app;
mod daemon;
mod ui;
use anyhow::{Context, Result, bail};
use app::{App, Cli, CliCommand};
use clap::Parser;
fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(command) = cli.command.clone() {
run_cli_command(&cli, command)
} else {
App::new(cli).run()
}
}
fn run_cli_command(cli: &Cli, command: CliCommand) -> Result<()> {
match command {
CliCommand::List => print_daemon_list(cli),
CliCommand::Status { unit } => print_status(cli, unit.as_deref()),
CliCommand::Journal { unit } => print_journal(cli, &unit),
CliCommand::Events { unit } => print_events(cli, &unit),
CliCommand::Check { unit } => check_unit(&unit),
CliCommand::Failed => print_failed_services(),
CliCommand::Active { unit } => run_unit_query(&unit, "is-active"),
CliCommand::Enabled { unit } => run_unit_query(&unit, "is-enabled"),
CliCommand::Start { unit } => run_action(&unit, daemon::DaemonAction::Start),
CliCommand::Stop { unit } => run_action(&unit, daemon::DaemonAction::Stop),
CliCommand::Restart { unit } => run_action(&unit, daemon::DaemonAction::Restart),
CliCommand::TryRestart { unit } => run_action(&unit, daemon::DaemonAction::TryRestart),
CliCommand::Reload { unit } => run_action(&unit, daemon::DaemonAction::Reload),
CliCommand::ReloadOrRestart { unit } => {
run_action(&unit, daemon::DaemonAction::ReloadOrRestart)
}
CliCommand::Enable { unit } => run_action(&unit, daemon::DaemonAction::Enable),
CliCommand::Disable { unit } => run_action(&unit, daemon::DaemonAction::Disable),
CliCommand::Mask { unit } => run_action(&unit, daemon::DaemonAction::Mask),
CliCommand::Unmask { unit } => run_action(&unit, daemon::DaemonAction::Unmask),
CliCommand::DaemonReload => run_systemctl(&["daemon-reload"]),
CliCommand::Term { unit } => run_action(&unit, daemon::DaemonAction::KillTerm),
CliCommand::Kill { unit } => run_action(&unit, daemon::DaemonAction::KillKill),
}
}
fn print_daemon_list(cli: &Cli) -> Result<()> {
let mut app = App::new(cli.clone());
app.daemons = daemon::load_daemons()?;
let rows = app.visible_indices();
println!(
"{:<44} {:<18} {:>8} {:>10} SIGNAL",
"UNIT", "STATE", "PID", "MEM"
);
for idx in rows {
let daemon = &app.daemons[idx];
println!(
"{:<44} {:<18} {:>8} {:>10} {}",
daemon.unit,
daemon.state_label(),
daemon
.main_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon::format_bytes(daemon.memory_bytes),
daemon.anomaly_summary()
);
}
Ok(())
}
fn print_failed_services() -> Result<()> {
let daemons = daemon::load_daemons()?;
let failed = daemons
.iter()
.filter(|daemon| daemon.active == "failed" || daemon.severity() > 0)
.collect::<Vec<_>>();
if failed.is_empty() {
println!("no failed or anomalous services");
return Ok(());
}
println!("{:<44} {:<18} {:>8} SIGNAL", "UNIT", "STATE", "SEVERITY");
for daemon in failed {
println!(
"{:<44} {:<18} {:>8} {}",
daemon.unit,
daemon.state_label(),
daemon.severity(),
daemon.anomaly_summary()
);
}
Ok(())
}
fn check_unit(unit: &str) -> Result<()> {
let unit = daemon::service_unit(unit);
let daemons = daemon::load_daemons()?;
let daemon = daemons
.iter()
.find(|daemon| daemon.unit == unit)
.with_context(|| format!("service {unit} was not found"))?;
println!("{}", check_line(daemon));
if daemon.active == "active" && daemon.severity() == 0 {
Ok(())
} else {
bail!(
"{} is {} with signal {}",
daemon.unit,
daemon.state_label(),
daemon.anomaly_summary()
)
}
}
fn check_line(daemon: &daemon::Daemon) -> String {
format!(
"{} {} load={} enabled={} pid={} restarts={} memory={} signal={}",
daemon.unit,
daemon.state_label(),
dash(&daemon.load),
dash(&daemon.unit_file_state),
daemon
.main_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon
.restarts
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon::format_bytes(daemon.memory_bytes),
daemon.anomaly_summary()
)
}
fn dash(value: &str) -> &str {
if value.is_empty() { "-" } else { value }
}
fn print_status(cli: &Cli, unit: Option<&str>) -> Result<()> {
if let Some(unit) = unit {
return print_unit_status(unit);
}
print_status_overview(cli)
}
fn print_unit_status(unit: &str) -> Result<()> {
let unit = daemon::service_unit(unit);
let report = daemon::status_report(&unit)?;
print_report(&report)
}
fn print_status_overview(cli: &Cli) -> Result<()> {
let mut app = App::new(cli.clone());
app.daemons = daemon::load_daemons()?;
let active = app
.daemons
.iter()
.filter(|daemon| daemon.active == "active")
.count();
let running = app
.daemons
.iter()
.filter(|daemon| daemon.active == "active" && daemon.sub == "running")
.count();
let failed = app
.daemons
.iter()
.filter(|daemon| daemon.active == "failed")
.count();
let anomalous = app
.daemons
.iter()
.filter(|daemon| daemon.severity() > 0)
.count();
println!(
"services total={} active={} running={} failed={} anomalous={}",
app.daemons.len(),
active,
running,
failed,
anomalous
);
let rows = app
.visible_indices()
.into_iter()
.filter(|idx| {
let daemon = &app.daemons[*idx];
daemon.active == "active" && daemon.sub == "running"
})
.collect::<Vec<_>>();
if rows.is_empty() {
println!("no running services in the current view");
return Ok(());
}
println!(
"{:<44} {:<18} {:>8} {:>10} {:>8} SIGNAL",
"UNIT", "STATE", "PID", "MEM", "RESTARTS"
);
for idx in rows {
let daemon = &app.daemons[idx];
println!("{}", status_overview_line(daemon));
}
Ok(())
}
fn status_overview_line(daemon: &daemon::Daemon) -> String {
format!(
"{:<44} {:<18} {:>8} {:>10} {:>8} {}",
daemon.unit,
daemon.state_label(),
daemon
.main_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon::format_bytes(daemon.memory_bytes),
daemon
.restarts
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon.anomaly_summary()
)
}
fn print_journal(cli: &Cli, unit: &str) -> Result<()> {
let unit = daemon::service_unit(unit);
for line in daemon::load_journal(&unit, cli.lookback_hours, cli.journal_lines)? {
println!("{line}");
}
Ok(())
}
fn print_events(cli: &Cli, unit: &str) -> Result<()> {
let unit = daemon::service_unit(unit);
let journal = daemon::load_journal(&unit, cli.lookback_hours, cli.journal_lines)?;
for line in daemon::notable_events(&journal) {
println!("{line}");
}
Ok(())
}
fn run_unit_query(unit: &str, command: &str) -> Result<()> {
let unit = daemon::service_unit(unit);
run_systemctl(&[command, &unit])
}
fn run_action(unit: &str, action: daemon::DaemonAction) -> Result<()> {
let unit = daemon::service_unit(unit);
let report = daemon::apply_action(&unit, action)?;
print_report(&report)
}
fn run_systemctl(args: &[&str]) -> Result<()> {
let report = daemon::run_systemctl(args)?;
print_report(&report)
}
fn print_report(report: &daemon::CommandReport) -> Result<()> {
if !report.output.is_empty() {
println!("{}", report.output);
}
if report.ok {
Ok(())
} else {
anyhow::bail!("{} failed", report.command)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_line_is_script_friendly() {
let daemon = daemon::Daemon {
unit: "demo.service".to_string(),
load: "loaded".to_string(),
active: "active".to_string(),
sub: "running".to_string(),
main_pid: Some(42),
restarts: Some(1),
memory_bytes: Some(2048),
unit_file_state: "enabled".to_string(),
..daemon::Daemon::default()
};
assert_eq!(
check_line(&daemon),
"demo.service active/running load=loaded enabled=enabled pid=42 restarts=1 memory=2.0 KiB signal=ok"
);
}
#[test]
fn status_overview_line_has_running_service_data() {
let daemon = daemon::Daemon {
unit: "demo.service".to_string(),
active: "active".to_string(),
sub: "running".to_string(),
main_pid: Some(42),
restarts: Some(2),
memory_bytes: Some(4096),
..daemon::Daemon::default()
};
let line = status_overview_line(&daemon);
assert!(line.contains("demo.service"));
assert!(line.contains("active/running"));
assert!(line.contains("42"));
assert!(line.contains("4.0 KiB"));
assert!(line.ends_with("ok"));
}
}