提交 cc1c2ddb authored 作者: Serhij S's avatar Serhij S

robo new and cli refactoring

上级 bb8bd1e9
...@@ -985,7 +985,7 @@ dependencies = [ ...@@ -985,7 +985,7 @@ dependencies = [
[[package]] [[package]]
name = "roboplc-cli" name = "roboplc-cli"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"clap", "clap",
"colored", "colored",
......
[package] [package]
name = "roboplc-cli" name = "roboplc-cli"
version = "0.1.10" version = "0.1.11"
edition = "2021" edition = "2021"
authors = ["Serhij S. <div@altertech.com>"] authors = ["Serhij S. <div@altertech.com>"]
license = "Apache-2.0" license = "Apache-2.0"
......
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser)]
#[clap(author = "Bohemia Automation (https://bma.ai)",
version = env!("CARGO_PKG_VERSION"),
about = env!("CARGO_PKG_DESCRIPTION"))]
pub struct Args {
#[clap(short = 'T', long, help = "Manager API timeout")]
pub timeout: Option<u64>,
#[clap(short = 'U', long, env = "ROBOPLC_URL", help = "Manager URL")]
pub url: Option<String>,
#[clap(short = 'k', long, env = "ROBOPLC_KEY", help = "Management key")]
pub key: Option<String>,
#[clap(subcommand)]
pub subcmd: SubCommand,
}
#[derive(Parser)]
pub enum SubCommand {
#[clap(name = "new", about = "Generate a new project")]
New(NewCommand),
#[clap(name = "stat", about = "Get program status")]
Stat,
#[clap(name = "config", about = "Switch remote into CONFIG mode")]
Config,
#[clap(name = "run", about = "Switch remote into RUN mode")]
Run,
#[clap(name = "flash", about = "Flash program")]
Flash(FlashCommand),
#[clap(name = "purge", about = "Purge program data directory")]
Purge,
}
#[derive(Parser)]
pub struct NewCommand {
#[clap(help = "Project name")]
pub name: String,
#[clap(long, help = "RoboPLC crate features")]
pub features: Vec<String>,
#[clap(last(true), help = "extra cargo arguments")]
pub extras: Vec<String>,
}
#[derive(Parser)]
pub struct FlashCommand {
#[clap(long, env = "CARGO", help = "cargo/cross binary path")]
pub cargo: Option<PathBuf>,
#[clap(long, help = "Override remote cargo target")]
pub cargo_target: Option<String>,
#[clap(long, help = "Extra cargo arguments")]
pub cargo_args: Option<String>,
#[clap(long, help = "Do not compile a Rust project, use a file instead")]
pub file: Option<PathBuf>,
#[clap(long, help = "Force flash (automatically put remote in CONFIG mode)")]
pub force: bool,
#[clap(long, help = "Put remote in RUN mode after flashing")]
pub run: bool,
}
use core::fmt;
use std::{env, path::PathBuf};
use colored::Colorize;
use serde::{Deserialize, Serialize};
pub const CONFIG_FILE_NAME: &str = "robo.toml";
#[derive(Debug, Serialize, Deserialize)]
pub struct State {
pid: Option<u32>,
mode: Mode,
memory_used: Option<u64>,
run_time: Option<u64>,
}
impl State {
pub fn print_std(&self) {
let mode_colored = match self.mode {
Mode::Run => format!("{}", self.mode).green(),
Mode::Config => format!("{}", self.mode).yellow(),
Mode::Unknown => format!("{}", self.mode).red(),
};
println!("Mode {}", mode_colored);
if let Some(pid) = self.pid {
println!("PID {}", pid);
}
if let Some(memory) = self.memory_used {
println!("Mem {}", memory);
}
if let Some(run_time) = self.run_time {
println!("Up {}", run_time);
}
}
}
#[derive(Deserialize)]
pub struct KernelInfo {
machine: String,
}
impl KernelInfo {
pub fn to_machine_cargo_target(&self) -> String {
format!("{}-unknown-linux-gnu", self.machine)
}
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Run,
Config,
Unknown,
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Mode::Run => write!(f, "RUN"),
Mode::Config => write!(f, "CONFIG"),
Mode::Unknown => write!(f, "UNKNOWN"),
}
}
}
pub fn find_robo_toml() -> Option<PathBuf> {
let mut current_dir = env::current_dir().ok()?;
loop {
let mut cargo_toml_path = current_dir.clone();
cargo_toml_path.push("Cargo.toml");
if cargo_toml_path.exists() {
let mut roboplc_toml_path = current_dir.clone();
roboplc_toml_path.push(CONFIG_FILE_NAME);
if roboplc_toml_path.exists() {
return Some(roboplc_toml_path);
}
}
if !current_dir.pop() {
break;
}
}
None
}
#[allow(clippy::unnecessary_wraps)]
pub fn report_ok() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", "OK".green());
Ok(())
}
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Config {
#[serde(default)]
pub remote: Remote,
#[serde(default)]
pub build: Build,
}
#[derive(Deserialize, Serialize, Default)]
pub struct Remote {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
}
#[derive(Deserialize, Serialize, Default)]
pub struct Build {
#[serde(skip_serializing_if = "Option::is_none")]
pub cargo: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cargo_args: Option<String>,
}
use std::{
env, fs,
path::{Path, PathBuf},
};
use colored::Colorize as _;
use serde_json::json;
use ureq::Agent;
use ureq_multipart::MultipartBuilder;
use which::which;
use crate::{
arguments::FlashCommand,
common::{report_ok, KernelInfo},
config,
ureq_err::PrintErr,
API_PREFIX,
};
fn flash_file(
url: &str,
key: &str,
agent: Agent,
file: PathBuf,
force: bool,
run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let (content_type, data) = MultipartBuilder::new()
.add_file("file", file)?
.add_text(
"params",
&serde_json::to_string(&json! {
{
"force": force,
"run": run,
}
})?,
)?
.finish()?;
agent
.post(&format!("{}{}/flash", url, API_PREFIX))
.set("x-auth-key", key)
.set("content-type", &content_type)
.send_bytes(&data)
.process_error()?;
Ok(())
}
pub fn flash(
url: &str,
key: &str,
agent: Agent,
opts: FlashCommand,
build_config: config::Build,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = opts.file {
flash_file(url, key, agent, file, opts.force, opts.run)?;
} else {
let mut cargo_target: Option<String> = None;
if let Some(c) = opts.cargo_target {
cargo_target.replace(c);
}
if cargo_target.is_none() {
cargo_target = build_config.target;
}
if cargo_target.is_none() {
let resp = agent
.post(&format!("{}{}/query.info.kernel", url, API_PREFIX))
.set("x-auth-key", key)
.call()?;
let info: KernelInfo = resp.into_json()?;
cargo_target.replace(info.to_machine_cargo_target());
}
let mut cargo: Option<PathBuf> = None;
if let Some(c) = opts.cargo {
cargo.replace(c);
}
if cargo.is_none() {
cargo = build_config.cargo;
}
if cargo.is_none() {
cargo = which("cross").ok();
}
let cargo_target = cargo_target.unwrap();
let cargo = cargo.unwrap_or_else(|| "cargo".into());
let Some(name) = find_name_and_chdir() else {
return Err("Could not find Cargo.toml/binary name".into());
};
let mut cargo_args = None;
if let Some(args) = opts.cargo_args {
cargo_args.replace(args);
} else {
cargo_args = build_config.cargo_args;
}
let binary_name = Path::new("target")
.join(&cargo_target)
.join("release")
.join(name);
let mut args: Vec<String> = vec![
"build".into(),
"--release".into(),
"--target".into(),
cargo_target.clone(),
];
if let Some(extra) = cargo_args {
args.extend(shlex::split(&extra).expect("Invalid cargo args"));
}
println!("Remote: {}", url.yellow());
println!(
"Cargo command line: {} {}",
cargo.display().to_string().yellow(),
args.join(" ").yellow()
);
println!("Cargo target: {}", cargo_target.yellow());
println!("Binary: {}", binary_name.display().to_string().yellow());
println!("Compiling...");
let result = std::process::Command::new(cargo).args(args).status()?;
if !result.success() {
return Err("Compilation failed".into());
}
println!("Flashing...");
flash_file(url, key, agent, binary_name, opts.force, opts.run)?;
}
report_ok()
}
fn find_name_and_chdir() -> Option<String> {
let mut current_dir = env::current_dir().ok()?;
loop {
let mut cargo_toml_path = current_dir.clone();
cargo_toml_path.push("Cargo.toml");
if cargo_toml_path.exists() {
let contents = fs::read_to_string(cargo_toml_path).ok()?;
let value = contents.parse::<toml::Value>().ok()?;
env::set_current_dir(current_dir).ok()?;
return value["package"]["name"].as_str().map(String::from);
}
if !current_dir.pop() {
break;
}
}
None
}
差异被折叠。
use std::env;
use colored::Colorize as _;
use crate::{
arguments::NewCommand,
common::CONFIG_FILE_NAME,
config::{self, Config},
TPL_DEFAULT_RS,
};
pub fn create(
maybe_url: Option<String>,
maybe_key: Option<String>,
maybe_timeout: Option<u64>,
opts: &NewCommand,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Creating new project {}", opts.name.green());
let mut cmd = std::process::Command::new("cargo");
cmd.arg("new").arg(&opts.name);
if !opts.extras.is_empty() {
cmd.args(&opts.extras);
}
let result = cmd.status()?;
if !result.success() {
return Err("Failed to create new project with cargo".into());
}
let mut current_dir = env::current_dir()?;
current_dir.push(&opts.name);
env::set_current_dir(&current_dir)?;
let mut robo_features: Vec<&str> = Vec::new();
for feature in &opts.features {
for feature in feature.split(',') {
robo_features.push(feature);
}
}
add_dependency("roboplc", &robo_features)?;
add_dependency("tracing", &["log"])?;
let robo_toml = Config {
remote: config::Remote {
key: maybe_key,
url: maybe_url,
timeout: maybe_timeout,
},
build: <_>::default(),
};
std::fs::write(CONFIG_FILE_NAME, toml::to_string_pretty(&robo_toml)?)?;
std::fs::write("src/main.rs", prepare_main(TPL_DEFAULT_RS, &robo_features))?;
println!("Project created: {}", opts.name.green());
Ok(())
}
fn add_dependency(name: &str, features: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
println!("Adding dependency {}", name.green());
let mut cmd = std::process::Command::new("cargo");
cmd.arg("add").arg(name);
for feature in features {
cmd.arg("--features").arg(feature);
}
let result = cmd.status()?;
if !result.success() {
return Err(format!("Failed to add dependency {}", name).into());
}
Ok(())
}
#[allow(clippy::let_and_return)]
fn prepare_main(tpl: &str, features: &[&str]) -> String {
let out = if features.contains(&"metrics") {
tpl.replace(
" // METRICS",
r" roboplc::metrics_exporter()
.set_bucket_duration(Duration::from_secs(600))?
.install()?;",
)
} else {
tpl.replace(" // METRICS\n", "")
};
out
}
use ureq::Agent;
use crate::{
common::{report_ok, Mode, State},
ureq_err::{self, PrintErr},
API_PREFIX,
};
pub fn stat(url: &str, key: &str, agent: Agent) -> Result<(), Box<dyn std::error::Error>> {
let resp = agent
.post(&format!("{}{}/query.stats.program", url, API_PREFIX))
.set("x-auth-key", key)
.call()
.process_error()?;
let stats: State = resp.into_json()?;
stats.print_std();
Ok(())
}
pub fn set_mode(
url: &str,
key: &str,
agent: Agent,
mode: Mode,
) -> Result<(), Box<dyn std::error::Error>> {
agent
.post(&format!("{}{}/set.program.mode", url, API_PREFIX))
.set("x-auth-key", key)
.send_json(ureq::json!({
"mode": mode,
}))
.process_error()?;
report_ok()
}
pub fn purge(url: &str, key: &str, agent: Agent) -> Result<(), Box<dyn std::error::Error>> {
ureq_err::PrintErr::process_error(
agent
.post(&format!("{}{}/purge.program.data", url, API_PREFIX))
.set("x-auth-key", key)
.call(),
)?;
report_ok()
}
use colored::Colorize as _;
pub trait PrintErr<T> {
fn process_error(self) -> Result<T, Box<dyn std::error::Error>>;
}
impl<T> PrintErr<T> for Result<T, ureq::Error> {
fn process_error(self) -> Result<T, Box<dyn std::error::Error>> {
match self {
Ok(v) => Ok(v),
Err(e) => match e.kind() {
ureq::ErrorKind::HTTP => {
let response = e.into_response().unwrap();
let status = response.status();
let msg = format!(
"{} ({})",
response.into_string().unwrap_or_default(),
status
);
eprintln!("{}: {}", "Error".red(), msg);
Err("Remote".into())
}
_ => Err(e.into()),
},
}
}
}
use roboplc::controller::prelude::*;
use roboplc::prelude::*;
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
type Message = ();
type Variables = ();
#[derive(WorkerOpts)]
#[worker_opts(cpu = 0, priority = 50, scheduling = "fifo", blocking = true)]
struct Worker1 {}
impl Worker<Message, Variables> for Worker1 {
fn run(&mut self, _context: &Context<(), ()>) -> WResult {
loop {
std::thread::sleep(Duration::from_secs(1));
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
roboplc::configure_logger(roboplc::LevelFilter::Info);
roboplc::thread_rt::prealloc_heap(10_000_000)?;
// METRICS
let mut controller = Controller::<Message, Variables>::new();
controller.spawn_worker(Worker1 {})?;
controller.register_signals(SHUTDOWN_TIMEOUT)?;
controller.block();
Ok(())
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论