ostool-server 0.2.1

Server for managing development boards, serial sessions, and TFTP artifacts
use std::{net::Ipv4Addr, path::PathBuf, process::Command, sync::Mutex};

use anyhow::{Context, bail};
use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TftpStatus {
    pub provider: String,
    pub enabled: bool,
    pub healthy: bool,
    pub writable: bool,
    pub resolved_server_ip: Option<String>,
    pub resolved_netmask: Option<String>,
    pub root_dir: PathBuf,
    pub bind_addr_or_address: Option<String>,
    pub service_state: Option<String>,
    pub last_error: Option<String>,
}

pub fn set_last_error(store: &Mutex<Option<String>>, error: impl Into<String>) {
    *store.lock().unwrap() = Some(error.into());
}

pub fn clear_last_error(store: &Mutex<Option<String>>) {
    *store.lock().unwrap() = None;
}

pub fn current_last_error(store: &Mutex<Option<String>>) -> Option<String> {
    store.lock().unwrap().clone()
}

pub fn resolve_interface_ipv4(interface_name: &str) -> anyhow::Result<Option<String>> {
    if interface_name.trim().is_empty() {
        return Ok(None);
    }

    let interfaces = NetworkInterface::show()?;
    for interface in interfaces {
        if interface.name != interface_name {
            continue;
        }

        for addr in interface.addr {
            if let Addr::V4(v4) = addr {
                return Ok(Some(v4.ip.to_string()));
            }
        }
    }

    Ok(None)
}

pub fn run_capture(program: &str, args: &[&str]) -> anyhow::Result<String> {
    let output = Command::new(program)
        .args(args)
        .output()
        .with_context(|| format!("failed to execute `{program}`"))?;
    if !output.status.success() {
        bail!("command `{program}` exited with status {}", output.status);
    }
    Ok(String::from_utf8(output.stdout)
        .with_context(|| format!("failed to decode output from `{program}`"))?
        .trim()
        .to_string())
}

pub fn system_service_state(service_name: &str) -> Option<String> {
    if !cfg!(target_os = "linux") {
        return None;
    }

    run_capture("systemctl", &["is-active", service_name]).ok()
}

pub fn udp_port_69_is_listening() -> anyhow::Result<bool> {
    if !cfg!(target_os = "linux") {
        return Ok(true);
    }

    let output = run_capture("ss", &["-lun"])?;
    Ok(output.lines().any(|line| {
        let line = line.trim();
        !line.is_empty()
            && !line.starts_with("State")
            && line.split_whitespace().any(|field| {
                field.ends_with(":69")
                    || field.ends_with(":69,")
                    || field.ends_with("]:69")
                    || field == "*:69"
                    || field == "0.0.0.0:69"
                    || field == "[::]:69"
            })
    }))
}

pub fn ipv4_unspecified() -> Ipv4Addr {
    Ipv4Addr::UNSPECIFIED
}