add rudimentary calling of methods on objects

This commit is contained in:
Rokas Puzonas 2023-04-02 18:37:03 +03:00
parent f9561f2883
commit 780be584e6
6 changed files with 792 additions and 346 deletions

147
Cargo.lock generated
View File

@ -375,6 +375,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -898,6 +913,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
@ -1300,6 +1321,21 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
@ -1576,6 +1612,28 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "openssl-sys"
version = "0.9.83"
@ -1704,6 +1762,20 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "plist"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64",
"indexmap",
"line-wrap",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "png"
version = "0.17.7"
@ -1757,6 +1829,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c1a97b1bc42b1d550bfb48d4262153fe400a12bab1511821736f7eac76d7e2"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.26"
@ -1882,6 +1963,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
@ -2147,6 +2234,29 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syntect"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
dependencies = [
"bincode",
"bitflags",
"flate2",
"fnv",
"lazy_static",
"once_cell",
"onig",
"plist",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"thiserror",
"walkdir",
"yaml-rust",
]
[[package]]
name = "tempfile"
version = "3.4.0"
@ -2180,6 +2290,33 @@ dependencies = [
"syn 2.0.10",
]
[[package]]
name = "time"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-skia"
version = "0.8.3"
@ -2338,6 +2475,7 @@ dependencies = [
"shell-escape",
"smol",
"ssh2",
"syntect",
"thiserror",
"tokio",
"toml",
@ -2843,6 +2981,15 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zbus"
version = "3.11.1"

View File

@ -18,6 +18,7 @@ serde_json = "1.0.94"
shell-escape = "0.1.5"
smol = "1.3.0"
ssh2 = "0.9.4"
syntect = "5.0.0"
thiserror = "1.0.40"
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
toml = "0.7.3"

View File

@ -1,17 +1,22 @@
use std::{
default,
net::{SocketAddr, SocketAddrV4, ToSocketAddrs},
net::{SocketAddr, SocketAddrV4},
sync::mpsc::{Receiver, Sender},
vec, rc::Rc,
};
use anyhow::{Error, Result};
use anyhow::Result;
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
use eframe::CreationContext;
use serde::de::IntoDeserializer;
use egui::TextEdit;
use serde_json::Value;
use crate::ubus;
pub enum AsyncEvent {
Connect(Result<AsyncSession<AsyncIoTcpStream>>),
Disconnect(Result<()>)
Disconnect(Result<()>),
ListObjects(Result<Vec<ubus::Object>>),
Call(Result<Value>)
}
pub struct App {
@ -21,6 +26,13 @@ pub struct App {
password: String,
session: Option<AsyncSession<AsyncIoTcpStream>>,
selected_object: Option<Rc<ubus::Object>>,
selected_method: Option<String>,
object_filter: String,
objects: Vec<Rc<ubus::Object>>,
payload: String,
response: Option<Value>,
is_connecting: bool,
is_disconnecting: bool,
@ -39,6 +51,13 @@ impl Default for App {
password: "admin01".to_owned(),
session: None,
object_filter: "".into(),
selected_object: None,
selected_method: None,
payload: "".into(),
response: None,
objects: vec![],
is_connecting: false,
is_disconnecting: false,
@ -48,7 +67,11 @@ impl Default for App {
}
}
async fn connect<A>(socket_addr: A, username: String, password: String) -> Result<AsyncSession<AsyncIoTcpStream>>
async fn connect<A>(
socket_addr: A,
username: String,
password: String,
) -> Result<AsyncSession<AsyncIoTcpStream>>
where
A: Into<SocketAddr>,
{
@ -59,36 +82,52 @@ where
}
async fn disconnect(session: AsyncSession<AsyncIoTcpStream>) -> Result<()> {
session.disconnect(Some(ssh2::DisconnectCode::ByApplication), "Disconnect", Some("en")).await?;
session
.disconnect(
Some(ssh2::DisconnectCode::ByApplication),
"Disconnect",
Some("en"),
)
.await?;
Ok(())
}
impl App {
pub fn init(&mut self, _cc: &CreationContext) {}
fn handle_connect_event(&mut self, result: Result<AsyncSession<AsyncIoTcpStream>>) {
self.is_connecting = false;
match result {
Ok(session) => {
self.session = Some(session)
},
Err(_) => todo!(),
}
}
fn handle_events(&mut self, _ctx: &egui::Context) {
use AsyncEvent::*;
fn handle_disconnect_event(&mut self, result: Result<()>) {
self.is_disconnecting = false;
if let Err(err) = result {
todo!()
}
}
fn handle_events(&mut self, ctx: &egui::Context) {
if let Ok(event) = self.rx.try_recv() {
match event {
AsyncEvent::Connect(result) => self.handle_connect_event(result),
AsyncEvent::Disconnect(result) => self.handle_disconnect_event(result),
Connect(result) => {
self.is_connecting = false;
match result {
Ok(session) => {
self.session = Some(session);
self.start_list_objects()
}
Err(err) => todo!("{}", err),
}
}
Disconnect(result) => {
self.is_disconnecting = false;
if let Err(err) = result {
todo!("{}", err)
}
}
ListObjects(result) => match result {
Ok(objects) => self.objects = objects.into_iter().map(Rc::new).collect(),
Err(err) => todo!("{}", err),
},
Call(result) => match result {
Ok(response) => self.response = Some(response),
Err(err) => todo!("{}", err),
}
}
}
}
@ -97,19 +136,24 @@ impl App {
where
A: Into<SocketAddr>,
{
if self.session.is_some() { return; }
if self.session.is_some() {
return;
}
self.is_connecting = true;
let tx = self.tx.clone();
let socket_addr = socket_addr.into();
tokio::spawn(async move {
let result = connect(socket_addr, username, password).await;
tx.send(AsyncEvent::Connect(result)).expect("Failed to send event");
tx.send(AsyncEvent::Connect(result))
.expect("Failed to send event");
});
}
fn start_disconnect(&mut self) {
if self.session.is_none() { return; }
if self.session.is_none() {
return;
}
self.is_disconnecting = true;
let tx = self.tx.clone();
@ -117,67 +161,231 @@ impl App {
self.session = None;
tokio::spawn(async move {
let result = disconnect(session).await;
tx.send(AsyncEvent::Disconnect(result)).expect("Failed to send event");
tx.send(AsyncEvent::Disconnect(result))
.expect("Failed to send event");
});
}
fn start_call(&mut self, object: String, method: String, message: Option<Value>) {
if self.session.is_none() {
return;
}
let tx = self.tx.clone();
let session = self.session.clone().unwrap();
self.session = None;
tokio::spawn(async move {
let result = ubus::call(&session, &object, &method, message.as_ref()).await;
tx.send(AsyncEvent::Call(result)).expect("Failed to send event");
});
}
fn start_list_objects(&self) {
if self.session.is_none() {
return;
}
let session = self.session.clone().unwrap();
let tx = self.tx.clone();
tokio::spawn(async move {
let result = ubus::list_verbose(&session, None).await;
tx.send(AsyncEvent::ListObjects(result))
.expect("Failed to send event");
});
}
fn show_left_panel(&mut self, ui: &mut egui::Ui) {
use egui::*;
ui.text_edit_singleline(&mut self.object_filter);
egui::ScrollArea::vertical().show(ui, |ui| {
for obj in &self.objects {
CollapsingHeader::new(&obj.name).show(ui, |ui| {
for (name, _) in &obj.methods {
if ui.button(name).clicked() {}
}
});
}
});
}
fn show_right_panel(&mut self, ui: &mut egui::Ui) {
use egui::*;
egui::ScrollArea::vertical()
.show(ui, |ui| {
ui.text_edit_singleline(&mut self.address);
// TODO: ui.text_edit_singleline(&mut self.port);
ui.text_edit_singleline(&mut self.username);
ui.text_edit_singleline(&mut self.password);
if self.is_connecting {
ui.add_enabled(false, Button::new("Connecting..."));
} else if self.session.is_none() {
if ui.button("Connect").clicked() {
let socket_addr = SocketAddrV4::new(self.address.parse().unwrap(), self.port);
self.start_connect(socket_addr, self.username.clone(), self.password.clone());
}
} else {
if ui.button("Disconnect").clicked() {
self.start_disconnect()
}
}
});
}
fn create_default_payload(params: &[(&str, ubus::UbusParamType)]) -> String {
let mut lines = vec![];
for (param_name, param_type) in params {
use ubus::UbusParamType::*;
let param_value = match param_type {
Unknown => "\"<unknown>\"",
Integer => "0",
Boolean => "false",
Table => "{}",
String => "\"\"",
Array => "[]",
Double => "0.00",
};
lines.push(format!("\t\"{}\": {}", &param_name, &param_value));
}
return format!("{{\n{}\n}}", lines.join(",\n"));
}
fn show_central_panel(&mut self, ui: &mut egui::Ui) {
// Object dropdown
{
let object_name = self.selected_object.as_ref().map(|obj| obj.name.as_ref()).unwrap_or("");
let object_combobox = egui::ComboBox::from_id_source("selected_object")
.selected_text(object_name)
.width(200.0)
.show_ui(ui, |ui| {
let mut selection = None;
for object in &self.objects {
ui.selectable_value(&mut selection, Some(object.clone()), &object.name);
}
return selection;
});
match object_combobox.inner {
Some(Some(object)) => {
self.selected_method = None;
self.selected_object = Some(object);
},
_ => {}
};
}
// Method dropdown
{
let selected_method_name = self.selected_method.as_deref().unwrap_or("");
let method_combobox = ui.add_enabled_ui(self.selected_object.is_some(), |ui| {
let mut selection = None;
egui::ComboBox::from_id_source("selected_method")
.selected_text(selected_method_name)
.width(200.0)
.show_ui(ui, |ui| {
if let Some(object) = &self.selected_object {
for method in &object.methods {
let method_name = &method.0;
let mut label_response = ui.selectable_label(selected_method_name == method_name, method_name);
if label_response.clicked() && selected_method_name != method_name {
selection = Some(method_name.clone());
label_response.mark_changed();
}
}
}
});
return selection;
});
match (method_combobox.inner, &self.selected_object) {
(Some(method), Some(object)) => {
let method_params = object.methods.iter()
.find(|(name, _)| name.eq(&method))
.map(|(_, params)| params)
.unwrap()
.iter()
.map(|(param_name, param_type)| (param_name.as_str(), *param_type))
.collect::<Vec<_>>();
self.payload = App::create_default_payload(&method_params);
self.selected_method = Some(method);
},
_ => {}
};
}
let call_enabled = self.selected_object.is_some() && self.selected_method.is_some();
ui.add_enabled_ui(call_enabled, |ui| {
if ui.button("call").clicked() {
let object_name = self.selected_object.as_ref().unwrap().name.clone();
let method_name = self.selected_method.as_ref().unwrap().clone();
let message = serde_json::from_str(&self.payload).unwrap(); // TODO: handle parsing error
self.start_call(object_name, method_name, Some(message));
// TODO: Block sending other requests
}
});
ui.separator();
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
let mut layout_job = crate::syntax_highlighting::highlight(ui.ctx(), string, "json");
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
};
if let Some(response) = &self.response {
egui::TopBottomPanel::bottom("bottom_panel")
.resizable(true)
.show_inside(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut serde_json::to_string_pretty(response).unwrap())
.font(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.desired_rows(10)
.lock_focus(true)
.desired_width(f32::INFINITY)
.layouter(&mut layouter),
);
})
});
}
egui::CentralPanel::default()
.show_inside(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut self.payload)
.font(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.desired_rows(10)
.lock_focus(true)
.desired_width(f32::INFINITY)
.layouter(&mut layouter),
);
});
});
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
use egui::*;
self.handle_events(ctx);
egui::CentralPanel::default().show(ctx, |ui| {
egui::SidePanel::left("left_panel")
.resizable(true)
.default_width(150.0)
.width_range(80.0..=200.0)
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading("Left Panel");
});
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label("Foo");
});
});
.width_range(100.0..=300.0)
.show_inside(ui, |ui| self.show_left_panel(ui));
egui::SidePanel::right("right_panel")
.resizable(true)
.default_width(150.0)
.width_range(80.0..=200.0)
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading("Right Panel");
});
egui::ScrollArea::vertical().show(ui, |ui| {
ui.text_edit_singleline(&mut self.address);
// TODO: ui.text_edit_singleline(&mut self.port);
ui.text_edit_singleline(&mut self.username);
ui.text_edit_singleline(&mut self.password);
if self.is_connecting {
ui.button("Connecting...");
} else if self.session.is_none() {
if ui.button("Connect").clicked() {
let socket_addr = SocketAddrV4::new(self.address.parse().unwrap(), self.port);
self.start_connect(socket_addr, self.username.clone(), self.password.clone());
}
} else {
if ui.button("Disconnect").clicked() {
self.start_disconnect()
}
}
});
});
.width_range(100.0..=200.0)
.show_inside(ui, |ui| self.show_right_panel(ui));
egui::CentralPanel::default().show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading("Central Panel");
});
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label("foo");
});
});
egui::CentralPanel::default()
.show_inside(ui, |ui| self.show_central_panel(ui));
});
}
}

