can-utils-rs 0.3.0

A pure Rust CLI utility for managing and manipulating CAN interfaces and printing pretty CAN data.
Documentation
use anyhow::{Result, bail};
use inquire::Select;
use std::env;
use std::fmt;
use std::path::Path;

use crate::setup::exec::run_sudo;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Prerequisite {
    Ip,
    Slcand,
    Modprobe,
}

impl Prerequisite {
    pub fn binary_name(&self) -> &'static str {
        match self {
            Prerequisite::Ip => "ip",
            Prerequisite::Slcand => "slcand",
            Prerequisite::Modprobe => "modprobe",
        }
    }

    pub fn description(&self) -> &'static str {
        match self {
            Prerequisite::Ip => "iproute2 / ip",
            Prerequisite::Slcand => "can-utils / slcand",
            Prerequisite::Modprobe => "kmod / modprobe",
        }
    }
}

impl fmt::Display for Prerequisite {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StartupAction {
    InstallPrerequisites,
    ContinueAnyway,
    Exit,
}

impl fmt::Display for StartupAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            StartupAction::InstallPrerequisites => write!(f, "Install prerequisites"),
            StartupAction::ContinueAnyway => write!(f, "Continue anyway"),
            StartupAction::Exit => write!(f, "Exit"),
        }
    }
}

pub fn command_exists(cmd: &str) -> bool {
    let Some(paths) = env::var_os("PATH") else {
        return false;
    };

    env::split_paths(&paths).any(|dir| Path::new(&dir).join(cmd).exists())
}

pub fn has_apt() -> bool {
    command_exists("apt")
}

pub fn missing_prerequisites() -> Vec<Prerequisite> {
    let all = [
        Prerequisite::Ip,
        Prerequisite::Slcand,
        Prerequisite::Modprobe,
    ];

    all.into_iter()
        .filter(|p| !command_exists(p.binary_name()))
        .collect()
}

pub fn print_missing_prerequisites(missing: &[Prerequisite]) {
    if missing.is_empty() {
        return;
    }

    eprintln!("Missing prerequisites:");
    for item in missing {
        eprintln!("  - {}", item.description());
    }
    eprintln!();
}

pub fn install_missing_prerequisites(missing: &[Prerequisite]) -> Result<()> {
    if missing.is_empty() {
        return Ok(());
    }

    if !has_apt() {
        bail!("automatic installation is currently only supported on apt-based systems");
    }

    let mut packages = Vec::new();

    if missing.iter().any(|p| matches!(p, Prerequisite::Ip)) {
        packages.push("iproute2");
    }
    if missing.iter().any(|p| matches!(p, Prerequisite::Slcand)) {
        packages.push("can-utils");
    }
    if missing.iter().any(|p| matches!(p, Prerequisite::Modprobe)) {
        packages.push("kmod");
    }

    packages.sort_unstable();
    packages.dedup();

    println!("The following packages will be installed:");
    for pkg in &packages {
        println!("  - {pkg}");
    }
    println!();

    let confirm = Select::new("Proceed with installation?", vec!["Yes", "No"]).prompt()?;

    if confirm != "Yes" {
        bail!("installation cancelled by user");
    }

    run_sudo(&["apt", "update"])?;

    let mut args: Vec<&str> = vec!["apt", "install", "-y"];
    args.extend(packages.iter().copied());

    run_sudo(&args)?;

    let still_missing = missing_prerequisites();
    if !still_missing.is_empty() {
        print_missing_prerequisites(&still_missing);
        bail!("some prerequisites are still missing after installation");
    }

    Ok(())
}

pub fn handle_missing_prerequisites() -> Result<()> {
    let missing = missing_prerequisites();

    if missing.is_empty() {
        return Ok(());
    }

    print_missing_prerequisites(&missing);

    let mut actions = Vec::new();
    if has_apt() {
        actions.push(StartupAction::InstallPrerequisites);
    }
    actions.push(StartupAction::ContinueAnyway);
    actions.push(StartupAction::Exit);

    let action = Select::new(
        "Some required tools are missing. What do you want to do?",
        actions,
    )
    .prompt()?;

    match action {
        StartupAction::InstallPrerequisites => install_missing_prerequisites(&missing)?,
        StartupAction::ContinueAnyway => {}
        StartupAction::Exit => bail!("exiting because prerequisites are missing"),
    }

    Ok(())
}