add rudimentary calling of methods on objects
This commit is contained in:
parent
f9561f2883
commit
780be584e6
147
Cargo.lock
generated
147
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
360
src/app.rs
360
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<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\"{}\": {}", ¶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::<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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
46
src/main.rs
46
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::<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
107
src/syntax_highlighting.rs
Normal 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())
|
||||
}
|
477
src/ubus.rs
477
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<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?;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user