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

exec docs

上级 f8931858
差异被折叠。
......@@ -17,6 +17,7 @@ path = "src/main.rs"
clap = { version = "=4.1", features = ["derive", "env"] }
colored = "1"
dirs = "5.0.1"
futures-util = { version = "0.3.30", features = ["sink"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
shlex = "1.3.0"
......@@ -24,8 +25,15 @@ toml = "0.5"
ureq = { version = "2.9.6", features = ["json", "native-certs", "native-tls"] }
ureq_multipart = "1.1.1"
which = "3"
term_size = "0.3.2"
tokio = { version = "=1.36", features = ["rt", "fs"] }
tokio-tungstenite = { version = "0.23.1", features = ["rustls"] }
[target.'cfg(windows)'.dependencies]
ansi_term = "0.12.1"
[profile.release]
strip = true
[target.'cfg(not(target_os = "windows"))'.dependencies]
termios = "0.3.3"
......@@ -44,6 +44,11 @@ pub enum SubCommand {
Restart,
#[clap(name = "flash", about = "Flash program")]
Flash(FlashCommand),
#[clap(
name = "x",
about = "Execute program on the remote host in a virtual terminal"
)]
Exec(ExecCommand),
#[clap(name = "purge", about = "Purge program data directory")]
Purge,
}
......@@ -99,3 +104,65 @@ pub struct FlashCommand {
#[clap(short = 'r', long, help = "Put remote in RUN mode after flashing")]
pub run: bool,
}
#[derive(Parser)]
pub struct ExecCommand {
#[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(
short = 'f',
long,
help = "Force execute (ignore if other program is being executed)"
)]
pub force: bool,
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Arguments after -- are passed to the program as-is"
)]
pub args: Vec<String>,
}
pub struct FlashExec {
pub cargo: Option<PathBuf>,
pub cargo_target: Option<String>,
pub cargo_args: Option<String>,
pub file: Option<PathBuf>,
pub force: bool,
pub run: bool,
pub program_args: Vec<String>,
}
impl From<FlashCommand> for FlashExec {
fn from(cmd: FlashCommand) -> Self {
Self {
cargo: cmd.cargo,
cargo_target: cmd.cargo_target,
cargo_args: cmd.cargo_args,
file: cmd.file,
force: cmd.force,
run: cmd.run,
program_args: Vec::new(),
}
}
}
impl From<ExecCommand> for FlashExec {
fn from(cmd: ExecCommand) -> Self {
Self {
cargo: cmd.cargo,
cargo_target: cmd.cargo_target,
cargo_args: cmd.cargo_args,
file: cmd.file,
force: cmd.force,
run: false,
program_args: cmd.args,
}
}
}
use std::io::Write as _;
use std::path::Path;
use colored::Colorize;
#[cfg(not(target_os = "windows"))]
use tokio::io::AsyncReadExt as _;
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio_tungstenite::tungstenite::Message;
pub fn exec(
url: &str,
key: &str,
file: &Path,
force: bool,
args: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(exec_remote(url, key, file, force, args))?;
Ok(())
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
enum Output {
Error(String),
Terminated(i32),
}
#[allow(clippy::too_many_lines)]
async fn exec_remote(
url: &str,
key: &str,
file: &Path,
force: bool,
args: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let (ws_uri, url_short) = if let Some(u) = url.strip_prefix("http://") {
(format!("ws://{}/roboplc/api/ws.execute", u), u)
} else if let Some(u) = url.strip_prefix("https://") {
(format!("wss://{}/roboplc/api/ws.execute", u), u)
} else {
return Err("Invalid URL".into());
};
println!("Executing on the remote host {}", url_short.green().bold());
println!();
let (mut socket, _) = tokio_tungstenite::connect_async(&ws_uri).await?;
let (width, height) = term_size::dimensions().ok_or("Failed to get terminal size")?;
let payload = json!({
"k": key,
"force": force,
"args": args,
"term": {
"width": width,
"height": height,
"name": std::env::var("TERM").unwrap_or("xterm-256color".to_string()),
},
});
socket
.send(Message::Text(serde_json::to_string(&payload)?))
.await?;
let Some(Ok(Message::Text(msg))) = socket.next().await else {
return Err("Expected text message".into());
};
if msg != "upload" {
if let Ok(Output::Error(e)) = serde_json::from_str::<Output>(&msg) {
return Err(e.into());
}
return Err(format!("Unexpected message: {}", msg).into());
}
let f = tokio::fs::read(file).await?;
socket.send(Message::Binary(f)).await?;
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
#[allow(unused_mut)]
let (mut sender, mut receiver) = socket.split();
#[cfg(target_os = "windows")]
let _ = sender;
// input on windows is currently not supported
#[cfg(not(target_os = "windows"))]
let input_fut = tokio::spawn(async move {
let stdin = std::os::fd::AsRawFd::as_raw_fd(&std::io::stdin().lock());
let mut termios =
termios::Termios::from_fd(stdin).expect("Failed to get termios for stdin");
termios.c_lflag &= !(termios::ICANON | termios::ECHO);
termios::tcsetattr(stdin, termios::TCSANOW, &termios)
.expect("Failed to set termios for stdin");
let mut f = unsafe { <tokio::fs::File as std::os::fd::FromRawFd>::from_raw_fd(stdin) };
let buf = &mut [0u8; 4096];
while let Ok(b) = f.read(buf).await {
if b == 0 {
break;
}
if let Err(e) = sender.send(Message::Binary(buf[..b].to_vec())).await {
eprintln!("Error sending input: {}", e);
break;
}
}
});
macro_rules! handle_out {
($out: expr) => {
let Some(Ok(Message::Binary(b))) = receiver.next().await else {
return Err("Expected binary message".into());
};
$out.write_all(&b)?;
$out.flush()?;
};
}
while let Some(Ok(msg)) = receiver.next().await {
if let Message::Text(m) = msg {
match m.as_str() {
"o" => {
handle_out!(stdout);
}
"e" => {
handle_out!(stderr);
}
v => {
let output = serde_json::from_str::<Output>(v)?;
match output {
Output::Error(e) => {
eprintln!("Program error: {}", e);
break;
}
Output::Terminated(code) => {
if code == 0 {
std::process::exit(0);
} else {
eprintln!("Program terminated with code {}", code);
std::process::exit(code);
}
}
}
}
}
}
}
// actually unreachable
#[cfg(not(target_os = "windows"))]
input_fut.abort();
Ok(())
}
......@@ -10,13 +10,14 @@ use ureq_multipart::MultipartBuilder;
use which::which;
use crate::{
arguments::FlashCommand,
arguments::FlashExec,
common::{report_ok, KernelInfo},
config,
ureq_err::PrintErr,
API_PREFIX,
};
#[allow(clippy::too_many_arguments)]
fn flash_file(
url: &str,
key: &str,
......@@ -24,10 +25,15 @@ fn flash_file(
file: &Path,
force: bool,
run: bool,
exec_only: bool,
program_args: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
if !file.exists() {
return Err(format!("File not found: {}", file.display()).into());
}
if exec_only {
return crate::exec::exec(url, key, file, force, program_args);
}
let (content_type, data) = MultipartBuilder::new()
.add_file("file", file)?
.add_text(
......@@ -50,6 +56,7 @@ fn flash_file(
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_build_custom(
url: &str,
key: &str,
......@@ -58,6 +65,8 @@ fn run_build_custom(
run: bool,
cmd: &str,
file: &Path,
exec_only: bool,
program_args: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Remote: {}", url.yellow());
println!("Build command line: {}", cmd.yellow());
......@@ -73,20 +82,31 @@ fn run_build_custom(
if !file.exists() {
return Err(format!("File not found: {}", file.display()).into());
}
flash_file(url, key, agent, file, force, run)?;
flash_file(url, key, agent, file, force, run, exec_only, program_args)?;
Ok(())
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub fn flash(
url: &str,
key: &str,
agent: Agent,
opts: FlashCommand,
opts: FlashExec,
build_config: config::Build,
build_custom: config::BuildCustom,
exec_only: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = opts.file {
flash_file(url, key, agent, &file, opts.force, opts.run)?;
flash_file(
url,
key,
agent,
&file,
opts.force,
opts.run,
exec_only,
opts.program_args,
)?;
} else if let Some(custom_cmd) = build_custom.command {
run_build_custom(
url,
......@@ -98,6 +118,8 @@ pub fn flash(
&build_custom
.file
.ok_or("Custom build command requires a file")?,
exec_only,
opts.program_args,
)?;
} else {
let mut cargo_target: Option<String> = None;
......@@ -163,7 +185,16 @@ pub fn flash(
return Err("Compilation failed".into());
}
println!("Flashing...");
flash_file(url, key, agent, &binary_name, opts.force, opts.run)?;
flash_file(
url,
key,
agent,
&binary_name,
opts.force,
opts.run,
exec_only,
opts.program_args,
)?;
}
report_ok()
}
......
......@@ -14,6 +14,7 @@ const TPL_DEFAULT_RS: &str = include_str!("../tpl/default.rs");
mod arguments;
mod common;
mod config;
mod exec;
mod flashing;
mod project;
mod remote;
......@@ -98,9 +99,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
&url,
&key,
agent,
opts,
opts.into(),
build_config.unwrap_or_default(),
build_custom.unwrap_or_default(),
false,
)?;
}
SubCommand::Exec(opts) => {
flashing::flash(
&url,
&key,
agent,
opts.into(),
build_config.unwrap_or_default(),
build_custom.unwrap_or_default(),
true,
)?;
}
SubCommand::Purge => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论