rplc 0.3.0

PLC programming in Rust
Documentation
use clap::Parser;
use colored::Colorize;
use eva_common::EResult;
use prettytable::Row;
use rplc::tasks::{Affinity, Status};
use rplc::{client, eapi};
use std::collections::BTreeMap;
use std::path::Path;

#[macro_use]
extern crate prettytable;

#[derive(Parser)]
struct Args {
    #[clap(long = "color")]
    color: Option<Color>,
    #[clap(subcommand)]
    command: Command,
}

#[derive(clap::ValueEnum, Copy, Clone)]
enum Color {
    Always,
    Never,
}

impl Color {
    fn ovrride(self) {
        colored::control::set_override(match self {
            Color::Always => true,
            Color::Never => false,
        });
    }
}

#[derive(Parser)]
enum Command {
    #[clap(about = "list PLCs")]
    List(ListParams),
    #[clap(about = "Test PLC")]
    Test(PlcParams),
    #[clap(about = "PLC info")]
    Info(PlcParams),
    #[clap(about = "PLC task (thread) stats")]
    Stat(PlcParams),
    #[clap(about = "reset PLC task (thread) stats")]
    Reset(PlcParams),
    #[clap(about = "register PLC binary in systemd")]
    Register(PlcRegisterParams),
    #[clap(about = "unregister PLC binary from systemd (stop if running)")]
    Unregister(PlcParams),
    #[clap(about = "start PLC with systemd")]
    Start(PlcParams),
    #[clap(about = "stop PLC with systemd")]
    Stop(PlcParams),
    #[clap(about = "restart PLC with systemd")]
    Restart(PlcParams),
    #[clap(about = "PLC systemd status")]
    Status(PlcParams),
}

#[derive(Parser)]
struct PlcRegisterParams {
    plc_file_path: String,
    #[clap(short = 'a', help = "thread affinity: NAME=CPU,PRIORITY")]
    thread_affinity: Vec<String>,
    #[clap(
        short = 'e',
        long = "eapi",
        help = "EVA ICS bus connection: path[,timeout=Xs][,buf_size=X][,queue_size=X][,buf_ttl=Xms]"
    )]
    eapi: Option<String>,
    #[clap(long = "var", help = "Custom environment variable: name=value")]
    vars: Vec<String>,
    #[clap(long = "force")]
    force: bool,
    #[clap(short = 's', long = "start", help = "start PLC after registration")]
    start: bool,
}

trait StatusColored {
    fn as_colored_string(&self) -> colored::ColoredString;
}

impl StatusColored for Status {
    fn as_colored_string(&self) -> colored::ColoredString {
        if *self == Status::Active {
            self.to_string().green()
        } else if *self <= Status::Stopping {
            self.to_string().yellow()
        } else {
            self.to_string().normal()
        }
    }
}

#[derive(Parser)]
struct ListParams {
    #[clap(short = 'y', long = "full")]
    full: bool,
}

#[derive(Parser)]
struct PlcParams {
    name: String,
}

fn ctable(titles: &[&str]) -> prettytable::Table {
    let mut table = prettytable::Table::new();
    let format = prettytable::format::FormatBuilder::new()
        .column_separator(' ')
        .borders(' ')
        .separators(
            &[prettytable::format::LinePosition::Title],
            prettytable::format::LineSeparator::new('-', '-', '-', '-'),
        )
        .padding(0, 1)
        .build();
    table.set_format(format);
    let mut titlevec: Vec<prettytable::Cell> = Vec::new();
    for t in titles {
        titlevec.push(cell!(t.blue()));
    }
    table.set_titles(prettytable::Row::new(titlevec));
    table
}

async fn handle_list(p: ListParams, var_dir: &Path) -> EResult<()> {
    if p.full {
        let mut table = ctable(&[
            "name",
            "description",
            "version",
            "systemd",
            "status",
            "pid",
            "uptime",
        ]);
        for r in client::list_extended(var_dir).await? {
            let systemd = r
                .sds
                .and_then(|v| v.status)
                .map_or_else(String::new, |v| v.to_string());
            let plc_info = r.plc_info;
            let status = if plc_info.status == -1000 {
                "API_ERROR".red()
            } else {
                Status::from(plc_info.status).as_colored_string()
            };
            table.add_row(row![
                plc_info.name,
                plc_info.description,
                plc_info.version,
                systemd,
                status,
                plc_info.pid,
                plc_info.uptime.trunc()
            ]);
        }
        table.printstd();
    } else {
        for name in client::list(var_dir).await? {
            println!("{}", name);
        }
    }
    Ok(())
}