View File

@ -1,66 +1,26 @@
#[cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::{net::ToSocketAddrs};
use anyhow::Result;
use smol::channel;
use async_ssh2_lite::{AsyncSession};
use app::App;
use tokio::runtime::Runtime;
use ubus::MonitorEvent;
use crate::ubus::Ubus;
mod app;
mod ubus;
mod async_line_reader;
mod syntax_highlighting;
// TODO: Save config to file
// TODO: call history
fn main() {
let rt = Runtime::new().expect("Unable to create Runtime");
let _enter = rt.enter();
/*
let address = "172.24.224.1:22";
let username = "root";
let password = "admin01";
let mut session = AsyncSession::<async_ssh2_lite::AsyncIoTcpStream>::connect(
address.to_socket_addrs()?.next().unwrap(),
None,
).await?;
session.handshake().await?;
session.userauth_password(username, password).await?;
let ubus = Ubus::new(session);
let (tx, rx) = channel::unbounded::<MonitorEvent>();
let listener = {
let tx = tx.clone();
tokio::spawn(async move {
println!("before listen");
if let Err(err) = ubus.monitor(None, &[], tx).await {
dbg!(err);
};
println!("after listen");
})
};
loop {
let e = rx.recv().await?;
dbg!(e);
}
*/
let mut native_options = eframe::NativeOptions::default();
native_options.decorated = true;
native_options.resizable = true;
let mut app = App::default();
eframe::run_native(
"ubusman",
native_options,
eframe::NativeOptions::default(),
Box::new(move |cc| {
app.init(cc);
Box::new(app)

107
src/syntax_highlighting.rs Normal file
View File

@ -0,0 +1,107 @@
use egui::text::LayoutJob;
const THEME: &str = "base16-mocha.dark";
/// Memoized Code highlighting
pub fn highlight(ctx: &egui::Context, code: &str, language: &str) -> LayoutJob {
impl egui::util::cache::ComputerMut<(&str, &str), LayoutJob> for Highlighter {
fn compute(&mut self, (code, lang): (&str, &str)) -> LayoutJob {
self.highlight(code, lang)
}
}
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
ctx.memory_mut(|mem| {
mem.caches
.cache::<HighlightCache>()
.get((code, language))
})
}
// ----------------------------------------------------------------------------
struct Highlighter {
ps: syntect::parsing::SyntaxSet,
ts: syntect::highlighting::ThemeSet,
}
impl Default for Highlighter {
fn default() -> Self {
Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(),
}
}
}
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, code: &str, lang: &str) -> LayoutJob {
self.highlight_impl(code, lang).unwrap_or_else(|| {
// Fallback:
LayoutJob::simple(
code.into(),
egui::FontId::monospace(12.0),
egui::Color32::DARK_GRAY,
f32::INFINITY,
)
})
}
fn highlight_impl(&self, text: &str, language: &str) -> Option<LayoutJob> {
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings;
let syntax = self
.ps
.find_syntax_by_name(language)
.or_else(|| self.ps.find_syntax_by_extension(language))?;
let mut h = HighlightLines::new(syntax, &self.ts.themes[THEME]);
use egui::text::{LayoutSection, TextFormat};
let mut job = LayoutJob {
text: text.into(),
..Default::default()
};
for line in LinesWithEndings::from(text) {
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
let fg = style.foreground;
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
let italics = style.font_style.contains(FontStyle::ITALIC);
let underline = style.font_style.contains(FontStyle::ITALIC);
let underline = if underline {
egui::Stroke::new(1.0, text_color)
} else {
egui::Stroke::NONE
};
job.sections.push(LayoutSection {
leading_space: 0.0,
byte_range: as_byte_range(text, range),
format: TextFormat {
font_id: egui::FontId::monospace(12.0),
color: text_color,
italics,
underline,
..Default::default()
},
});
}
}
Some(job)
}
}
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
let whole_start = whole.as_ptr() as usize;
let range_start = range.as_ptr() as usize;
assert!(whole_start <= range_start);
assert!(range_start + range.len() <= whole_start + whole.len());
let offset = range_start - whole_start;
offset..(offset + range.len())
}

