From 780be584e66972afcd169993a12475a4b28f2982 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 2 Apr 2023 18:37:03 +0300 Subject: [PATCH] add rudimentary calling of methods on objects --- Cargo.lock | 147 ++++++++++++ Cargo.toml | 1 + src/app.rs | 360 ++++++++++++++++++++++------ src/main.rs | 46 +--- src/syntax_highlighting.rs | 107 +++++++++ src/ubus.rs | 477 +++++++++++++++++++------------------ 6 files changed, 792 insertions(+), 346 deletions(-) create mode 100644 src/syntax_highlighting.rs diff --git a/Cargo.lock b/Cargo.lock index 937c51a..1e73886 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 5410dd6..69b7a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/app.rs b/src/app.rs index 47a34bf..d2d3dbe 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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>), - Disconnect(Result<()>) + Disconnect(Result<()>), + ListObjects(Result>), + Call(Result) } pub struct App { @@ -21,6 +26,13 @@ pub struct App { password: String, session: Option>, + selected_object: Option>, + selected_method: Option, + object_filter: String, + objects: Vec>, + payload: String, + response: Option, + 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(socket_addr: A, username: String, password: String) -> Result> +async fn connect( + socket_addr: A, + username: String, + password: String, +) -> Result> where A: Into, { @@ -59,36 +82,52 @@ where } async fn disconnect(session: AsyncSession) -> 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>) { - 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, { - 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) { + 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 => "\"\"", + Integer => "0", + Boolean => "false", + Table => "{}", + String => "\"\"", + Array => "[]", + Double => "0.00", + }; + lines.push(format!("\t\"{}\": {}", ¶m_name, ¶m_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::>(); + 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)); }); } } diff --git a/src/main.rs b/src/main.rs index 4455175..366c73b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::::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::(); - 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) diff --git a/src/syntax_highlighting.rs b/src/syntax_highlighting.rs new file mode 100644 index 0000000..572b91e --- /dev/null +++ b/src/syntax_highlighting.rs @@ -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; + + ctx.memory_mut(|mem| { + mem.caches + .cache::() + .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 { + 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 { + 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()) +} diff --git a/src/ubus.rs b/src/ubus.rs index b7ac9a0..26ffa5e 100644 --- a/src/ubus.rs +++ b/src/ubus.rs @@ -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; + // TODO: Add tests -pub struct Ubus { - session: AsyncSession> -} - -#[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, +} + +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 { + 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 { @@ -150,7 +159,7 @@ fn parse_parameter_type(param: &str) -> Option { "Table" => Some(Table), "Array" => Some(Array), "(unknown)" => Some(Unknown), - _ => None + _ => None, } } @@ -172,7 +181,7 @@ fn parse_error_code(code: i32) -> Option { 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>) -> Ubus { - Ubus { - session - } +async fn exec_cmd(session: &Session, cmd: &str) -> Result { + 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 { - 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> { + 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::>()) +} - pub async fn list(&self, path: Option<&str>) -> Result> { - 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::>()) - } +pub async fn list_verbose(session: &Session, path: Option<&str>) -> Result> { + 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> { - 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 { - 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::(&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 { - 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) -> 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 { - let line = bytes.iter() - .map(|c| char::from_u32(*c as u32).unwrap()) - .collect::(); - - 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, filter: &[MonitorEventType], sender: Sender) -> 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 { + 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::(&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 { + 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) -> 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 { + let line = bytes + .iter() + .map(|c| char::from_u32(*c as u32).unwrap()) + .collect::(); + + 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, + filter: &[MonitorEventType], + sender: Sender, +) -> 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?; } }