async fn handle_stat(p: PlcParams, var_dir: &Path) -> EResult<()> {
    let tasks = client::stat_extended(&p.name, var_dir).await?;
    let mut table = ctable(&[
        "task", "spid", "cpu", "rt", "iters", "jmin", "jmax", "jlast", "javg",
    ]);
    for task in tasks {
        let mut cols = vec![
            cell!(task.name),
            cell!(task.spid.to_string().green()),
            cell!(task.cpu_id.to_string().bold()),
            cell!(if task.rt_priority > 0 {
                task.rt_priority.to_string().cyan()
            } else {
                String::new().normal()
            }),
        ];
        if let Some(t) = task.thread_info {
            let cols_t = vec![
                cell!(t.iters),
                cell!(t.jitter_min),
                cell!(if t.jitter_max < 150 {
                    t.jitter_max.to_string().normal()
                } else if t.jitter_max < 250 {
                    t.jitter_max.to_string().yellow()
                } else {
                    t.jitter_max.to_string().red()
                }),
                cell!(t.jitter_last),
                cell!(t.jitter_avg),
            ];
            cols.extend(cols_t);
        }
        table.add_row(Row::new(cols));
    }
    table.printstd();
    Ok(())
}

async fn handle_info(p: PlcParams, var_dir: &Path) -> EResult<()> {
    let result = client::info(&p.name, var_dir).await?;
    let mut table = ctable(&["variable", "value"]);
    table.add_row(row!["name", result.name]);
    table.add_row(row!["description", result.description]);
    table.add_row(row!["version", result.version]);
    table.add_row(row![
        "status",
        Status::from(result.status).as_colored_string()
    ]);
    table.add_row(row!["pid", result.pid]);
    table.add_row(row!["system_name", result.system_name]);
    table.add_row(row!["uptime", result.uptime.trunc()]);
    table.printstd();
    Ok(())
}

async fn handle_start(name: &str) -> EResult<()> {
    client::start(name).await?;
    println!("{} has been started", name);
    Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> EResult<()> {
    eva_common::self_test();
    let args = Args::parse();
    let var_dir = rplc::var_dir();
    if let Some(color) = args.color {
        color.ovrride();
    }
    match args.command {
        Command::List(p) => {
            handle_list(p, &var_dir).await?;
        }
        Command::Test(p) => {
            client::test(&p.name, &var_dir).await?;
            println!("OK");
        }
        Command::Info(p) => {
            handle_info(p, &var_dir).await?;
        }
        Command::Stat(p) => {
            handle_stat(p, &var_dir).await?;
        }
        Command::Reset(p) => {
            client::reset_stat(&p.name, &var_dir).await?;
            println!("{} stats have been reset", p.name);
        }
        Command::Register(p) => {
            let aff: BTreeMap<String, Affinity> = p
                .thread_affinity
                .into_iter()
                .map(|a| {
                    let mut sp = a.split('=');
                    let name = sp.next().unwrap().to_owned();
                    let a: Affinity = sp
                        .next()
                        .ok_or_else(|| panic!("no affinity specified"))
                        .unwrap()
                        .parse()
                        .unwrap();
                    (name, a)
                })
                .collect();
            let eapi_params: Option<eapi::Params> = p.eapi.map(|s| s.parse().unwrap());
            let (name, svc_name) = client::register(
                Path::new(&p.plc_file_path),
                &var_dir,
                p.force,
                &aff,
                eapi_params.as_ref(),
                &p.vars,
            )
            .await?;
            println!(
                "{} has been registered in systemd as: {} ({})",
                p.plc_file_path, name, svc_name
            );
            if p.force {
                handle_start(&name).await?;
            }
        }
        Command::Unregister(p) => {
            client::unregister(&p.name).await?;
            println!("{} has been unregistered from systemd", p.name);
        }
        Command::Start(p) => {
            handle_start(&p.name).await?;
        }
        Command::Stop(p) => {
            client::stop(&p.name).await?;
            println!("{} has been stopped", p.name);
        }
        Command::Restart(p) => {
            client::restart(&p.name).await?;
            println!("{} has been restarted", p.name);
        }
        Command::Status(p) => {
            println!("{}", client::status(&p.name).await?);
        }
    }
    Ok(())
}