View File

@ -1,23 +1,21 @@
use std::{borrow::Cow, net::TcpStream, str::FromStr};
use async_ssh2_lite::AsyncSession;
use anyhow::{anyhow, bail, Result};
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
use hex::FromHex;
use lazy_regex::regex_captures;
use serde_json::Value;
use shell_escape::unix::escape;
use hex::FromHex;
use anyhow::{Result, bail, anyhow};
use smol::{Async, io::AsyncReadExt, channel::Sender};
use smol::{channel::Sender, io::AsyncReadExt, Async};
use thiserror::Error;
use crate::async_line_reader::AsyncLineReader;
type Session = AsyncSession<AsyncIoTcpStream>;
// TODO: Add tests
pub struct Ubus {
session: AsyncSession<Async<TcpStream>>
}
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub enum UbusParamType {
Unknown,
Integer,
@ -25,7 +23,7 @@ pub enum UbusParamType {
Table,
String,
Array,
Double
Double,
}
#[derive(Error, Debug)]
@ -58,17 +56,25 @@ pub enum UbusError {
SystemError,
}
#[derive(Debug)]
pub struct UbusObject {
pub type Method = (String, Vec<(String, UbusParamType)>);
#[derive(Debug, Clone)]
pub struct Object {
pub name: String,
pub id: u32,
pub methods: Vec<(String, Vec<(String, UbusParamType)>)>
pub methods: Vec<Method>,
}
impl PartialEq for Object {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.id == other.id
}
}
#[derive(Debug)]
pub struct ListenEvent {
pub path: String,
pub value: Value
pub value: Value,
}
#[derive(Debug, Clone, Copy)]
@ -88,19 +94,21 @@ pub enum MonitorEventType {
impl ToString for MonitorEventType {
fn to_string(&self) -> String {
use MonitorEventType::*;
match self {
MonitorEventType::Hello => "hello",
MonitorEventType::Status => "status",
MonitorEventType::Data => "data",
MonitorEventType::Ping => "ping",
MonitorEventType::Lookup => "lookup",
MonitorEventType::Invoke => "invoke",
MonitorEventType::AddObject => "add_object",
MonitorEventType::RemoveObject => "remove_object",
MonitorEventType::Subscribe => "subscribe",
MonitorEventType::Unsubscribe => "unsubscribe",
MonitorEventType::Notify => "notify",
}.into()
Hello => "hello",
Status => "status",
Data => "data",
Ping => "ping",
Lookup => "lookup",
Invoke => "invoke",
AddObject => "add_object",
RemoveObject => "remove_object",
Subscribe => "subscribe",
Unsubscribe => "unsubscribe",
Notify => "notify",
}
.into()
}
}
@ -108,36 +116,37 @@ impl FromStr for MonitorEventType {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
use MonitorEventType::*;
match s {
"hello" => Ok(MonitorEventType::Hello),
"status" => Ok(MonitorEventType::Status),
"data" => Ok(MonitorEventType::Data),
"ping" => Ok(MonitorEventType::Ping),
"lookup" => Ok(MonitorEventType::Lookup),
"invoke" => Ok(MonitorEventType::Invoke),
"add_object" => Ok(MonitorEventType::AddObject),
"remove_object" => Ok(MonitorEventType::RemoveObject),
"subscribe" => Ok(MonitorEventType::Subscribe),
"unsubscribe" => Ok(MonitorEventType::Unsubscribe),
"notify" => Ok(MonitorEventType::Notify),
_ => Err(anyhow!("Unknown event type '{}'", s))
"hello" => Ok(Hello),
"status" => Ok(Status),
"data" => Ok(Data),
"ping" => Ok(Ping),
"lookup" => Ok(Lookup),
"invoke" => Ok(Invoke),
"add_object" => Ok(AddObject),
"remove_object" => Ok(RemoveObject),
"subscribe" => Ok(Subscribe),
"unsubscribe" => Ok(Unsubscribe),
"notify" => Ok(Notify),
_ => Err(anyhow!("Unknown event type '{}'", s)),
}
}
}
#[derive(Debug)]
pub struct MonitorEvent {
pub struct MonitorEvent {
direction: MonitorDir,
client: u32,
peer: u32,
kind: MonitorEventType,
data: Value // TODO: Figure out the possible values for every `MonitorEventType`, to make this more safe.
data: Value, // TODO: Figure out the possible values for every `MonitorEventType`, to make this more safe.
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MonitorDir {
Rx,
Tx
Tx,
}
fn parse_parameter_type(param: &str) -> Option<UbusParamType> {
@ -150,7 +159,7 @@ fn parse_parameter_type(param: &str) -> Option<UbusParamType> {
"Table" => Some(Table),
"Array" => Some(Array),
"(unknown)" => Some(Unknown),
_ => None
_ => None,
}
}
@ -172,7 +181,7 @@ fn parse_error_code(code: i32) -> Option<UbusError> {
11 => Some(NoMemory),
12 => Some(ParseError),
13 => Some(SystemError),
_ => Some(UnknownError)
_ => Some(UnknownError),
}
}
@ -190,203 +199,217 @@ fn escape_json(json: &Value) -> String {
escape(Cow::from(json.to_string())).into()
}
impl Ubus {
pub fn new(session: AsyncSession<Async<TcpStream>>) -> Ubus {
Ubus {
session
}
async fn exec_cmd(session: &Session, cmd: &str) -> Result<String> {
let mut channel = session.channel_session().await?;
channel.exec(cmd).await?;
channel.close().await?;
if let Some(err) = parse_error_code(channel.exit_status()?) {
return Err(err.into());
}
async fn exec_cmd(&self, cmd: &str) -> Result<String> {
let mut channel = self.session.channel_session().await?;
channel.exec(cmd).await?;
channel.close().await?;
if let Some(err) = parse_error_code(channel.exit_status()?) {
return Err(err.into())
}
let mut output = String::new();
channel.read_to_string(&mut output).await?;
Ok(output)
}
let mut output = String::new();
channel.read_to_string(&mut output).await?;
Ok(output)
}
pub async fn list(session: &Session, path: Option<&str>) -> Result<Vec<String>> {
let output = match path {
Some(path) => exec_cmd(session, &format!("ubus -S list {}", path)).await?,
None => exec_cmd(session, "ubus -S list").await?,
};
Ok(output.lines().map(ToOwned::to_owned).collect::<Vec<_>>())
}
pub async fn list(&self, path: Option<&str>) -> Result<Vec<String>> {
let output = match path {
Some(path) => self.exec_cmd(&format!("ubus -S list {}", path)).await?,
None => self.exec_cmd("ubus -S list").await?,
};
Ok(output.lines().map(ToOwned::to_owned).collect::<Vec<_>>())
}
pub async fn list_verbose(session: &Session, path: Option<&str>) -> Result<Vec<Object>> {
let output = match path {
Some(path) => exec_cmd(session, &format!("ubus -v list {}", path)).await?,
None => exec_cmd(session, "ubus -v list").await?,
};
pub async fn list_verbose(&self, path: Option<&str>) -> Result<Vec<UbusObject>> {
let output = match path {
Some(path) => self.exec_cmd(&format!("ubus -v list {}", path)).await?,
None => self.exec_cmd("ubus -v list").await?,
};
let mut cur_name = None;
let mut cur_id = None;
let mut cur_methods = vec![];
let mut cur_name = None;
let mut cur_id = None;
let mut cur_methods = vec![];
let mut objects = vec![];
for line in output.lines() {
if let Some((_, name, id)) = regex_captures!(r"^'([\w.-]+)' @([0-9a-zA-Z]{8})$", line) {
if cur_name.is_some() && cur_id.is_some() {
objects.push(UbusObject {
id: cur_id.unwrap(),
name: cur_name.unwrap(),
methods: cur_methods
});
cur_methods = vec![];
}
cur_name = Some(name.into());
cur_id = Some(parse_hex_id(id)?);
} else if let Some((_, name, params_body)) = regex_captures!(r#"^\s+"([\w-]+)":\{(.*)}$"#, line) {
let mut params = vec![];
if !params_body.is_empty() {
for param in params_body.split(",") {
let (_, name, param_type_name) = regex_captures!(r#"^"([\w-]+)":"(\w+)"$"#, param)
.ok_or(anyhow!("Failed to parse parameter '{}' in line '{}'", param, line))?;
let param_type = parse_parameter_type(param_type_name)
.ok_or(anyhow!("Unknown parameter type '{}'", param_type_name))?;
params.push((name.into(), param_type));
}
}
cur_methods.push((name.into(), params));
} else {
bail!("Failed to parse line '{}'", line);
let mut objects = vec![];
for line in output.lines() {
if let Some((_, name, id)) = regex_captures!(r"^'([\w.-]+)' @([0-9a-zA-Z]{8})$", line) {
if cur_name.is_some() && cur_id.is_some() {
objects.push(Object {
id: cur_id.unwrap(),
name: cur_name.unwrap(),
methods: cur_methods,
});
cur_methods = vec![];
}
}
Ok(objects)
}
pub async fn call(&self, path: &str, method: &str, message: Option<&Value>) -> Result<Value> {
let cmd = match message {
Some(msg) => format!("ubus -S call {} {} {}", path, method, escape_json(msg)),
None => format!("ubus -S call {} {}", path, method),
};
cur_name = Some(name.into());
cur_id = Some(parse_hex_id(id)?);
} else if let Some((_, name, params_body)) =
regex_captures!(r#"^\s+"([\w-]+)":\{(.*)}$"#, line)
{
let mut params = vec![];
if !params_body.is_empty() {
for param in params_body.split(",") {
let (_, name, param_type_name) =
regex_captures!(r#"^"([\w-]+)":"(\w+)"$"#, param).ok_or(anyhow!(
"Failed to parse parameter '{}' in line '{}'",
param,
line
))?;
let output = self.exec_cmd(&cmd).await?;
let value = serde_json::from_str::<Value>(&output)?;
Ok(value)
}
let param_type = parse_parameter_type(param_type_name)
.ok_or(anyhow!("Unknown parameter type '{}'", param_type_name))?;
pub async fn send(&self, event_type: &str, message: Option<&Value>) -> Result<()> {
let cmd = match message {
Some(msg) => format!("ubus -S send {} {}", event_type, escape_json(msg)),
None => format!("ubus -S send {}", event_type),
};
self.exec_cmd(&cmd).await?;
Ok(())
}
pub async fn wait_for(&self, objects: &[&str]) -> Result<()> {
if objects.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus -S wait_for {}", objects.join(" "));
let mut channel = self.session.channel_session().await?;
channel.exec(&cmd).await?;
channel.close().await?;
Ok(())
}
fn parse_listen_event(bytes: &[u8]) -> Result<ListenEvent> {
let event_value: Value = serde_json::from_slice(bytes)?;
let event_map = event_value.as_object()
.ok_or(anyhow!("Expected event to be an object"))?;
if event_map.keys().len() != 1 {
bail!("Expected event object to only contain one key");
}
let path = event_map.keys().next().unwrap().clone();
let value = event_map.get(&path).unwrap().clone();
Ok(ListenEvent { path, value })
}
pub async fn listen(&self, paths: &[&str], sender: Sender<ListenEvent>) -> Result<()> {
let cmd = format!("ubus -S listen {}", paths.join(" "));
let mut channel = self.session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Handle error? 'channel.exit_status()', idk if needed
let mut line_reader = AsyncLineReader::new(channel.stream(0));
loop {
let line = line_reader.read_line().await?;
let event = Ubus::parse_listen_event(&line)?;
sender.send(event).await?;
}
}
pub async fn subscribe(&self, paths: &[&str]) -> Result<()> {
if paths.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus -S subscribe {}", paths.join(" "));
let mut channel = self.session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Haven't figured out how to test subscribe event using default objects on ubus.
todo!();
}
fn parse_monitor_event(bytes: &[u8]) -> Result<MonitorEvent> {
let line = bytes.iter()
.map(|c| char::from_u32(*c as u32).unwrap())
.collect::<String>();
let (_,
direction_str,
client_str,
peer_str,
kind_str,
data_str
) = regex_captures!(r"^([<\->]{2}) ([0-9a-zA-Z]{8}) #([0-9a-zA-Z]{8})\s+([a-z_]+): (\{.*\})$", &line)
.ok_or(anyhow!("Unknown pattern of monitor message '{}'", line))?;
let direction = match direction_str {
"->" => MonitorDir::Tx,
"<-" => MonitorDir::Rx,
_ => bail!("Unknown monitor message direction '{}'", direction_str)
};
let client = parse_hex_id(client_str)?;
let peer = parse_hex_id(peer_str)?;
let kind = MonitorEventType::from_str(kind_str)?;
let data = serde_json::from_str(data_str)?;
Ok(MonitorEvent { direction, client, peer, kind, data })
}
pub async fn monitor(&self, dir: Option<MonitorDir>, filter: &[MonitorEventType], sender: Sender<MonitorEvent>) -> Result<()> {
let mut cmd = vec!["ubus -S".into()];
if let Some(dir) = dir {
if dir == MonitorDir::Rx {
cmd.push("-M r".into());
} else if dir == MonitorDir::Tx {
cmd.push("-M t".into());
params.push((name.into(), param_type));
}
}
cur_methods.push((name.into(), params));
} else {
bail!("Failed to parse line '{}'", line);
}
cmd.extend(filter.iter().map(|e| format!("-m {}", e.to_string())));
cmd.push("monitor".into());
}
Ok(objects)
}
let mut channel = self.session.channel_session().await?;
println!("{}", cmd.join(" "));
channel.exec(&cmd.join(" ")).await?;
// TODO: Handle error? 'channel.exit_status()', idk if needed
pub async fn call(
session: &Session,
path: &str,
method: &str,
message: Option<&Value>,
) -> Result<Value> {
let cmd = match message {
Some(msg) => format!("ubus -S call {} {} {}", path, method, escape_json(msg)),
None => format!("ubus -S call {} {}", path, method),
};
let mut line_reader = AsyncLineReader::new(channel.stream(0));
loop {
let line = line_reader.read_line().await?;
let event = Ubus::parse_monitor_event(&line)?;
sender.send(event).await?;
}
// TODO: handle cases where output is empty? ("")
let output = exec_cmd(session, &cmd).await?;
let value = serde_json::from_str::<Value>(&output)?;
Ok(value)
}
pub async fn send(session: &Session, event_type: &str, message: Option<&Value>) -> Result<()> {
let cmd = match message {
Some(msg) => format!("ubus -S send {} {}", event_type, escape_json(msg)),
None => format!("ubus -S send {}", event_type),
};
exec_cmd(session, &cmd).await?;
Ok(())
}
pub async fn wait_for(session: &Session, objects: &[&str]) -> Result<()> {
if objects.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus -S wait_for {}", objects.join(" "));
let mut channel = session.channel_session().await?;
channel.exec(&cmd).await?;
channel.close().await?;
Ok(())
}
fn parse_listen_event(bytes: &[u8]) -> Result<ListenEvent> {
let event_value: Value = serde_json::from_slice(bytes)?;
let event_map = event_value
.as_object()
.ok_or(anyhow!("Expected event to be an object"))?;
if event_map.keys().len() != 1 {
bail!("Expected event object to only contain one key");
}
let path = event_map.keys().next().unwrap().clone();
let value = event_map.get(&path).unwrap().clone();
Ok(ListenEvent { path, value })
}
pub async fn listen(session: &Session, paths: &[&str], sender: Sender<ListenEvent>) -> Result<()> {
let cmd = format!("ubus -S listen {}", paths.join(" "));
let mut channel = session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Handle error? 'channel.exit_status()', idk if needed
let mut line_reader = AsyncLineReader::new(channel.stream(0));
loop {
let line = line_reader.read_line().await?;
let event = parse_listen_event(&line)?;
sender.send(event).await?;
}
}
pub async fn subscribe(session: &Session, paths: &[&str]) -> Result<()> {
if paths.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus -S subscribe {}", paths.join(" "));
let mut channel = session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Haven't figured out how to test subscribe event using default objects on ubus.
todo!();
}
fn parse_monitor_event(bytes: &[u8]) -> Result<MonitorEvent> {
let line = bytes
.iter()
.map(|c| char::from_u32(*c as u32).unwrap())
.collect::<String>();
let (_, direction_str, client_str, peer_str, kind_str, data_str) = regex_captures!(
r"^([<\->]{2}) ([0-9a-zA-Z]{8}) #([0-9a-zA-Z]{8})\s+([a-z_]+): (\{.*\})$",
&line
)
.ok_or(anyhow!("Unknown pattern of monitor message '{}'", line))?;
let direction = match direction_str {
"->" => MonitorDir::Tx,
"<-" => MonitorDir::Rx,
_ => bail!("Unknown monitor message direction '{}'", direction_str),
};
let client = parse_hex_id(client_str)?;
let peer = parse_hex_id(peer_str)?;
let kind = MonitorEventType::from_str(kind_str)?;
let data = serde_json::from_str(data_str)?;
Ok(MonitorEvent {
direction,
client,
peer,
kind,
data,
})
}
pub async fn monitor(
session: &Session,
dir: Option<MonitorDir>,
filter: &[MonitorEventType],
sender: Sender<MonitorEvent>,
) -> Result<()> {
let mut cmd = vec!["ubus -S".into()];
if let Some(dir) = dir {
if dir == MonitorDir::Rx {
cmd.push("-M r".into());
} else if dir == MonitorDir::Tx {
cmd.push("-M t".into());
}
}
cmd.extend(filter.iter().map(|e| format!("-m {}", e.to_string())));
cmd.push("monitor".into());
let mut channel = session.channel_session().await?;
println!("{}", cmd.join(" "));
channel.exec(&cmd.join(" ")).await?;
// TODO: Handle error? 'channel.exit_status()', idk if needed
let mut line_reader = AsyncLineReader::new(channel.stream(0));
loop {
let line = line_reader.read_line().await?;
let event = parse_monitor_event(&line)?;
sender.send(event).await?;
}
}