Move files into old directory

This commit is contained in:
Michael Mikovsky
2026-02-16 12:52:46 -07:00
parent 2be2e8dbd9
commit c9b0e6f88f
68 changed files with 137 additions and 4214 deletions
+47
View File
@@ -0,0 +1,47 @@
[package]
name = "ush-gui"
version.workspace = true
edition.workspace = true
authors.workspace = true
include.workspace = true
[lib]
crate-type = ["cdylib"]
[package.metadata.docs.rs]
all-features = true
targets = [
# "x86_64-unknown-linux-gnu",
"wasm32-unknown-unknown"
]
[dependencies]
unshell = { path="../" }
# log = "0.4.27"
chrono = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
reqwest = {version = "0.12.26", features=["json"]}
log = "0.4.29"
# Stuff for app functionality
egui = "0.33.0"
eframe = { version = "0.33.0", default-features = false, features = [
# "accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
"wayland", # To support Linux (and CI)
"x11", # To support older Linux distributions (restores one of the default features)
] }
egui_extras = "0.33.2"
egui_tiles = "0.14.0"
egui-async = "0.2.6"
# Web Stuff
wasm-bindgen-futures = "0.4.50"
wasm-bindgen = "0.2.108"
web-sys = { version = "0.3.85", features = ["Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] }
+20
View File
@@ -0,0 +1,20 @@
[build]
filehash = false
# # Proxy /api/* to backend
# [[proxy]]
# backend = "http://localhost:8081"
# rewrite = "/api/"
# # Proxy /auth/* to backend
# [[proxy]]
# backend = "http://localhost:8081"
# rewrite = "/auth/"
[[proxy]]
backend = "http://localhost:3000/api" # Address to proxy requests to
# ws = false # Use WebSocket for this proxy
# insecure = true # Disable certificate validation
# no_system_proxy = false # Disable system proxy
# rewrite = "" # Strip the given prefix off paths
# no_redirect = false # Disable following redirects of proxy responses
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+11
View File
@@ -0,0 +1,11 @@
{
"name": "egui Template PWA",
"short_name": "egui-template-pwa",
"icons": [],
"lang": "en-US",
"id": "/index.html",
"start_url": "./index.html",
"display": "standalone",
"background_color": "white",
"theme_color": "white"
}
+77
View File
@@ -0,0 +1,77 @@
var cacheName = "egui-template-pwa";
var filesToCache = [
"./",
"./index.html",
"./eframe_template.js",
"./eframe_template_bg.wasm",
];
/* Start the service worker and cache all of the app's content */
self.addEventListener("install", function (e) {
e.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(filesToCache);
}),
);
});
/* Serve cached content when offline */
self.addEventListener("fetch", function (e) {
e.respondWith(
caches.match(e.request).then(function (response) {
return response || fetch(e.request);
}),
);
});
// export function httpGet(theUrl) {
// var xmlHttp = new XMLHttpRequest();
// xmlHttp.open("GET", theUrl, false); // false for synchronous request
// xmlHttp.send(null);
// return xmlHttp.responseText;
// }
function startHttpRequest(callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState !== 4) return;
if (xmlHttp.status == 200) callback(true, xmlHttp.responseText);
else if (xmlHttp.status == 401) callback(false, "Unauthorized");
else if (xmlHttp.status == 500) callback(false, "Internal Server Error");
else callback(false, xmlHttp.responseText);
};
return xmlHttp;
}
export function httpGet(theUrl, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.send(null);
}
export function httpPost(url, body, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("POST", url, true);
xmlHttp.setRequestHeader("Content-Type", "application/json");
// var data = JSON.stringify({ email: "[email protected]", password: "101010" });
xmlHttp.send(body);
}
export function httpGetAuth(theUrl, auth, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.setRequestHeader("authorization", auth);
xmlHttp.send(null);
}
export function httpPostAuth(url, auth, body, callback) {
var xmlHttp = startHttpRequest(callback);
xmlHttp.open("POST", url, true);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.setRequestHeader("authorization", auth);
// var data = JSON.stringify({ email: "[email protected]", password: "101010" });
xmlHttp.send(body);
}
+178
View File
@@ -0,0 +1,178 @@
<!doctype html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<head>
<!-- change this to your project name -->
<title>eframe template</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<link data-trunk rel="rust" data-wasm-opt="2" />
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base data-trunk-public-url />
<link data-trunk rel="icon" href="assets/favicon.ico" />
<link data-trunk rel="copy-file" href="assets/sw.js" />
<link data-trunk rel="copy-file" href="assets/manifest.json" />
<!--<link
data-trunk
rel="copy-file"
href="assets/icon-1024.png"
data-target-path="assets"
/>
<link
data-trunk
rel="copy-file"
href="assets/icon-256.png"
data-target-path="assets"
/>
<link
data-trunk
rel="copy-file"
href="assets/icon_ios_touch_192.png"
data-target-path="assets"
/>
<link
data-trunk
rel="copy-file"
href="assets/maskable_icon_x512.png"
data-target-path="assets"
/>-->
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="assets/icon_ios_touch_192.png" />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="white"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#404040"
/>
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Make canvas fill entire document: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<!-- the loading spinner will be removed in main.rs -->
<div class="centered" id="loading_text">
<noscript>You need javascript to use this website</noscript>
<p style="font-size: 16px">Loading…</p>
<div class="lds-dual-ring"></div>
</div>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if (
"serviceWorker" in navigator &&
window.location.hash !== "#dev"
) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("sw.js");
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->
+94
View File
@@ -0,0 +1,94 @@
use egui::Frame;
use egui_tiles::Tree;
use crate::{
FORCE_REDRAW_DELAY,
app::{AppState, windows::WindowWrapper},
};
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct TemplateApp {
// tab: Tab,
state: AppState,
tree: Tree<WindowWrapper>,
}
impl Default for TemplateApp {
fn default() -> Self {
Self {
state: AppState::default(),
tree: egui_tiles::Tree::new_horizontal("tree_root", Vec::new()),
}
}
}
impl TemplateApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
} else {
Default::default()
}
}
}
impl eframe::App for TemplateApp {
/// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
if !self.state.auth.logged_in() {
egui::CentralPanel::default()
.frame(Frame::central_panel(&ctx.style()).inner_margin(0))
.show(ctx, |ui| self.state.auth.update(ui));
} else {
egui::TopBottomPanel::top("tab_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar:
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("File", |ui| {
ui.label("File");
});
ui.menu_button("View", |ui| {
ui.label("View");
self.state.labels(&mut self.tree, ui);
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
egui::widgets::global_theme_preference_switch(ui);
});
// ui.style
});
});
egui::TopBottomPanel::bottom("tab_panel").show(ctx, |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(format!("UnShell UI {}", env!("CARGO_PKG_VERSION")));
egui::warn_if_debug_build(ui);
});
});
egui::CentralPanel::default()
.frame(Frame::central_panel(&ctx.style()).inner_margin(0))
.show(ctx, |ui| self.tree.ui(&mut self.state, ui));
}
ctx.request_repaint_after(FORCE_REDRAW_DELAY);
}
}
+135
View File
@@ -0,0 +1,135 @@
mod app;
mod windows;
use std::collections::HashSet;
use crate::{
app::windows::WindowWrapper, auth::Auth, config::Config, flowchart::FlowChart,
interface::InterfaceWindow, log_viewer::LogViewer, payload_config::PayloadConfig,
};
pub use app::TemplateApp;
use egui_tiles::{TileId, Tree};
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct AppState {
pub auth: Auth,
pub open_windows: HashSet<AppWindow>,
pub flowchart: FlowChart,
pub config: Config,
pub payload_config: PayloadConfig,
pub log_viewer: LogViewer,
pub interface: InterfaceWindow,
}
impl AppState {
pub fn labels(&mut self, tree: &mut Tree<WindowWrapper>, ui: &mut egui::Ui) {
for (_, (key, name)) in (vec![
(AppWindow::Flowchart, "Flowchart"),
(AppWindow::PayloadConfig, "Payload Config"),
(AppWindow::Config, "Config"),
(AppWindow::LogViewer, "Log Viewer"),
(AppWindow::InterfaceWindow, "Tree Test"),
])
.iter()
.enumerate()
{
let enabled = self.open_windows.contains(&key);
if ui.selectable_label(enabled, *name).clicked() {
if enabled {
self.close_window(tree, key);
} else {
self.open_window(tree, key, name);
}
}
}
}
pub fn close_window(&mut self, tree: &mut Tree<WindowWrapper>, key: &AppWindow) {
match Self::find_pane_id(*key, tree) {
Some(tid) => {
let tid = tid.clone();
tree.remove_recursively(tid);
tree.tiles.remove(tid);
self.open_windows.remove(&key);
}
None => unreachable!(),
}
}
pub fn open_window(&mut self, tree: &mut Tree<WindowWrapper>, key: &AppWindow, name: &str) {
let tid = tree.tiles.insert_pane(WindowWrapper {
name: name.to_string(),
window: *key,
});
match self.open_windows.len() {
0 => {
tree.root = Some(tid);
}
1 => {
let old_root = tree.root.unwrap();
let tab_id = tree.tiles.insert_tab_tile(vec![old_root, tid]);
tree.root = Some(tab_id);
tree.make_active(|t, _| t == tid);
}
_ => {
let root = tree.root().unwrap();
let n = tree.tiles.get_container(root).unwrap().num_children();
tree.move_tile_to_container(tid, tree.root.unwrap().clone(), n, true);
}
}
self.open_windows.insert(key.clone());
}
fn find_pane_id(window_type: AppWindow, tree: &Tree<WindowWrapper>) -> Option<&TileId> {
for (tid, window) in tree.tiles.iter() {
match window {
egui_tiles::Tile::Pane(pane) => {
if pane.window == window_type {
return Some(tid);
}
}
egui_tiles::Tile::Container(_) => {}
}
}
None
}
}
#[derive(Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq, Hash)]
pub enum AppWindow {
Flowchart,
Config,
PayloadConfig,
LogViewer,
InterfaceWindow,
}
impl AppWindow {
fn update(&self, state: &mut AppState, ui: &mut egui::Ui) {
match self {
AppWindow::Flowchart => state.flowchart.paint(ui),
AppWindow::Config => state.config.update(&mut state.auth, ui),
AppWindow::PayloadConfig => state.payload_config.update(ui),
AppWindow::LogViewer => state.log_viewer.update(&mut state.auth, ui),
AppWindow::InterfaceWindow => state.interface.update(&mut state.auth, ui),
}
}
fn render_title_buttons(&self, state: &mut AppState, ui: &mut egui::Ui) {
match self {
AppWindow::Flowchart => {
state.flowchart.titlebar_buttons(ui);
}
AppWindow::Config => {
state.config.titlebar_buttons(ui);
}
_ => {
ui.label("");
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
use egui::Rect;
use crate::app::{AppState, AppWindow};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct WindowWrapper {
pub name: String,
pub window: AppWindow,
}
impl egui_tiles::Behavior<WindowWrapper> for AppState {
fn tab_title_for_pane(&mut self, pane: &WindowWrapper) -> egui::WidgetText {
format!("{}", pane.name).into()
}
fn pane_ui(
&mut self,
ui: &mut egui::Ui,
_tile_id: egui_tiles::TileId,
pane: &mut WindowWrapper,
) -> egui_tiles::UiResponse {
let mut ret = egui_tiles::UiResponse::None;
let mut rect = Rect::NOTHING;
ui.horizontal(|ui| {
rect = ui.max_rect();
let bg_color = ui.style().visuals.extreme_bg_color;
ui.painter().rect_filled(rect, 0.0, bg_color);
ui.vertical_centered(|ui| {
ui.label(&pane.name);
});
});
let mut open_space = Rect::NOTHING;
#[allow(deprecated)]
ui.allocate_ui_at_rect(rect, |ui| {
ui.horizontal(|ui| {
pane.window.render_title_buttons(self, ui);
open_space = ui.available_rect_before_wrap();
})
});
let drag_sense = ui.interact(
open_space,
ui.id().with(&format!("Pane_{}_sense", pane.name)),
egui::Sense::drag(),
);
if drag_sense.drag_started() {
ret = egui_tiles::UiResponse::DragStarted;
}
if drag_sense.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
}
pane.window.update(self, ui);
ret
}
fn is_tab_closable(
&self,
tiles: &egui_tiles::Tiles<WindowWrapper>,
tile_id: egui_tiles::TileId,
) -> bool {
match tiles.get(tile_id).unwrap() {
egui_tiles::Tile::Pane(_) => true,
egui_tiles::Tile::Container(_) => false,
}
}
fn on_tab_close(
&mut self,
tiles: &mut egui_tiles::Tiles<WindowWrapper>,
tile_id: egui_tiles::TileId,
) -> bool {
match tiles.get(tile_id).unwrap() {
egui_tiles::Tile::Pane(pane) => self.open_windows.remove(&pane.window),
egui_tiles::Tile::Container(_) => false,
}
}
fn tab_bar_color(&self, visuals: &egui::Visuals) -> egui::Color32 {
visuals.panel_fill // same as the tab contents
}
}
+108
View File
@@ -0,0 +1,108 @@
use serde::de::DeserializeOwned;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::js_sys::Promise;
pub struct PromiseWrapper<T> {
state: Rc<RefCell<PromiseState<T>>>,
}
enum PromiseState<T> {
Pending,
Resolved(T),
Rejected(String),
}
impl<T: 'static> PromiseWrapper<T>
where
T: DeserializeOwned,
{
/// Create a new PromiseWrapper from a JavaScript Promise
/// The promise should resolve to a Response object (from fetch)
pub fn new(promise: Promise) -> Self {
let state = Rc::new(RefCell::new(PromiseState::Pending));
let state_clone = state.clone();
let state_clone2 = state.clone();
// Success callback
let success = Closure::once(move |value: JsValue| {
if let Ok(response) = value.dyn_into::<web_sys::Response>() {
if let Ok(json_promise) = response.json() {
let state_inner = state_clone.clone();
let json_success = Closure::once(move |json_value: JsValue| {
*state_inner.borrow_mut() = if let Some(body) = json_value.as_string() {
match serde_json::from_str(&body) {
Ok(data) => PromiseState::Resolved(data),
Err(e) => PromiseState::Rejected(format!("{:?}", e)),
}
} else {
PromiseState::Rejected(format!("Response was not a string"))
};
match serde_wasm_bindgen::from_value::<T>(json_value) {
Ok(data) => *state_inner.borrow_mut() = PromiseState::Resolved(data),
Err(e) => {
*state_inner.borrow_mut() =
PromiseState::Rejected(format!("{:?}", e))
}
}
});
let _ = json_promise.then(&json_success);
json_success.forget();
}
}
});
// Error callback
let error = Closure::once(move |err: JsValue| {
*state_clone2.borrow_mut() = PromiseState::Rejected(
err.as_string()
.unwrap_or_else(|| "Unknown error".to_string()),
);
});
let _ = promise.then2(&success, &error);
success.forget();
error.forget();
Self { state }
}
/// Poll the promise to check if it has completed
/// Returns Some(T) if resolved successfully, None if still pending or rejected
/// This is a lightweight check that's safe to call every frame
#[inline]
pub fn poll(&self) -> Option<T>
where
T: Clone,
{
match &*self.state.borrow() {
PromiseState::Resolved(value) => Some(value.clone()),
_ => None,
}
}
/// Check if the promise is still pending (lightweight)
#[inline]
pub fn is_pending(&self) -> bool {
matches!(&*self.state.borrow(), PromiseState::Pending)
}
/// Check if the promise was rejected (lightweight)
#[inline]
pub fn is_rejected(&self) -> bool {
matches!(&*self.state.borrow(), PromiseState::Rejected(_))
}
/// Get the error message if rejected
pub fn error(&self) -> Option<String> {
match &*self.state.borrow() {
PromiseState::Rejected(err) => Some(err.clone()),
_ => None,
}
}
}
+222
View File
@@ -0,0 +1,222 @@
use egui::{Align2, Area, Color32, Frame, Order, Sense, UiKind, Vec2, mutex::Mutex};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::json;
use std::sync::Arc;
use wasm_bindgen::prelude::Closure;
use unshell::Result;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Auth {
// Auth Stuff
token: Option<Token>,
#[serde(skip)]
auth_state: Arc<Mutex<AuthState>>,
// UI Stuff
username: String,
#[serde(skip)]
password: String,
show_password: bool,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
struct Token {
expiration: u128,
token: String,
}
#[derive(Debug, PartialEq, Eq)]
enum AuthState {
Unset,
NotLoggedIn,
RequestSent,
Authorised(Token),
Error(String),
}
impl Default for AuthState {
fn default() -> Self {
Self::Unset
}
}
impl Auth {
/// Refresh the authentication state
pub fn logged_in(&mut self) -> bool {
match (self.token.is_some(), &*self.auth_state.lock()) {
// The client is actually authorized
(true, AuthState::Authorised(_)) => true,
// If the user has just reloaded the session,
// the AuthState is not automatically set by any other process
(true, AuthState::Unset) => true,
// If the authentication state has been updated to unauthorized, delete the token
(true, _) => {
self.token = None;
false
}
// If the authentication state has been updated to authorized, set the token
(false, AuthState::Authorised(token)) => {
self.token = Some(token.clone());
// Also clear the password because it is bad to store it
self.password.clear();
true
}
// The client is actually unauthorized
(false, _) => false,
}
}
pub fn update(&mut self, ui: &mut egui::Ui) {
Area::new("Auth".into())
.kind(UiKind::Modal)
.sense(Sense::hover())
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.order(Order::Foreground)
.interactable(true)
.show(ui.ctx(), |ui| {
Frame::popup(ui.style()).show(ui, |ui| {
ui.heading("UnShell Login");
ui.horizontal(|ui| {
ui.label("Username");
ui.text_edit_singleline(&mut self.username);
});
ui.horizontal(|ui| {
ui.label("Password");
let _ = ui.add(
// [ui.available_width(), 24.],
egui::TextEdit::singleline(&mut self.password)
.password(!self.show_password),
);
self.show_password = ui.button("Show").is_pointer_button_down_on();
// ui.toggle_value(&mut self.show_password, "Show");
});
ui.horizontal(|ui| {
let (show_spinner, err_text) = match *self.auth_state.lock() {
AuthState::Error(ref e) => {
// self.login_button(ui);
(false, e.clone())
}
AuthState::RequestSent => (true, "".into()),
_ => (false, "".into()),
};
if !show_spinner {
if ui.button("Login").clicked() {
let state = self.auth_state.clone();
crate::httpPost(
"/api/auth",
&json!({
"username": self.username.clone(),
"password": self.password.clone()
})
.to_string(),
Closure::once_into_js(move |ok: bool, response: String| {
*(state.lock()) = if ok {
if let Ok(token) =
serde_json::from_str::<Token>(&response)
{
AuthState::Authorised(token)
} else {
AuthState::Error("Malformed Response".into())
}
} else {
AuthState::Error(response)
}
}),
);
*(self.auth_state.lock()) = AuthState::RequestSent;
}
} else {
ui.spinner();
}
ui.colored_label(Color32::RED, err_text);
});
});
});
// });
// });
}
pub fn get<R, F>(&self, path: &str, ret: F) -> Result<()>
where
F: FnOnce(R) + 'static,
R: DeserializeOwned,
{
if let Some(ref token) = self.token {
let state = self.auth_state.clone();
crate::httpGetAuth(
path,
format!("Bearer {}", token.token),
Closure::once_into_js(move |ok: bool, response: String| {
if ok {
if let Ok(value) = serde_json::from_str::<R>(&response) {
ret(value)
} else {
*(state.lock()) = AuthState::Error("Malformed Response".into());
}
} else {
*(state.lock()) = AuthState::Error(response);
}
}),
);
}
Ok(())
}
pub fn post<R, T, F>(&self, path: &str, data: &T, ret: F) -> Result<()>
where
R: DeserializeOwned,
T: Serialize,
F: FnOnce(R) + 'static,
{
if let Some(ref token) = self.token {
let state = self.auth_state.clone();
crate::httpPostAuth(
path,
format!("Bearer {}", token.token),
&serde_json::to_string(data)?,
Closure::once_into_js(move |ok: bool, response: String| {
if ok {
if let Ok(value) = serde_json::from_str::<R>(&response) {
ret(value)
} else {
*(state.lock()) = AuthState::Error("Malformed Response".into());
}
} else {
*(state.lock()) = AuthState::Error(response);
}
}),
);
}
Ok(())
}
}
impl Default for Auth {
fn default() -> Self {
Self {
token: Default::default(),
auth_state: Arc::new(Mutex::new(AuthState::NotLoggedIn)),
username: Default::default(),
password: Default::default(),
show_password: Default::default(),
}
}
}
+113
View File
@@ -0,0 +1,113 @@
impl Auth {
// pub fn get_test(url: String) -> Promise {
// let fut = Self::get_async(&url);
// // wasm_bindgen_futures::(fut)
// }
// pub fn get_async<R>(&self, url: &str) -> PromiseWrapper<R>
// where
// // F: FnOnce(R) + 'static,
// R: DeserializeOwned + 'static,
// {
// let token = self.token.as_ref().unwrap();
// let opts = RequestInit::new();
// opts.set_method("GET");
// let request = Request::new_with_str_and_init(url, &opts).unwrap();
// let headers = request.headers();
// headers.set("content-type", "application/json").unwrap();
// headers
// .set("Authorization", &format!("Bearer {}", token.token))
// .unwrap();
// let window = web_sys::window().unwrap();
// let promise = window.fetch_with_request(&request);
// // wasm_bindgen_futures::spawn_local(async move {
// // let resp_value = JsFuture::from(window.fetch_with_request(&request))
// // .await
// // .unwrap();
// // // `resp_value` is a `Response` object.
// // assert!(resp_value.is_instance_of::<Response>());
// // let resp: Response = resp_value.dyn_into().unwrap();
// // // Convert this other `Promise` into a rust `Future`.
// // if let Ok(json) = resp.json() {
// // if let Ok(json) = JsFuture::from(json).await {
// // crate::log(&format!("{json:?}"));
// // // let json = ;
// // }
// // }
// // // crate::log(text);
// // // Any follow-up work here
// // });
// PromiseWrapper::new(promise)
// // Ok(())
// }
// pub fn get_async_callback<R, F>(&self, url: &str, callback: F) -> Result<()>
// where
// F: FnOnce(R) + 'static,
// R: DeserializeOwned + 'static,
// {
// if self.token.is_none() {
// return Err(ModuleError::Error("Not authenticated".into()));
// }
// let token = self.token.clone().unwrap();
// let url = url.to_string();
// let state_clone = self.auth_state.clone();
// wasm_bindgen_futures::future_to_promise(async move {
// let result = Self::get_async(&url, &token).await;
// match result {
// Ok(result) => callback(result),
// Err(err) => (*state_clone.lock()) = AuthState::Error(err.into()),
// }
// Ok(JsValue::NULL)
// });
// Ok(())
// }
// async fn get_async<R>(url: &str, token: &Token) -> Result<R>
// where
// R: DeserializeOwned + 'static,
// {
// let res = reqwest::Client::new()
// .get(format!("http://localhost:8080{url}"))
// .bearer_auth(&token.token)
// .send()
// .await
// .map_err(|e| ModuleError::Error(e.to_string()))?;
// match res.error_for_status() {
// Ok(res) => res
// .json()
// .await
// .map_err(|e| ModuleError::Error(e.to_string())),
// Err(err) => Err(ModuleError::Error(format!(
// "Server returned error: {err:?}"
// ))),
// }
// // .json::<R>()
// // .await
// // .map_err(|e| ModuleError::Error(e.to_string()))?;
// // serde_json::from_str(&res).map_err(|e| ModuleError::SerdeJsonError(e.to_string()))
// }
}
View File
+190
View File
@@ -0,0 +1,190 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use egui_extras::{Column, TableBuilder};
use crate::auth::Auth;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Config {
tree_option: String,
#[serde(skip)]
state: Arc<Mutex<ConfigState>>,
// trees: Arc<Mutex<Option<Vec<String>>>>,
// #[serde(skip)]
// is_requesting: Arc<AtomicBool>,
}
#[derive(Default)]
struct ConfigState {
trees: Option<Vec<String>>,
tree_keys: Option<HashMap<String, String>>,
is_requesting: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
tree_option: "".into(),
state: Arc::new(Mutex::new(ConfigState {
trees: None,
tree_keys: None,
is_requesting: false,
})), // trees: Arc::new(Mutex::new(None)),
// is_requesting: Arc::new(AtomicBool::new(false)),
}
}
}
impl Config {
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
let state_lock = self.state.lock().unwrap();
let tree_list_none = state_lock.trees.is_none();
let key_list_none = state_lock.tree_keys.is_none();
let is_requesting = state_lock.is_requesting;
if !tree_list_none
&& !state_lock
.trees
.as_ref()
.unwrap()
.contains(&self.tree_option)
{
self.tree_option.clear();
}
drop(state_lock);
ui.horizontal(|ui| {
if tree_list_none && !is_requesting {
self.state.lock().unwrap().is_requesting = true;
let state_clone = self.state.clone();
auth.get("/api/trees", move |response: Vec<String>| {
let mut state_lock = state_clone.lock().unwrap();
state_lock.trees = Some(response);
state_lock.is_requesting = false;
drop(state_lock);
})
.unwrap();
} else if tree_list_none && is_requesting {
ui.spinner();
}
// let state_lock = self.state.lock().unwrap();
// // This might have changed since the above api call
// tree_list_none = state_lock.trees.is_none();
if !self.tree_option.is_empty() && !tree_list_none {
// ui.horizontal(|ui| {
// ui.label(&format!("Tree: {}", self.tree_option));
// });
if key_list_none && !is_requesting {
self.state.lock().unwrap().is_requesting = true;
let state_clone = self.state.clone();
auth.get(
&format!("/api/values/{}", self.tree_option),
move |response: Result<HashMap<String, String>, String>| {
let mut state_lock = state_clone.lock().unwrap();
state_lock.tree_keys = Some(response.unwrap());
state_lock.is_requesting = false;
},
)
.unwrap();
} else if key_list_none && is_requesting {
ui.spinner();
}
}
});
if !key_list_none {
// let body_text_size = TextStyle::Body.resolve(ui.style()).size;
// use egui_extras::{Size, StripBuilder};
// StripBuilder::new(ui)
// .size(Size::remainder().at_least(100.0)) // for the table
// .size(Size::exact(body_text_size))
// .vertical(|mut strip| {
// strip.cell(|ui| {
egui::ScrollArea::both().show(ui, |ui| {
let table = TableBuilder::new(ui)
.striped(true)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto())
.column(Column::auto())
.min_scrolled_height(0.0)
.sense(egui::Sense::click());
table
.header(20., |mut header| {
header.col(|ui| {
ui.strong("key");
});
header.col(|ui| {
ui.strong("value");
});
})
.body(|mut body| {
let state_lock = self.state.lock().unwrap();
let map = state_lock.tree_keys.as_ref().unwrap();
for (key, value) in (map).iter() {
// // let runtime = self.current_runtimes
body.row(18., |mut row| {
row.col(|ui| {
ui.label(key);
});
row.col(|ui| {
ui.label(value);
});
});
}
});
});
// });
// });
}
}
pub fn titlebar_buttons(&mut self, ui: &mut egui::Ui) {
let state_lock = self.state.lock().unwrap();
let mut tree_list_none = state_lock.trees.is_none();
drop(state_lock);
if ui.button("Refresh").clicked() {
let mut state_lock = self.state.lock().unwrap();
(*state_lock).trees = None;
(*state_lock).tree_keys = None;
drop(state_lock);
tree_list_none = true;
}
if !tree_list_none {
let state_lock = self.state.lock().unwrap();
let trees = state_lock.trees.as_ref().unwrap().clone();
drop(state_lock);
let before = &self.tree_option.clone();
egui::ComboBox::from_id_salt("Select Tree")
.selected_text(&self.tree_option)
.show_ui(ui, |ui| {
for tree in trees {
ui.selectable_value(&mut self.tree_option, tree.clone(), tree);
}
});
if before.ne(&self.tree_option) {
(*self.state.lock().unwrap()).tree_keys = None;
}
}
}
}
+230
View File
@@ -0,0 +1,230 @@
use egui::{TextStyle, Ui};
use egui_extras::{Column, TableBuilder};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Config {
state: ConfigState,
current_payloads: Vec<PayloadConfig>,
}
#[derive(serde::Deserialize, serde::Serialize)]
enum ConfigState {
Base,
NewConfig(PayloadConfig),
EditConfig(usize, PayloadConfig),
}
impl Default for Config {
fn default() -> Self {
Self {
state: ConfigState::Base,
current_payloads: vec![PayloadConfig {
name: "Test".to_string(),
components: vec!["server".to_string()],
// runtimes: Vec::new(),
}],
}
}
}
impl Config {
// pub fn title(&self) -> &str {
// match self.state {
// ConfigState::Base => "Config",
// ConfigState::NewConfig(..) => "Config/New",
// ConfigState::EditConfig(..) => "Config/Edit",
// }
// }
pub fn update(&mut self, ui: &mut Ui) {
match &mut self.state {
ConfigState::Base => self.table_ui(ui),
ConfigState::EditConfig(_, config) => {
ui.heading("Edit Payload");
match Self::edit_ui(ui, config) {
Some(true) => {
if let ConfigState::EditConfig(n, config) =
std::mem::replace(&mut self.state, ConfigState::Base)
{
self.current_payloads[n] = config;
}
}
Some(false) => {
self.state = ConfigState::Base;
}
_ => {}
}
}
ConfigState::NewConfig(config) => {
ui.heading("Edit Payload");
match Self::edit_ui(ui, config) {
Some(true) => {
if let ConfigState::NewConfig(config) =
std::mem::replace(&mut self.state, ConfigState::Base)
{
self.current_payloads.push(config);
}
}
Some(false) => {
self.state = ConfigState::Base;
}
_ => {}
}
}
}
}
fn table_ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.heading("Payloads");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("New").clicked() {
self.state = ConfigState::NewConfig(PayloadConfig::new());
}
});
});
let body_text_size = TextStyle::Body.resolve(ui.style()).size;
use egui_extras::{Size, StripBuilder};
StripBuilder::new(ui)
.size(Size::remainder().at_least(100.0)) // for the table
.size(Size::exact(body_text_size)) // for the source code link
.vertical(|mut strip| {
strip.cell(|ui| {
egui::ScrollArea::horizontal().show(ui, |ui| {
let table = TableBuilder::new(ui)
.striped(true)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto().resizable(false))
.column(Column::remainder())
.column(Column::remainder())
.column(Column::remainder())
.column(Column::auto().resizable(false))
.min_scrolled_height(0.0)
.sense(egui::Sense::click());
table
.header(20., |mut header| {
header.col(|ui| {
ui.strong("#");
});
header.col(|ui| {
ui.strong("Name");
});
header.col(|ui| {
ui.strong("Components");
});
header.col(|ui| {
ui.strong("Runtimes");
});
header.col(|_| {});
})
.body(|mut body| {
for i in 0..self.current_payloads.len() {
// let runtime = self.current_runtimes
body.row(18., |mut row| {
row.col(|ui| {
ui.label(i.to_string());
});
row.col(|ui| {
ui.label(self.current_payloads[i].name.clone());
});
row.col(|ui| {
ui.label(format!(
"{:?}",
self.current_payloads[i].components.clone()
));
});
row.col(|ui| {
ui.label("A");
});
row.col(|ui| {
if ui.button("Edit").clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
// if ui.button("Delete").clicked() {
// self.state = ConfigState::EditConfig(
// i,
// self.current_payloads[i].clone(),
// );
// }
});
if row.response().clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
});
}
});
});
});
});
}
fn edit_ui(ui: &mut Ui, config: &mut PayloadConfig) -> Option<bool> {
ui.horizontal(|ui| {
ui.label("Name");
ui.text_edit_singleline(&mut config.name);
});
ui.horizontal(|ui| {
ui.label("Components: ");
for component in vec!["client", "server"] {
let enabled = config
.components
.iter()
.enumerate()
.find(|s| s.1.eq(component));
if ui.selectable_label(enabled.is_some(), component).clicked() {
if let Some((i, _)) = enabled {
let _ = config.components.remove(i);
} else {
config.components.push(component.to_string());
}
}
}
});
let mut ret = None;
ui.horizontal(|ui| {
ret = if ui.button("Back").clicked() {
Some(false)
} else if ui.button("Save").clicked() {
Some(true)
} else {
None
};
});
ret
// return None;
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
struct PayloadConfig {
name: String,
components: Vec<String>,
// runtimes: Vec<RuntimeConfig>,
}
impl PayloadConfig {
pub fn new() -> Self {
Self {
name: "New Payload".to_string(),
components: Vec::new(),
// runtimes: Vec::new(),
}
}
}
+93
View File
@@ -0,0 +1,93 @@
use egui::{Pos2, Rect, UiBuilder, Vec2};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct DraggableContainer {
pub pos: egui::Vec2, // Offset from center of clip_rect
pub size: egui::Vec2,
is_dragging: bool,
drag_offset: egui::Vec2,
drag_id: String,
pub vel: egui::Vec2,
}
impl DraggableContainer {
// pub fn new(center_offset: egui::Vec2, size: egui::Vec2, id: usize) -> Self {
// Self {
// pos: center_offset,
// size,
// is_dragging: false,
// drag_offset: egui::Vec2::ZERO,
// drag_id: format!("flowchart_drag_area{}", id),
// vel: Vec2::ZERO,
// }
// }
pub fn new_zero(id: usize) -> Self {
Self {
pos: Vec2::ZERO,
size: Vec2 { x: 100., y: 100. },
is_dragging: false,
drag_offset: egui::Vec2::ZERO,
drag_id: format!("flowchart_drag_area{}", id),
vel: Vec2::ZERO,
}
}
pub fn show<R>(
&mut self,
ui: &mut egui::Ui,
add_contents: impl FnOnce(&mut egui::Ui, &Rect) -> R,
) -> R {
// Calculate center of the clip rect
let clip_center = Pos2::ZERO;
// Calculate actual position from center offset
let center_pos = clip_center + self.pos;
let pos = center_pos - self.size / 2.0; // Top-left corner from center
let rect = egui::Rect::from_min_size(pos, self.size);
// Handle dragging logic
let response = ui.interact(rect, ui.id().with(&self.drag_id), egui::Sense::drag());
// if response.secondary_clicked() {
// }
if response.drag_started() {
self.is_dragging = true;
if let Some(pointer_pos) = ui.input(|i| i.pointer.latest_pos()) {
let pointer_pos = ui
.ctx()
.layer_transform_from_global(ui.painter().layer_id())
.unwrap_or_default()
* pointer_pos;
self.drag_offset = center_pos - pointer_pos;
}
}
if response.dragged() && self.is_dragging {
// Pointer code from https://github.com/emilk/egui/pull/7149
if let Some(pointer_pos) = ui.input(|i| i.pointer.latest_pos()) {
let pointer_pos = ui
.ctx()
.layer_transform_from_global(ui.painter().layer_id())
.unwrap_or_default()
* pointer_pos;
let new_center = pointer_pos + self.drag_offset;
self.pos = new_center - clip_center;
}
}
if response.drag_stopped() {
self.is_dragging = false;
}
// Create a child UI at the specified position
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
// Add contents
add_contents(&mut child_ui, &rect)
}
}
+154
View File
@@ -0,0 +1,154 @@
use egui::{Color32, Painter, Pos2, Rect, Scene, Shape, Ui};
use crate::flowchart::CONNECTION_STROKE;
use crate::flowchart::GROUP_BORDER_MARGIN;
use crate::flowchart::ITERATIONS;
use crate::flowchart::RESOLUTION;
use crate::flowchart::container::DraggableContainer;
use crate::flowchart::group::convex_hull;
use crate::flowchart::{BG_STROKE, TARGET_LINE_GAP};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct FlowChart {
scene_rect: Rect,
pub containers: Vec<DraggableContainer>,
pub connections: Vec<(usize, usize)>,
pub groups: Vec<Vec<usize>>,
}
impl Default for FlowChart {
fn default() -> Self {
let mut this = Self {
scene_rect: Rect::ZERO,
containers: vec![
DraggableContainer::new_zero(0),
DraggableContainer::new_zero(1),
DraggableContainer::new_zero(2),
DraggableContainer::new_zero(3),
DraggableContainer::new_zero(4),
DraggableContainer::new_zero(5),
DraggableContainer::new_zero(6),
DraggableContainer::new_zero(7),
],
connections: vec![(0, 1), (1, 2), (1, 3), (1, 4), (3, 5), (3, 6), (3, 7)],
groups: vec![vec![1, 3, 5, 7]],
};
this.arrange_circle();
this.arrange();
this
}
}
impl FlowChart {
pub fn arrange(&mut self) {
for _ in 0..ITERATIONS {
self.force(RESOLUTION);
}
}
fn paint_bg(rect: &Rect, painter: &Painter) {
let h_start = (rect.min.x / TARGET_LINE_GAP).round() as i32;
let h_end = ((rect.min.x + rect.width()) / TARGET_LINE_GAP).round() as i32 + 1;
for n in h_start..h_end {
painter.vline(n as f32 * TARGET_LINE_GAP, rect.y_range(), BG_STROKE);
}
let v_start = (rect.min.y / TARGET_LINE_GAP).round() as i32;
let v_end = ((rect.min.y + rect.height()) / TARGET_LINE_GAP).round() as i32 + 1;
for n in v_start..v_end {
painter.hline(rect.x_range(), n as f32 * TARGET_LINE_GAP, BG_STROKE);
}
}
fn paint_groups(groups: &Vec<Vec<usize>>, containers: &Vec<DraggableContainer>, ui: &mut Ui) {
for group in groups {
let mut points = Vec::new();
for n in group {
let container = &containers[*n];
let pos = container.pos.to_pos2();
let size = container.size;
points.append(&mut vec![
Pos2 {
x: pos.x - size.x / 2. - GROUP_BORDER_MARGIN,
y: pos.y - size.x / 2. - GROUP_BORDER_MARGIN,
},
Pos2 {
x: pos.x + size.x / 2. + GROUP_BORDER_MARGIN,
y: pos.y - size.y / 2. - GROUP_BORDER_MARGIN,
},
Pos2 {
x: pos.x - size.x / 2. - GROUP_BORDER_MARGIN,
y: pos.y + size.y / 2. + GROUP_BORDER_MARGIN,
},
Pos2 {
x: pos.x + size.x / 2. + GROUP_BORDER_MARGIN,
y: pos.y + size.y / 2. + GROUP_BORDER_MARGIN,
},
]);
}
let points = convex_hull(&points);
ui.painter().add(Shape::convex_polygon(
points,
Color32::DEBUG_COLOR,
BG_STROKE,
));
}
}
pub fn paint(&mut self, ui: &mut Ui) {
let scene = Scene::new()
// .max_inner_size([350.0, 1000.0])
.zoom_range(0.1..=2.0);
let containers = &mut self.containers;
let groups = &self.groups;
let mut inner_rect = Rect::NAN;
let rect = &self.scene_rect.clone();
let response = scene
.show(ui, &mut self.scene_rect, |mut ui| {
Self::paint_bg(rect, ui.painter());
Self::paint_groups(groups, containers, &mut ui);
for (a, b) in &self.connections {
ui.painter().line_segment(
[containers[*a].pos.to_pos2(), containers[*b].pos.to_pos2()],
CONNECTION_STROKE,
);
}
for container in containers {
container.show(&mut ui, |ui, rect| {
ui.painter().rect(
// ui.top
*rect,
0.,
Color32::PURPLE,
BG_STROKE,
egui::StrokeKind::Outside,
);
});
}
inner_rect = ui.min_rect();
})
.response;
if response.double_clicked() {
self.scene_rect = inner_rect;
}
}
pub fn titlebar_buttons(&mut self, ui: &mut egui::Ui) {
if ui.button("Arrange").clicked() {
self.arrange();
}
}
}
+106
View File
@@ -0,0 +1,106 @@
use std::f32::consts::TAU;
use egui::Vec2;
use crate::flowchart::{
ATTRACTION_STRENGTH, CENTER_ATTRACTION_STRENGTH, DAMPING, FlowChart, GROUP_ATTRACTION_STRENGTH,
REPULSION_STRENGTH, REST_LENGTH,
};
pub fn normalize(v: &Vec2) -> Vec2 {
let len = v.length();
if len > 0.0 {
Vec2 {
x: v.x / len,
y: v.y / len,
}
} else {
Vec2 { x: 0.0, y: 0.0 }
}
}
impl FlowChart {
pub fn force(&mut self, delta_time: f32) {
let num_nodes = self.containers.len();
let mut forces = vec![Vec2::new(0.0, 0.0); num_nodes];
// Calculate repulsive forces between all nodes
for i in 0..num_nodes {
for j in (i + 1)..num_nodes {
let diff = self.containers[i].pos - self.containers[j].pos;
let dist = diff.length().max(0.1); // Prevent division by zero
let force = normalize(&diff) * (REPULSION_STRENGTH / (dist * dist));
forces[i] = forces[i] + force;
forces[j] = forces[j] + (force * -1.0);
}
}
// Calculate attractive forces along connections
for &(i, j) in &self.connections {
let diff = self.containers[j].pos - self.containers[i].pos;
let dist = diff.length();
let displacement = dist - REST_LENGTH;
let force = normalize(&diff) * (displacement * ATTRACTION_STRENGTH);
forces[i] = forces[i] + force;
forces[j] = forces[j] + (force * -1.0);
}
// Apply force to center
for i in 0..num_nodes {
let diff = self.containers[i].pos;
let dist = diff.length();
let displacement = dist - REST_LENGTH;
let force = normalize(&diff) * (displacement * CENTER_ATTRACTION_STRENGTH);
forces[i] = forces[i] + force * -1.;
}
let group_avg = &self
.groups
.iter()
.map(|group| {
let mut sum = Vec2::ZERO;
for n in group {
sum += self.containers[*n].pos;
}
sum / group.len() as f32
})
.collect::<Vec<Vec2>>();
for (group, group_avg) in self.groups.iter().zip(group_avg) {
for i in 0..num_nodes {
let diff = self.containers[i].pos - *group_avg;
let dist = diff.length();
let displacement = dist - REST_LENGTH;
let force = normalize(&diff) * (displacement * GROUP_ATTRACTION_STRENGTH);
if group.contains(&i) {
forces[i] = forces[i] + force * -1.;
} else {
forces[i] = forces[i] + force;
}
}
}
// Update velocities and positions
for i in 0..num_nodes {
let c = &mut self.containers[i];
c.vel = (c.vel + forces[i] * delta_time) * DAMPING;
c.pos += c.vel * delta_time;
}
}
pub fn arrange_circle(&mut self) {
let node_count = self.containers.len() as f32;
for (i, m) in self.containers.iter_mut().enumerate() {
let ang = -(i as f32 / node_count) * TAU;
m.pos = Vec2 {
x: 300. * ang.sin(),
y: 300. * ang.cos(),
};
m.vel = Vec2::ZERO;
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use egui::Pos2;
/// Calculate the convex hull of a set of points using Graham scan
pub fn convex_hull(points: &[Pos2]) -> Vec<Pos2> {
if points.len() < 3 {
return points.to_vec();
}
let mut pts = points.to_vec();
// Find the point with lowest y-coordinate (and leftmost if tie)
let start_idx = pts
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
a.y.partial_cmp(&b.y)
.unwrap()
.then(a.x.partial_cmp(&b.x).unwrap())
})
.unwrap()
.0;
pts.swap(0, start_idx);
let start = pts[0];
// Sort points by polar angle with respect to start point
pts[1..].sort_by(|a, b| {
let angle_a = polar_angle_to(&start, a);
let angle_b = polar_angle_to(&start, b);
angle_a.partial_cmp(&angle_b).unwrap()
});
// Build convex hull
let mut hull = Vec::new();
hull.push(pts[0]);
hull.push(pts[1]);
for i in 2..pts.len() {
while hull.len() > 1
&& cross_product(&hull[hull.len() - 2], &hull[hull.len() - 1], &pts[i]) <= 0.0
{
hull.pop();
}
hull.push(pts[i]);
}
hull
}
/// Calculate cross product of vectors (self->p2) and (self->p3)
/// Positive if counter-clockwise, negative if clockwise, zero if collinear
fn cross_product(p1: &Pos2, p2: &Pos2, p3: &Pos2) -> f32 {
(p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
}
/// Calculate polar angle from self to other point
fn polar_angle_to(a: &Pos2, b: &Pos2) -> f32 {
(b.y - a.y).atan2(b.x - a.x)
}
+31
View File
@@ -0,0 +1,31 @@
mod container;
mod flowchart;
mod force;
mod group;
use egui::{Color32, Stroke};
pub use flowchart::FlowChart;
const ITERATIONS: usize = 1_000;
const RESOLUTION: f32 = 0.6;
const TARGET_LINE_GAP: f32 = 80.;
const BG_STROKE: Stroke = Stroke {
width: 0.3,
color: Color32::GRAY,
};
const CONNECTION_STROKE: Stroke = Stroke {
width: 3.,
color: Color32::WHITE,
};
const GROUP_BORDER_MARGIN: f32 = 20.;
static REPULSION_STRENGTH: f32 = 100000.0; // repulsion_strength
static ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
static CENTER_ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
static GROUP_ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
static REST_LENGTH: f32 = 50.0; // rest_length
static DAMPING: f32 = 0.9; // damping
+82
View File
@@ -0,0 +1,82 @@
use egui::{Color32, TextEdit};
use unshell::config::{ConfigStructField, InterfaceData, InterfaceStruct, config_struct};
use crate::config::Config;
struct ConfigInterface(Config);
pub fn render(
ui: &mut egui::Ui,
interface_struct: &InterfaceStruct,
interface_data: &mut InterfaceData,
) {
match (interface_struct, interface_data) {
(InterfaceStruct::ConfigStruct(interface), InterfaceData::ConfigStruct(data)) => {
render_config_struct(ui, interface, data);
}
}
}
fn render_config_struct(
ui: &mut egui::Ui,
interface: &config_struct::ConfigStructKeys,
data: &mut config_struct::ConfigStructValues,
) {
for (interface, data) in interface.iter().zip(data) {
match (interface, data) {
(ConfigStructField::Header(text), serde_json::Value::Null) => {
ui.heading(text);
}
(ConfigStructField::Text(text), serde_json::Value::Null) => {
ui.label(text);
}
(
ConfigStructField::String {
default: _,
max_length,
protected,
},
serde_json::Value::String(value),
) => {
ui.horizontal(|ui| {
let mut widget = TextEdit::singleline(value);
if let Some(limit) = &max_length {
widget = widget.char_limit(*limit);
}
let password = *protected && !ui.button("👁").is_pointer_button_down_on();
widget = widget.password(password);
// if protected
// ui.selectable_label(show_plaintext, "👁")
// .on_hover_text("Show/hide password")
// .clicked();
// {
// widget = widget.password(true);
// }
ui.add(widget);
if let Some(limit) = max_length {
ui.label(format!("{}/{}", value.len(), limit));
}
});
}
(
ConfigStructField::Integer { default, min, max },
serde_json::Value::Number(number),
) => todo!(),
(interface, data) => {
ui.colored_label(
Color32::RED,
&format!("Incorrect type and value! {interface:?} and {data:?}"),
);
}
}
}
}
+173
View File
@@ -0,0 +1,173 @@
mod interface;
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use unshell::{Result, config::TreeMessage};
use crate::auth::Auth;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct InterfaceWindow {
path: PathBuf,
// #[serde(skip)]
// data_bind: Bind<String, ModuleError>,
#[serde(skip)]
state: Arc<Mutex<InterfaceWindowState>>,
// #[serde(skip)]
// promise: Option<PromiseWrapper<Result<TreeMessage>>>,
}
pub struct InterfaceWindowState {
is_request: bool,
is_error: bool,
branch: Option<TreeMessage>,
}
impl InterfaceWindow {
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
// let data_bind = Bind::<InterfaceWindowState, ModuleError>::new(false);
ui.heading("Interface");
ui.label(self.path.to_string_lossy());
{
if !self.path.eq("/") && ui.button("Go up").clicked() {
self.go_up();
self.state.lock().unwrap().branch = None;
}
let state_lock = self.state.lock().unwrap();
let (is_request, is_error, is_some) = (
state_lock.is_request,
state_lock.is_error,
state_lock.branch.is_some(),
);
drop(state_lock);
if is_request {
ui.spinner();
} else if is_error {
self.reset_path();
let mut state_lock = self.state.lock().unwrap();
state_lock.is_error = false;
state_lock.is_request = false;
state_lock.branch = None;
drop(state_lock)
} else if !is_some {
{
let mut state_lock = self.state.lock().unwrap();
state_lock.is_request = true;
}
let state_clone = self.state.clone();
auth.get(
&format!("/api/interface{}", self.path.display()),
move |response: Result<TreeMessage>| {
let mut state_lock = state_clone.lock().unwrap();
match response {
Ok(item) => {
state_lock.branch = Some(item);
}
Err(err) => {
crate::log(&format!("Got error {err:?}"));
state_lock.is_error = true;
}
}
state_lock.is_request = false;
drop(state_lock);
},
)
.unwrap();
} else {
let state_clone = self.state.clone();
let mut state_lock = state_clone.lock().unwrap();
let branch = (state_lock.branch).as_mut().unwrap();
let clear = match branch {
TreeMessage::InterfaceAndValue(interface_struct, interface_data) => {
interface::render(ui, interface_struct, interface_data);
if ui.button("Save").clicked() {
auth.post(
&format!("/api/interface{}", self.path.display()),
&TreeMessage::State(interface_data.clone()),
move |response: Result<TreeMessage>| {
// debug!("{response:?}");
},
)
.unwrap();
}
false
}
TreeMessage::Folder(items) => {
let mut clear = false;
for item in items {
if ui.button(&format!("Item {}", item)).clicked() {
self.go_down(item.clone());
clear = true;
break;
}
}
clear
}
_ => false,
};
if clear {
state_lock.branch = None;
}
drop(state_lock)
}
}
}
fn reset_path(&mut self) {
self.path = PathBuf::from("/");
}
fn go_up(&mut self) {
if let Some(parent) = self.path.parent() {
self.path = PathBuf::from(parent);
}
}
fn go_down(&mut self, path: String) {
self.path = self.path.join(path);
}
}
impl Default for InterfaceWindow {
fn default() -> Self {
Self {
path: "/".into(),
// promise: None,
state: Arc::new(Mutex::new(InterfaceWindowState::default())),
// data_bind: Bind::new(false),
}
}
}
impl Default for InterfaceWindowState {
fn default() -> Self {
Self {
is_request: false,
branch: None,
is_error: false,
}
}
}
+90
View File
@@ -0,0 +1,90 @@
#![warn(clippy::all, rust_2018_idioms)]
#![macro_use]
// #[allow(unused_extern_crates)]
// extern crate log;
pub mod app;
mod auth;
mod blobs;
mod config;
mod flowchart;
mod interface;
mod log_viewer;
mod payload_config;
use std::time::Duration;
const FORCE_REDRAW_DELAY: Duration = Duration::from_millis(300);
// mod JsFunc {
// use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
pub fn log(text: &str);
}
#[wasm_bindgen(module = "/assets/sw.js")]
extern "C" {
pub fn httpGet(url: &str, ok_callback: JsValue);
pub fn httpPost(url: &str, data: &str, ok_callback: JsValue);
pub fn httpGetAuth(url: &str, auth: String, ok_callback: JsValue);
pub fn httpPostAuth(url: &str, auth: String, data: &str, ok_callback: JsValue);
}
// When compiling to web using trunk:
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(start)]
pub async fn run() {
use wasm_bindgen::JsCast as _;
use app::TemplateApp;
// Redirect `log` message to `console.log` and friends:
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
let web_options = eframe::WebOptions::default();
let document = web_sys::window()
.expect("No window")
.document()
.expect("No document");
let canvas = document
.get_element_by_id("the_canvas_id")
.expect("Failed to find the_canvas_id")
.dyn_into::<web_sys::HtmlCanvasElement>()
.expect("the_canvas_id was not a HtmlCanvasElement");
let start_result = eframe::WebRunner::new()
.start(
canvas,
web_options,
Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
)
.await;
// Remove the loading text and spinner:
if let Some(loading_text) = document.get_element_by_id("loading_text") {
match start_result {
Ok(_) => {
loading_text.remove();
}
Err(e) => {
loading_text.set_inner_html(
"<p> The app has crashed. See the developer console for details. </p>",
);
panic!("Failed to start eframe: {e:?}");
}
}
}
}
// }
// #[cfg(not(target_arch = "wasm32"))]
// mod JsFunc {
// pub fn httpGet(url: &str, callback: fn() -> {}) {}
// }
+207
View File
@@ -0,0 +1,207 @@
use chrono::DateTime;
use chrono::Utc;
use egui::Color32;
use egui::TextStyle;
use egui_extras::Column;
use egui_extras::TableBuilder;
use crate::auth::Auth;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::SystemTime;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct LogViewer {
enable_location: bool,
#[serde(skip)]
state: Arc<Mutex<LogState>>,
}
#[derive(Default)]
struct LogState {
logs: Vec<Record>,
// trees: Option<Vec<String>>,
// tree_keys: Option<HashMap<String, String>>,
is_requesting: bool,
requested_data: bool,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Record {
log_level: LogLevel,
location: Option<String>,
time: SystemTime,
message: String,
}
impl Record {
pub fn display_level(&self, ui: &mut egui::Ui) {
match self.log_level {
LogLevel::Debug => ui.colored_label(Color32::LIGHT_BLUE, "DBUG"),
LogLevel::Info => ui.colored_label(Color32::DARK_GREEN, "INFO"),
LogLevel::Warn => ui.colored_label(Color32::YELLOW, "WARN"),
LogLevel::Error => ui.colored_label(Color32::RED, "ERR!"),
};
}
pub fn display_location(&self, ui: &mut egui::Ui) {
if let Some(ref location) = self.location {
ui.label(location);
}
}
pub fn display_time(&self, ui: &mut egui::Ui) {
let date: DateTime<Utc> = self.time.into();
let date = date.to_rfc2822().to_string();
ui.label(date);
}
pub fn display_message(&self, ui: &mut egui::Ui) {
ui.strong(&self.message);
}
}
impl LogViewer {
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
ui.heading("Log Viewer");
ui.horizontal(|ui| {
if ui.button("Refresh").clicked() {
self.refresh_logs(auth);
}
ui.checkbox(&mut self.enable_location, "Enable Location");
});
// let logs = ;
let body_text_size = TextStyle::Body.resolve(ui.style()).size;
use egui_extras::{Size, StripBuilder};
StripBuilder::new(ui)
.size(Size::remainder().at_least(100.0)) // for the table
.size(Size::exact(body_text_size))
.vertical(|mut strip| {
strip.cell(|ui| {
egui::ScrollArea::both()
.stick_to_bottom(true)
.show(ui, |ui| {
let table = TableBuilder::new(ui)
.striped(true)
.resizable(true)
.stick_to_bottom(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.min_scrolled_height(0.0)
.sense(egui::Sense::click());
let table = if self.enable_location {
table.column(Column::auto())
} else {
table
};
table
.header(20., |mut header| {
header.col(|ui| {
ui.strong("Time");
});
header.col(|ui| {
ui.strong("Level");
});
header.col(|ui| {
ui.strong("Message");
});
if self.enable_location {
header.col(|ui| {
ui.strong("Location");
});
}
})
.body(|mut body| {
let state_lock = self.state.lock().unwrap();
// let logs = state_lock.logs.as_ref();
for log in state_lock.logs.iter() {
// // let runtime = self.current_runtimes
body.row(18., |mut row| {
row.col(|ui| log.display_time(ui));
row.col(|ui| log.display_level(ui));
row.col(|ui| log.display_message(ui));
if self.enable_location {
row.col(|ui| log.display_location(ui));
}
});
}
});
});
});
});
{
let state_lock = self.state.lock().unwrap();
match (
state_lock.is_requesting,
state_lock.logs.len() == 0,
state_lock.requested_data,
) {
(true, _, _) => {
drop(state_lock);
ui.spinner();
}
(false, true, true) => {
drop(state_lock);
ui.label("There are no logs");
}
(false, true, false) => {
drop(state_lock);
self.refresh_logs(auth);
}
_ => {
drop(state_lock);
}
}
}
}
fn refresh_logs(&self, auth: &mut Auth) {
let state_clone = self.state.clone();
{
let mut state_lock = self.state.lock().unwrap();
state_lock.logs.clear();
state_lock.is_requesting = true;
}
auth.get(&format!("/api/log/{}", 0), move |e: Vec<String>| {
let mut state_lock = state_clone.lock().unwrap();
state_lock.logs.append(
&mut e
.iter()
.map(|log| serde_json::from_str(log).unwrap())
.collect(),
);
state_lock.is_requesting = false;
state_lock.requested_data = true;
// crate::log(&format!("{e:?}"));
})
.unwrap();
}
}
impl Default for LogViewer {
fn default() -> Self {
Self {
enable_location: false,
state: Arc::new(Mutex::new(LogState::default())),
}
}
}
+21
View File
@@ -0,0 +1,21 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result {
// pretty_env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
// let native_options = eframe::NativeOptions {
// viewport: egui::ViewportBuilder::default()
// .with_inner_size([400.0, 300.0])
// .with_min_inner_size([300.0, 220.0]),
// ..Default::default()
// };
// eframe::run_native(
// "eframe template",
// native_options,
// Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
// )
todo!()
}
+30
View File
@@ -0,0 +1,30 @@
// use crate::payload_config::structs::ConfigStructField;
mod structs;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct PayloadConfig {
config_struct: structs::Config,
}
// struct ServerConfigState {
// // config: Vec<PayloadConfig>
// }
impl PayloadConfig {
pub fn update(&mut self, ui: &mut egui::Ui) {
if ui.button("export").clicked() {
crate::log(&self.config_struct.export());
}
// ui.heading("Test");
self.config_struct.update(ui);
}
}
impl Default for PayloadConfig {
fn default() -> Self {
Self {
config_struct: structs::default_configurable(),
}
}
}
+126
View File
@@ -0,0 +1,126 @@
use std::collections::HashMap;
use egui::TextEdit;
use serde_json::Value;
#[derive(serde::Deserialize, serde::Serialize)]
enum ConfigStructField {
Header(String),
Text(String),
String {
default: Option<String>,
max_length: Option<usize>,
protected: bool,
},
Integer {
default: i32,
min: Option<i32>,
max: Option<i32>,
},
// Checkbox
// Dropdown
// Collapsing header
// Slider
// ...
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Config {
config: Vec<(String, ConfigStructField)>,
state: HashMap<String, Value>,
}
impl Config {
fn new(config: Vec<(String, ConfigStructField)>) -> Self {
Self {
config,
state: HashMap::default(),
}
}
pub fn update(&mut self, ui: &mut egui::Ui) {
for (id, field) in &self.config {
match field {
ConfigStructField::Header(text) => {
ui.heading(text);
}
ConfigStructField::Text(text) => {
ui.label(text);
}
ConfigStructField::String {
default,
max_length,
protected,
} => {
let value = if let Some(Value::String(value)) = self.state.get_mut(id) {
value
} else {
self.state.insert(
id.clone(),
Value::String(default.clone().unwrap_or(String::new())),
);
if let Some(Value::String(value)) = self.state.get_mut(id) {
value
} else {
unreachable!()
}
};
let mut widget = TextEdit::singleline(value).password(*protected);
if let Some(limit) = &max_length {
widget = widget.char_limit(*limit);
}
ui.add(widget);
}
_ => {} // ConfigStructField::Integer { default, min, max } => todo!(),
}
}
// match &self.field {
// ConfigStructField::Header(text) => {
// ui.heading(text);
// }
// ConfigStructField::Text(text) => {
// ui.label(text);
// }
// ConfigStructField::String {
// default,
// max_length,
// protected,
// } => ui.text_edit_singleline(),
// ConfigStructField::Integer { default, min, max } => todo!(),
// }
}
pub fn export(&self) -> String {
serde_json::to_string(&self.config).unwrap()
}
}
pub fn default_configurable() -> Config {
Config::new(vec![
(
"Header".into(),
ConfigStructField::Header("Test Header!".into()),
),
("text".into(), ConfigStructField::Text("Test Text!".into())),
(
"Config".into(),
ConfigStructField::String {
default: Some("Test String".into()),
max_length: Some(30),
protected: false,
},
),
(
"Protected".into(),
ConfigStructField::String {
default: Some("Protected String".into()),
max_length: None,
protected: true,
},
),
])
}
+845
View File
@@ -0,0 +1,845 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.2.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1"
[[package]]
name = "hybrid-array"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0"
dependencies = [
"typenum",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding 0.3.3",
"generic-array",
]
[[package]]
name = "itoa"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "static_init"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bae1df58c5fea7502e8e352ec26b5579f6178e1fdb311e088580c980dee25ed"
dependencies = [
"bitflags 1.3.2",
"cfg_aliases 0.2.1",
"libc",
"parking_lot",
"parking_lot_core",
"static_init_macro",
"winapi",
]
[[package]]
name = "static_init_macro"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1389c88ddd739ec6d3f8f83343764a0e944cd23cfbf126a9796a714b0b6edd6f"
dependencies = [
"cfg_aliases 0.1.1",
"memchr",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unshell-crypt"
version = "0.1.0"
dependencies = [
"aes",
"block-padding 0.4.2",
"cbc",
"getrandom",
"hex",
"hex-literal",
"regex",
"sha2",
]
[[package]]
name = "unshell-lib"
version = "0.0.0"
dependencies = [
"chrono",
"serde",
"serde_json",
"unshell-obfuscate",
]
[[package]]
name = "unshell-manager"
version = "0.0.0"
dependencies = [
"bincode",
"libc",
"libloading",
"rand",
"unshell-lib",
"unshell-obfuscate",
]
[[package]]
name = "unshell-obfuscate"
version = "0.0.0"
dependencies = [
"proc-macro2",
"quote",
"rand",
"static_init",
"syn 2.0.111",
"unshell-crypt",
]
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.111",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "zerocopy"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "ush-manager"
version.workspace = true
edition.workspace = true
authors.workspace = true
include.workspace = true
[features]
log = ["unshell/log"]
log_debug = ["unshell/log_debug"]
obfuscate = ["unshell/obfuscate"]
[dependencies]
unshell = {path = "../", default-features = false}
bincode = "2.0.1"
libc = "0.2.178"
libloading = "0.8.9"
rand = "0.9.2"
+39
View File
@@ -0,0 +1,39 @@
// use std::collections::HashMap;
use std::fmt::Debug;
use unshell::Result;
use unshell::config::RuntimeConfig;
use crate::ModuleRuntime;
pub struct PayloadConfig {
pub id: &'static str,
pub components: Vec<NamedComponent>,
pub runtime_config: Vec<RuntimeConfig>,
}
#[derive(Clone)]
pub struct NamedComponent {
pub name: &'static str,
// + Sync + Sync + Sync + Sync + Sync + Sync + Sync + Sync
pub get_interface: &'static (dyn Fn() -> Option<&'static (dyn InterfaceWrapper + Sync)> + Sync),
pub start_runtime:
&'static (dyn Fn(&'static RuntimeConfig) -> Result<Box<dyn ModuleRuntime>> + Sync),
}
impl Debug for NamedComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NamedComponent")
.field("name", &self.name)
// .field("get_interface", &self.get_interface)
// .field("start_runtime", &self.start_runtime)
.finish()
}
}
/// Trait that wraps the get_interface<T>() function inside of components
pub trait InterfaceWrapper: Send + Sync {
fn get_interface<T: 'static>(&self) -> Option<T>
where
Self: Sized;
}
+28
View File
@@ -0,0 +1,28 @@
mod manager;
mod module;
mod module_interface;
// pub mod network;
mod proc_load;
pub mod interface;
use std::sync::{Arc, Mutex};
pub use manager::Manager;
pub use module::Module;
pub use interface::{InterfaceWrapper, NamedComponent, PayloadConfig};
// extern crate unshell_lib;
use unshell::Result;
/// Trait for defining modules that have a runtime.
pub trait ModuleRuntime: Send + Sync {
fn init(&mut self, manager: Arc<Mutex<Manager>>) -> Result<()>;
/// Returns true if the module is running.
/// After returning false, the module will be dropped.
fn is_running(&self) -> bool;
/// Consumes the module, implementation should kill whatever is running.
fn kill(self: Box<Self>);
}
@@ -0,0 +1,16 @@
use crate::Manager;
use unshell::Announcement;
impl Manager {
pub fn recv_announcement(&mut self, announcement: &Announcement) {
match announcement {
Announcement::TestAnnouncement(str) => {
println!("Got test announcement: {}", str)
} // Announcement::GetRuntimes => todo!(),
// Announcement::GetRuntimesAck(_) => todo!(),
// Announcement::StartRuntime(runtime_config) => todo!(),
// Announcement::StartRuntimeAck(_) => todo!(),
// _ => {}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
use unshell_lib::{Announcement, Result};
// use crate::network::Stream;
use crate::Manager;
impl Manager {
pub fn add_connection(&mut self, connection: Box<dyn Stream<Announcement>>) {
self.connections.push(connection);
}
pub fn prune_connections(&mut self) {
self.connections.retain(|c| c.is_alive());
}
pub fn recv_connection_announcements(&mut self) {
// Collect all incoming announcements
let announcements = self
.connections
.iter_mut()
.map(|c| c.try_read())
.flat_map(|array| array)
.collect::<Vec<Announcement>>();
for announcement in announcements {
self.recv_announcement(&announcement)
}
}
// pub fn broadcast(&mut self, announcement: Announcement) -> Result<()> {
// for connection in &mut self.connections {
// connection.write(announcement.clone())?;
// }
// Ok(())
// }
}
+196
View File
@@ -0,0 +1,196 @@
mod announcement;
// mod connection;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
thread::{self, JoinHandle},
time::Duration,
};
use unshell::obfuscate::symbol;
use unshell::{Result, config::RuntimeConfig, debug, warn};
use crate::{
ModuleRuntime,
interface::{NamedComponent, PayloadConfig},
module::Module,
// network::Stream,
};
// #[derive(Debug)]
pub struct Manager {
id: &'static str,
handle: Option<JoinHandle<()>>,
pub modules: Vec<Module>,
components: HashMap<String, NamedComponent>,
active_runtimes: Vec<Box<dyn ModuleRuntime>>,
// pub connections: Vec<Box<dyn Stream<Announcement>>>,
}
// static mut MANAGER_RUNTIME: Option<Arc<Mutex<Manager>>> = None;
impl Manager {
fn new(id: &'static str, components: Vec<NamedComponent>, modules: Vec<Module>) -> Self {
Self {
id,
handle: None,
modules,
components: components
.into_iter()
.map(|c| (c.name.to_string(), c))
.collect(),
active_runtimes: Vec::new(),
// connections: Vec::new(),
}
}
/// Create Manager, and run initialization for each Module
#[allow(static_mut_refs)]
pub fn start(config: &'static PayloadConfig, modules: Vec<Module>) -> Arc<Mutex<Self>> {
// Construct self
let mut this = Self::new(&config.id, config.components.clone(), modules);
debug!("Imported {} base components", this.components.len());
debug!("Imported {} base runtimes", &config.runtime_config.len());
// Load each of the pre-prepared modules
this.load_components();
let this = Arc::new(Mutex::new(this));
debug!("Creating runtimes...");
for runtime in &config.runtime_config {
Self::create_runtime(this.clone(), runtime);
}
debug!("Starting runtimes...");
for runtime in &mut this.lock().unwrap().active_runtimes {
if let Err(e) = runtime.init(this.clone()) {
warn!("Failed to start runtime: {}", e);
}
}
this.lock().unwrap().handle = Some(Self::start_thread(this.clone()));
this
}
fn load_components(&mut self) {
for module in &self.modules {
// Load get_components function from shared object library
let component_func = match module
.get_symbol::<fn() -> Vec<NamedComponent>>(symbol!("get_components").as_bytes())
{
Ok(func) => func,
Err(_) => {
warn!("get_components function not found");
continue;
}
};
let components = component_func();
let component_name = "TODO"; //TODO: Make this actually load component name
debug!("{} - Retrieved payload metadata", component_name);
// Add each component into self
for c in components {
debug!("{} - Found component '{}'", "TODO", c.name);
self.components.insert(c.name.to_owned(), c);
}
}
}
/// The manager thread. receives announcements, and kills runtimes.
fn start_thread(this: Arc<Mutex<Self>>) -> JoinHandle<()> {
thread::spawn(move || {
loop {
thread::sleep(Duration::from_millis(10));
let mut this_lock = this.lock().unwrap();
if this_lock.active_runtimes.len() <= 0 {
debug!("There are no more runtimes! Exiting...");
break;
}
this_lock.active_runtimes.retain(|runtime| {
if runtime.is_running() {
true
} else {
debug!("Runtime exited!"); //TODO: Make this better
false
}
});
// // Read announcements
// this_lock.recv_connection_announcements();
// // Prune dead connections
// this_lock.prune_connections();
drop(this_lock)
}
})
}
/// Wait for manager thread to finish.
pub fn join(this: Arc<Mutex<Self>>) {
loop {
if this.lock().unwrap().handle.as_ref().unwrap().is_finished() {
break;
}
thread::sleep(Duration::from_millis(100));
}
}
/// Start a runtime
fn create_runtime<'a>(this: Arc<Mutex<Self>>, runtime: &'static RuntimeConfig) {
let mut this_lock = this.lock().unwrap();
let component = match this_lock.components.get(&runtime.parent_component) {
Some(component) => component,
None => {
warn!(
"Could not find component '{}' which is referenced by runtime: {}",
runtime.parent_component, runtime.name
);
return;
}
};
debug!("Starting runtime: {}", runtime.name);
let runtime = match (*component.start_runtime)(runtime) {
Ok(runtime) => runtime,
Err(e) => {
warn!("Failed to start runtime: {:?}", e);
return;
}
};
this_lock.active_runtimes.push(runtime);
}
pub fn add_runtime(this: Arc<Mutex<Self>>, runtime: &'static RuntimeConfig) -> Result<()> {
Self::create_runtime(this.clone(), runtime);
this.lock()
.unwrap()
.active_runtimes
.iter_mut()
.last()
.unwrap()
.init(this.clone())
}
pub fn get_name(&self) -> &str {
self.id
}
}
+52
View File
@@ -0,0 +1,52 @@
use libloading::{Library, Symbol};
use unshell::{
ModuleError, Result,
logger::{self, SetupLogger, logger},
warn,
};
use crate::proc_load::memfd_create_dlopen;
pub struct Module {
lib: Library,
}
impl Module {
pub fn new(path: &str) -> Result<Self> {
let lib = unsafe { Library::new(&path) }
.map_err(|e| ModuleError::LibLoadingError(e.to_string()))?;
let this = Self { lib };
if let Ok(setup_logger) = this.get_symbol::<SetupLogger>(b"setup_logger") {
setup_logger(logger::logger());
} else {
warn!("setup_logger not found");
}
Ok(this)
}
// TODO: Implement actual reflective ELF loading (possibly even custom format)
// Look at https://github.com/weizhiao/rust-elfloader
pub fn new_bytes(bytes: &[u8]) -> Result<Self> {
let lib =
memfd_create_dlopen(bytes).map_err(|e| ModuleError::Error(e.to_string().into()))?;
let this = Self { lib };
if let Ok(setup_logger) = this.get_symbol::<SetupLogger>(b"setup_logger") {
setup_logger(logger());
} else {
warn!("setup_logger not found");
}
Ok(this)
}
pub fn get_symbol<T>(&self, symbol: &[u8]) -> Result<Symbol<'_, T>> {
let symbol = unsafe { self.lib.get::<T>(symbol) }
.map_err(|e| ModuleError::LinkError(format!("Failed to load symbol: {}", e)))?;
Ok(symbol)
}
}
+54
View File
@@ -0,0 +1,54 @@
/// "Module Interface" helper macro that creates a struct with function pointers
/// Useful for defining and requiring modules' functions accross FFI boundry.
#[macro_export]
macro_rules! module_interface {
($(#[$struct_meta:meta])* $interface_name:ident { $($(#[$fn_meta:meta])* fn $fn_name:ident $(<$($gen:ident),+ $(,)?>)?($($arg:ident : $ty:ty),* $(,)?) $(-> $ret:ty)? $(where $($where_clause:tt)*)?);* $(;)? }) => {
#[repr(C)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy)]
#[allow(improper_ctypes_definitions)]
$(#[$struct_meta])*
pub struct $interface_name {
$(
// This line will FAIL TO COMPILE if you use generics in the macro input.
// You MUST use concrete types like *mut c_void for "generic" data.
$fn_name: extern "C" fn($($ty),*) $(-> $ret)?,
)*
}
impl $interface_name {
$(
#[inline(always)]
$(#[$fn_meta])* // Propagate function attributes
// This is the fix for the `impl` block.
// It adds the captured generics and where-clause to the wrapper function.
pub fn $fn_name $(<$($gen),+>)? (&self, $($arg: $ty),*) $(-> $ret)?
$(where $($where_clause)*)?
{
(self.$fn_name)($($arg),*)
}
)*
/// Create from raw function pointers
///
/// # Safety
///
/// The caller must ensure all function pointers are valid and have
/// the correct signatures
pub fn from_raw(
$($fn_name: extern "C" fn($($ty),*) $(-> $ret)?),*
) -> Self {
Self {
$($fn_name),*
}
}
}
// impl crate::module::Interface for $interface_name {
// fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
// self
// }
// }
};
}
+30
View File
@@ -0,0 +1,30 @@
// mod connection;
mod tcp_stream;
pub use tcp_stream::TcpStream;
use unshell_lib::ModuleError;
/// This is the data transmission type
pub trait Stream<T>: Send + Sync {
// fn get_info(&self) -> String;
fn is_alive(&self) -> bool;
fn has_recv(&self) -> bool;
/// Possibly blocking stream read function
fn read(&mut self) -> Vec<T>;
/// Non-blocking read function
fn try_read(&mut self) -> Vec<T> {
if self.has_recv() {
self.read()
} else {
Vec::new()
}
}
fn write(&mut self, data: T) -> Result<(), ModuleError>;
fn try_clone(&self) -> Result<Box<dyn Stream<T> + Send + Sync>, ModuleError>;
}
+128
View File
@@ -0,0 +1,128 @@
use std::{
io::{Read, Write},
net,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use unshell_lib::{Announcement, ModuleError, Result, debug};
use crate::network::Stream;
pub struct TcpStream(Arc<AtomicBool>, net::TcpStream);
impl TcpStream {
pub fn new(stream: net::TcpStream) -> Self {
stream.set_nonblocking(true).unwrap();
Self(Arc::new(AtomicBool::new(true)), stream)
}
// Call this when the stream ends
fn disconnected(&mut self) {
self.0.store(false, Ordering::Relaxed);
}
}
impl Stream<Announcement> for TcpStream {
fn is_alive(&self) -> bool {
// if self.1.take_error().unwrap_or(None).is_some() {
// // self.1.pe
// warn!("Disconnected #################");
// return true;
// } else {
// return false;
// }
// let mut buf = [0u8; 1];
// match self.1.peek(&mut buf) {
// Ok(n) => n == 1,
// Err(_) => false,
// }
let mut buf = [0u8; 1];
match self.1.peek(&mut buf) {
Ok(0) => false, // Connection closed (EOF)
Ok(_) => true, // Data available or connection alive
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => true, // No data but alive
Err(_) => false, // Connection error
}
// true
// self.0.load(Ordering::Relaxed)
}
fn has_recv(&self) -> bool {
let mut buf = [0u8; 1];
match self.1.peek(&mut buf) {
Ok(n) if n > 0 => true, // Data is available
Ok(_) => false, // EOF (connection closed)
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => false, // No data
Err(_) => false,
}
// false
}
fn read(&mut self) -> Vec<Announcement> {
let mut ret = Vec::new();
while self.has_recv() {
let mut size_buf = [0u8; 4];
match self.1.read_exact(&mut size_buf) {
Ok(()) => {}
Err(_) => {
self.disconnected();
break;
}
};
let size = u32::from_be_bytes(size_buf);
let mut buf = vec![0u8; size as usize];
match self.1.read_exact(&mut buf) {
Ok(()) => {}
Err(_) => {
self.disconnected();
}
}
if let Some(a) = Announcement::decode(&buf) {
ret.push(a);
} else {
debug!("Malformed data");
}
}
ret
}
fn write(&mut self, announcement: Announcement) -> Result<()> {
let bytes = announcement.encode();
// Write length of bytes
self.1
.write_all(&u32::to_be_bytes(bytes.len() as u32))
.map_err(|e| ModuleError::Error(e.to_string().into()))?;
// Write data
self.1
.write_all(&bytes)
.map_err(|e| ModuleError::Error(e.to_string().into()))?;
// Flush data
self.1
.flush()
.map_err(|e| ModuleError::Error(e.to_string().into()))?;
Ok(())
}
fn try_clone(&self) -> Result<Box<dyn Stream<Announcement> + Send + Sync>> {
Ok(Box::new(Self(
self.0.clone(),
self.1
.try_clone()
.map_err(|e| ModuleError::Error(e.to_string().into()))?,
)))
}
}
+64
View File
@@ -0,0 +1,64 @@
// Load a shared object by saving bytes to a filesystem in /proc
use std::{ffi::CString, io};
use libloading::Library;
use unshell::{ModuleError, Result, warn};
// The `memfd_create` syscall flags (MFD_CLOEXEC is common and good practice)
const MFD_CLOEXEC: u32 = 0x0001;
const MFD_ALLOW_SEALING: u32 = 0x0002;
pub fn memfd_create_dlopen(payload: &[u8]) -> Result<Library> {
use rand::distr::{Alphanumeric, SampleString};
let string = Alphanumeric.sample_string(&mut rand::rng(), 16);
// 1. Create the anonymous in-memory file descriptor using the raw syscall
let c_name = CString::new(string)
.map_err(|e| ModuleError::LibLoadingError(format!("CString conversion failed: {e:?}")))?;
let fd = unsafe { libc::memfd_create(c_name.as_ptr(), MFD_CLOEXEC | MFD_ALLOW_SEALING) };
if fd < 0 {
return Err(ModuleError::LibLoadingError(format!(
"IO Error {:?}",
io::Error::last_os_error().to_string()
)));
}
// 2. Write the payload bytes to the in-memory file
let bytes_written =
unsafe { libc::write(fd, payload.as_ptr() as *const libc::c_void, payload.len()) };
if bytes_written != payload.len() as isize {
// If write fails or is incomplete, clean up the file descriptor
unsafe {
libc::close(fd);
}
return Err(ModuleError::LibLoadingError(
"Failed to write full payload to memfd".into(),
));
}
// Optional: Seal the file to prevent modification, common for security/integrity
// Note: The MFD_ALLOW_SEALING flag must be set during creation for this to work.
let seals = libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_WRITE;
if unsafe { libc::fcntl(fd, libc::F_ADD_SEALS, seals) } == -1 {
// Log a warning but continue if sealing fails (e.g., due to permissions)
warn!(
"memfd_create_dlopen: Failed to apply seals. Error: {}",
io::Error::last_os_error()
);
}
// 3. Construct the virtual path to the in-memory file
// This path is necessary for dlopen to work, as dlopen expects a filesystem path.
let dl_path = format!("/proc/self/fd/{}", fd);
// 4. Use dlopen (via libloading) on the virtual path
Ok(unsafe {
Library::new(&dl_path)
.map_err(|e| ModuleError::LibLoadingError(format!("Failed to import library: {}", e)))?
})
}
+1937
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "ush-server"
version.workspace = true
edition.workspace = true
authors.workspace = true
include.workspace = true
[features]
default = ["log_debug"]
log = ["unshell/log"]
log_debug = ["unshell/log_debug"]
[dependencies]
unshell = { path = "../" }
# ush-manager = { path = "../unshell-manager" }
chrono = { workspace = true }
toml = { workspace = true }
static_init = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sled = "0.34.7"
clap = {version = "4.5.53", features = ["derive"]}
axum = "0.8.7"
axum-extra = {version="0.12.2", features = ["typed-header"]}
tokio = {version="1.48.0", features = ["full"] }
jsonwebtoken = {version = "10.2.0", features = ["aws_lc_rs"]}
bcrypt = "0.17.1"
+3
View File
@@ -0,0 +1,3 @@
GET /api/interface/*/some_config -> Returns the interface and struct at that location
GET /api/interface/*/some_folder -> Returns the list of sub-folders and interfaces at that location
+105
View File
@@ -0,0 +1,105 @@
use axum::{
Extension, Json, Router,
extract::{Path, State},
middleware,
routing::{get, post},
};
use tokio::net::TcpListener;
use unshell::{debug, info};
// axum_extra::
use crate::{auth, auth::structs::CurrentUser, logger::Logger, server::Server};
macro_rules! route_get {
($router:expr, $path:expr, $func:expr) => {{
{
$router.route(
$path,
get($func).layer(middleware::from_fn(auth::authorize)),
)
}
}};
}
macro_rules! route_post {
($router:expr, $path:expr, $func:expr) => {{
{
$router.route(
$path,
post($func).layer(middleware::from_fn(auth::authorize)),
)
}
}};
}
pub async fn start_api(address: &str, server: Server) {
let listener = TcpListener::bind(address)
.await
.expect("Unable to start listener");
info!("Listening on {}", listener.local_addr().unwrap());
let mut router = Router::new().route("/api/auth", post(auth::sign_in));
router = route_trees(router);
router = route_get!(router, "/api/log/{*offset}", Logger::poll_logs_api);
router = route_get!(router, "/api/trees", Server::get_trees_api);
router = route_get!(router, "/api/keys/{*path}", Server::all_tree_keys_api);
router = route_get!(router, "/api/values/{*path}", Server::all_tree_values_api);
router = route_get!(router, "/api/interface/", Server::get_tree2_root);
router = route_get!(router, "/api/interface/{*path}", Server::get_tree2);
// router = router.route("/api/interface", get(Server::get_tree2_root));
// router = router.route("/api/interface/{*path}", post(Server::post_tree2));
router = route_post!(router, "/api/interface/{*path}", Server::post_tree2);
// router = route_get_log(router);
axum::serve(listener, router.with_state(server))
.await
.expect("Error serving application");
}
// Loop through all trees and add /api/<tree>/<path> POST aand GET listeners for them
fn route_trees(mut router: Router<Server>) -> Router<Server> {
for tree in crate::DATABASE_TREES.iter() {
router = router
// Route GET requests to this tree
.route(
&format!("/api/{}/{{*path}}", tree),
get(
async |State(server): State<Server>,
Path(path): Path<String>,
Extension(_): Extension<CurrentUser>| {
let result = server.get_value(tree, &path);
debug!("GET /api/{}/{}", tree.to_string(), path);
Json(serde_json::to_value(result).unwrap())
},
)
.layer(middleware::from_fn(auth::authorize)),
)
// Route POST requests to this tree
.route(
&format!("/api/{}/{{*path}}", tree),
post(
async |State(server): State<Server>,
Path(path): Path<String>,
Extension(_): Extension<CurrentUser>,
body: String| {
let result = server.put_value(tree, &path, &body);
debug!("POST /api/{}/{}", tree.to_string(), path);
Json(serde_json::to_value(result).unwrap())
},
)
.layer(middleware::from_fn(auth::authorize)),
);
}
router
}
+130
View File
@@ -0,0 +1,130 @@
pub mod structs;
use axum::{
body::Body,
extract::{Json, Request},
http::{self, Response, StatusCode},
middleware::Next,
};
use bcrypt::{DEFAULT_COST, hash, verify};
use chrono::Utc;
use jsonwebtoken::{Header, TokenData, Validation, decode, encode};
use serde_json::{Value, json};
use unshell::{debug, info};
use crate::{EXPIRE_DURATION, JWT_DECODING_KEY, JWT_ENCODING_KEY};
use structs::{AuthError, Cliams, CurrentUser, SignInData};
pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
let hash = hash(password, DEFAULT_COST)?;
Ok(hash)
}
pub fn encode_jwt(email: String) -> Result<(String, usize), StatusCode> {
let now = Utc::now();
let exp = (now + EXPIRE_DURATION).timestamp() as usize;
let iat = now.timestamp() as usize;
let claim = Cliams { iat, exp, email };
let token = encode(&Header::default(), &claim, &JWT_ENCODING_KEY)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((token, exp))
}
pub fn decode_jwt(jwt: String) -> Result<TokenData<Cliams>, StatusCode> {
decode(&jwt, &JWT_DECODING_KEY, &Validation::default())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
pub async fn authorize(mut req: Request, next: Next) -> Result<Response<Body>, AuthError> {
let auth_header = req.headers_mut().get(http::header::AUTHORIZATION);
let auth_header = match auth_header {
Some(header) => header.to_str().map_err(|_| AuthError {
message: "Empty header is not allowed".to_string(),
status_code: StatusCode::FORBIDDEN,
})?,
None => {
return Err(AuthError {
message: "Please add the JWT token to the header".to_string(),
status_code: StatusCode::FORBIDDEN,
});
}
};
let mut header = auth_header.split_whitespace();
let (_, token) = (header.next(), header.next());
let _token_data: TokenData<Cliams> = match decode_jwt(token.unwrap().to_string()) {
Ok(data) => data,
Err(_) => {
return Err(AuthError {
message: "Invalid Session".to_string(),
status_code: StatusCode::UNAUTHORIZED,
});
}
};
// // Fetch the user details from the database
// let current_user = match retrieve_user_by_email(&token_data.claims.email) {
// Some(user) => user,
// None => {
// return Err(AuthError {
// message: "Unauthorized".to_string(),
// status_code: StatusCode::UNAUTHORIZED,
// });
// }
// };
// req.extensions_mut().insert(current_user);
Ok(next.run(req).await)
}
pub async fn sign_in(Json(user_data): Json<SignInData>) -> Result<Json<Value>, StatusCode> {
// 1. Retrieve user from the database
let user = match retrieve_user_by_email(&user_data.username) {
Some(user) => user,
None => {
debug!(
"Denied user {}: Could not find user data",
user_data.username
);
return Err(StatusCode::UNAUTHORIZED);
} // User not found
};
// 2. Compare the password
if !verify(&user_data.password, &user.password_hash)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
// Handle bcrypt errors
{
debug!("Denied user {}: Incorrect password hash", user.username);
return Err(StatusCode::UNAUTHORIZED); // Wrong password
}
info!(
"Authenticated user {} for {}",
user_data.username, EXPIRE_DURATION
);
// 3. Generate JWT
let (token, experation) =
encode_jwt(user.username).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// 4. Return the token
Ok(Json(json!({
"token": token,
"expiration": experation,
})))
}
fn retrieve_user_by_email(_email: &str) -> Option<CurrentUser> {
let current_user: CurrentUser = CurrentUser {
username: "foo".to_string(),
password_hash: hash_password("bar").unwrap(),
};
Some(current_user)
}
+45
View File
@@ -0,0 +1,45 @@
use axum::{
Json,
body::Body,
http::{Response, StatusCode},
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize)]
pub struct SignInData {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone)]
pub struct CurrentUser {
pub username: String,
pub password_hash: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Cliams {
// pub exp: u128,
// pub iat: u128,
//
pub exp: usize,
pub iat: usize,
pub email: String,
}
pub struct AuthError {
pub message: String,
pub status_code: StatusCode,
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response<Body> {
let body = Json(json!({
"error": self.message,
}));
(self.status_code, body).into_response()
}
}
+59
View File
@@ -0,0 +1,59 @@
mod api;
mod auth;
// mod config;
pub mod logger;
mod server;
// use math
pub use server::Server;
use static_init::dynamic;
#[static_init::dynamic]
pub static DATABASE_TREES: Vec<&'static str> = vec!["users"];
#[static_init::dynamic]
pub static DEFAULT_HOST: String = "localhost".to_string();
#[static_init::dynamic]
pub static DATABASE_NAME: String = "database".to_string();
#[static_init::dynamic]
pub static SERVER_CONFIG: unshell_manager::PayloadConfig = unshell_manager::PayloadConfig {
id: "Server",
components: Vec::new(),
runtime_config: Vec::new(),
};
// Constants for server config
pub use api::start_api;
use chrono::Duration;
use jsonwebtoken::{DecodingKey, EncodingKey};
static EXPIRE_DURATION: Duration = Duration::hours(12);
#[dynamic]
static JWT_SECRET: String = {
if let Ok(env_secret) = std::env::var("JWT_SECRET") {
env_secret
} else {
println!(
r#"
##############
# WARNING: You are using the default JWT secret, used for creating user sessions
# With this default key, anyone can login as any user.
##############"#
);
"DEFAULT_SECRET".to_string()
}
};
// std::env::var("JWT_SECRET").unwrap_or(|| -> String {
// return "TEST".to_string();
// }());
#[dynamic]
static JWT_ENCODING_KEY: EncodingKey = EncodingKey::from_secret(JWT_SECRET.as_bytes());
#[dynamic]
static JWT_DECODING_KEY: DecodingKey = DecodingKey::from_secret(JWT_SECRET.as_bytes());
+100
View File
@@ -0,0 +1,100 @@
use axum::extract::{Path, State};
use axum::{Extension, Json};
use chrono::Local;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use unshell::debug;
use crate::Server;
use crate::auth::structs::CurrentUser;
const LOG_DIR: &str = "logs";
const LOG_COUNT: usize = 100;
/// The full path to the log file.
/// Initialized once based on the startup time.
#[static_init::dynamic]
static LOG_FILE_PATH: PathBuf = {
let log_dir_path = PathBuf::from(LOG_DIR);
if let Err(e) = fs::create_dir_all(&log_dir_path) {
eprintln!("Error creating log directory {:?}: {}", log_dir_path, e);
panic!("Failed to initialize log directory.");
}
let now = Local::now();
let filename = format!("{}.log", now.format("%Y%m%d_%H%M%S"));
log_dir_path.join(filename)
};
/// A static utility module for logging operations.
pub struct Logger;
impl Logger {
pub fn log(message: String) {
let log_line = format!("{}\n", message);
match OpenOptions::new()
.create(true)
.append(true)
.open(&*LOG_FILE_PATH)
{
Ok(mut file) => {
// 3. Write the log line to the file
if let Err(e) = file.write_all(log_line.as_bytes()) {
eprintln!("Error writing log to file {:?}: {}", *LOG_FILE_PATH, e);
}
}
Err(e) => {
eprintln!("Error opening log file {:?}: {}", *LOG_FILE_PATH, e);
}
}
}
pub fn poll_logs(offset: usize) -> Vec<String> {
match File::open(&*LOG_FILE_PATH) {
Ok(file) => {
let reader = BufReader::new(file);
let lines: Vec<String> = reader
.lines()
.filter_map(|line| line.ok()) // Ignore lines that fail to read
.collect();
let total_lines = lines.len();
if offset >= total_lines {
return Vec::new();
}
let start_index = total_lines
.checked_sub(offset)
.and_then(|i| i.checked_sub(LOG_COUNT))
.unwrap_or(0);
let end_index = total_lines.checked_sub(offset).unwrap_or(total_lines);
let slice = &lines[start_index..end_index];
slice.iter().cloned().collect()
}
Err(e) => {
eprintln!("Error reading log file {:?}: {}", *LOG_FILE_PATH, e);
Vec::new()
}
}
}
pub async fn poll_logs_api(
State(_): State<Server>,
Extension(_): Extension<CurrentUser>,
Path(offset): Path<usize>,
) -> axum::Json<serde_json::Value> {
debug!("GET /api/log/{}", offset);
let result = Self::poll_logs(offset);
Json(serde_json::to_value(result).unwrap())
}
}
+45
View File
@@ -0,0 +1,45 @@
use std::{error::Error, path::PathBuf};
use unshell_server::{Server, start_api};
use clap::Parser;
use unshell_server::{DATABASE_NAME, DEFAULT_HOST};
/// A fictional versioning CLI
#[derive(Debug, Parser)]
#[command(name = "unshell-server")]
#[command(about = "UnShell server", long_about = None)]
pub struct Args {
/// Host to listen on
#[clap(long, default_value_t = DEFAULT_HOST.clone())]
host: String,
/// Port to listen
#[arg(short, long, default_value_t = 3000)]
port: usize,
/// Name of database folder
#[clap(short, long, default_value_t = DATABASE_NAME.clone())]
database_name: String,
/// Load config from path
#[clap(short, long, value_parser)]
pub config: Vec<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
unshell::logger::PrettyLogger::init_output(|message| {
if let Ok(json) = serde_json::to_string(message) {
unshell_server::logger::Logger::log(json);
}
});
let database = Server::new(args.config, args.database_name)?;
start_api(&format!("{}:{}", args.host, args.port), database).await;
Ok(())
}
+9
View File
@@ -0,0 +1,9 @@
use serde_json::Value;
use crate::Server;
impl Server {
pub fn get_blobs(&self) -> Result<Vec<Value>, String> {
Ok(Vec::new())
}
}
+100
View File
@@ -0,0 +1,100 @@
use std::collections::HashMap;
use axum::{
Extension, Json,
extract::{Path, State},
};
use serde_json::Value;
use sled::Tree;
use unshell::{debug, error};
use crate::{auth::structs::CurrentUser, server::Server};
impl Server {
fn get_tree(&self, tree_name: &str) -> Result<Tree, String> {
self.db.open_tree(tree_name).map_err(|e| {
error!("DB Failed to open tree: {}", e);
"Internal server error".to_string()
})
}
pub async fn get_trees_api(State(server): State<Server>) -> Json<Value> {
debug!("GET tree list");
let result = server
.db
.tree_names()
.iter()
.map(|n| String::from_utf8_lossy(&n.to_vec()).to_string())
.collect::<Vec<String>>();
Json(serde_json::to_value(result).unwrap())
}
pub fn put_value(&self, tree_name: &str, key: &str, value: &str) -> Result<(), String> {
match self.get_tree(tree_name)?.insert(key, value) {
Ok(_) => Ok(()),
Err(e) => {
error!("Failed to load '{}' from database: {}", key, e);
Err("Internal server error".to_string())
}
}
}
pub fn get_value(&self, tree_name: &str, key: &str) -> Result<String, String> {
match self.get_tree(tree_name)?.get(key) {
Ok(v) => match v {
Some(v) => Ok(String::from_utf8_lossy(&v.to_vec()).to_string()),
None => Err(format!("Could not find key '{}'", key)),
},
Err(e) => {
error!("Failed to load '{}' from database: {}", key, e);
Err("Internal server error".to_string())
// Err(e.to_string())
}
}
}
fn get_keys(&self, tree_name: &str) -> Result<Vec<String>, String> {
Ok(self
.get_tree(tree_name)?
.iter()
.keys()
.map(|key| {
String::from_utf8_lossy(&key.expect("This key should exist").to_vec()).to_string()
})
.collect::<Vec<String>>())
}
// Route the "keys" api for each tree
pub async fn all_tree_keys_api(
State(server): State<Server>,
Path(tree_name): Path<String>,
Extension(_): Extension<CurrentUser>,
) -> Json<Value> {
let result = server.get_keys(&tree_name);
Json(serde_json::to_value(result).unwrap())
}
// Route the "values" api to get all the values for each tree
pub async fn all_tree_values_api(
State(server): State<Server>,
Path(tree_name): Path<String>,
Extension(_): Extension<CurrentUser>,
) -> Json<Value> {
let result = || -> Result<HashMap<String, String>, String> {
Ok(server
.get_keys(&tree_name)?
.iter()
.map(|key| -> Result<(String, String), String> {
Ok((key.clone(), server.get_value(&tree_name, &key)?))
})
.collect::<Result<Vec<(String, String)>, String>>()?
.into_iter()
.collect::<HashMap<String, String>>())
}();
Json(serde_json::to_value(result).unwrap())
}
}
+81
View File
@@ -0,0 +1,81 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use unshell::{
ModuleError, Result,
config::{ConfigStructField, Tree, TreeMessage, config_struct::Config},
};
use unshell_manager::Manager;
mod blobs;
mod database;
mod tree2;
#[derive(Clone)]
pub struct Server {
// pub component_configs: Vec<crate::config::ComponentState>,
// pub interface: InterfaceWrapper,
pub manager: Arc<Mutex<Manager>>,
pub db: sled::Db,
// pub tree: Tree2,
test_thing: Arc<Mutex<Config>>,
}
impl Server {
pub fn new(_config_paths: Vec<PathBuf>, database: String) -> Result<Self> {
// let mut component_configs: Vec<crate::config::ComponentState> = Vec::new(1);
// for config in &config_paths {
// component_configs.extend(crate::config::load_config(config)?);
// }
Ok(Self {
// component_configs,
manager: Manager::start(&crate::SERVER_CONFIG, Vec::new()),
db: sled::open(database).map_err(|e| ModuleError::DatabaseError(e.to_string()))?,
test_thing: Arc::new(Mutex::new(Config::new(vec![
ConfigStructField::Header("Test Heading".into()),
ConfigStructField::Text("Test Texttttttttttttttt".into()),
ConfigStructField::String {
default: "Test Texttttttttttttttt".into(),
max_length: None,
protected: true,
},
ConfigStructField::String {
default: "Test ".into(),
max_length: Some(15),
protected: false,
},
]))),
// tree: Tree2::default(),
// interface: get_test_interface(),
})
}
}
impl Tree for Server {
fn is_folder() -> bool {
true
}
fn get_children_string(&self) -> Vec<String> {
vec!["connection_count".into()]
}
fn select_child(&mut self, child: &str, message: TreeMessage) -> Result<TreeMessage> {
match child {
"connection_count" => self.test_thing.lock().unwrap().get(message),
_ => Err("No such child".into()),
}
}
}
impl Drop for Server {
fn drop(&mut self) {
self.db.flush().expect("Failed to flush database on drop");
// Manager::join(self.manager.clone());
}
}
+53
View File
@@ -0,0 +1,53 @@
use axum::{
Json,
extract::{Path, State},
};
use serde_json::Value;
use unshell::{
ModuleError,
config::{Tree, TreeMessage},
debug,
};
use crate::Server;
impl Server {
pub async fn get_tree2_root(
State(server): State<Server>,
// Extension(extension): Extension<CurrentUser>,
) -> Json<Value> {
Self::get_tree2(State(server), Path("".into())).await
}
pub async fn get_tree2(
State(mut server): State<Server>,
Path(path): Path<String>,
// Extension(_): Extension<CurrentUser>,
) -> Json<Value> {
debug!("GET /api/interface/{}", path);
let result = server
.get(&path, TreeMessage::RequestStructAndValue)
.map_err(|e| ModuleError::CryptError(e.to_string()));
Json(serde_json::to_value(result).unwrap())
}
pub async fn post_tree2(
State(mut server): State<Server>,
Path(path): Path<String>,
// Extension(_): Extension<CurrentUser>,
Json(tree_message): Json<TreeMessage>,
) -> Json<Value> {
debug!("POST /api/interface/");
// Json(Value::Null)
let result = server
.get(&path, tree_message)
.map_err(|e| ModuleError::CryptError(e.to_string()));
Json(serde_json::to_value(result).unwrap())
}
}