diff --git a/Cargo.lock b/Cargo.lock index d29868d..3b7c42f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,13 +43,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.75" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -60,9 +60,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" dependencies = [ "async-trait", "axum-core", @@ -89,13 +89,14 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" dependencies = [ "async-trait", "bytes", @@ -109,6 +110,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -514,9 +516,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -541,6 +543,25 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -553,13 +574,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -613,9 +634,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -623,6 +644,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -749,7 +780,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -776,18 +807,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -875,29 +906,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -906,9 +937,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "serde", @@ -988,9 +1019,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1020,22 +1051,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1065,7 +1096,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1175,12 +1206,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1208,7 +1254,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -1230,7 +1276,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1248,7 +1294,9 @@ dependencies = [ "axum", "env_logger", "futures-core", + "include_dir", "log", + "mime_guess", "orca", "serde", "thiserror", @@ -1290,11 +1338,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -1431,9 +1479,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.31" +version = "0.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" +checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6" dependencies = [ "memchr", ] diff --git a/wavehunter/Cargo.toml b/wavehunter/Cargo.toml index 067e353..1d94e20 100644 --- a/wavehunter/Cargo.toml +++ b/wavehunter/Cargo.toml @@ -16,3 +16,5 @@ thiserror = "1.0.52" log = "0.4.20" env_logger = "0.10.1" tokio-util = { version = "0.7.10", features = ["io"] } +include_dir = "0.7.3" +mime_guess = "2.0.4" diff --git a/wavehunter/src/main.rs b/wavehunter/src/main.rs index 0b74fa8..404d465 100644 --- a/wavehunter/src/main.rs +++ b/wavehunter/src/main.rs @@ -1,24 +1,29 @@ mod config; mod error; +mod pcap; mod server; +mod stats; use crate::config::{parse_config, parse_args}; -use crate::server::{ServerState, serve_pcap, serve_qmdl}; +use crate::server::{ServerState, get_qmdl, serve_static}; +use crate::pcap::get_pcap; +use crate::stats::{get_system_stats, get_diag_stats}; use crate::error::WavehunterError; -use log::debug; +use axum::response::Redirect; use orca::diag_device::DiagDevice; use orca::diag_reader::DiagReader; - use axum::routing::get; use axum::Router; use tokio::fs::File; +use log::debug; use std::net::SocketAddr; use tokio::net::TcpListener; use tokio::sync::RwLock; +use tokio::task::JoinHandle; use std::sync::Arc; -fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc>) -> tokio::task::JoinHandle> { +fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc>) -> JoinHandle> { tokio::task::spawn_blocking(move || { loop { // TODO: once we're actually doing analysis, we'll wanna use the messages @@ -45,8 +50,12 @@ async fn run_server(config: &config::Config, qmdl_bytes_written: Arc>) -> Result { + let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; + if qmdl_bytes_written == 0 { + return Err(( + StatusCode::SERVICE_UNAVAILABLE, + "QMDL file is empty, try again in a bit!".to_string() + )); + } + + let (tx, rx) = mpsc::channel(1); + let channel_reader = ChannelReader { rx }; + let channel_writer = ChannelWriter { tx }; + tokio::task::spawn_blocking(move || { + // the QMDL reader should stop at the last successfully written data + // chunk (qmdl_bytes_written) + let qmdl_file = File::open(&state.qmdl_path).unwrap(); + let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written)); + + let mut gsmtap_parser = GsmtapParser::new(); + let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap(); + pcap_writer.write_iface_header().unwrap(); + loop { + match qmdl_reader.read_response() { + Ok(messages) => { + for maybe_msg in messages { + match maybe_msg { + Ok(msg) => { + let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) + .expect("error parsing gsmtap message"); + if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { + pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) + .expect("error writing pcap packet"); + } + }, + Err(e) => error!("error parsing message: {:?}", e), + } + } + }, + // this is expected, and just means we've reached the end of the + // safely written QMDL data + Err(QmdlReaderError::MaxBytesReached(_)) => break, + Err(e) => { + error!("error reading qmdl file: {:?}", e); + break; + }, + } + } + }); + + let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")]; + let body = Body::from_stream(channel_reader); + Ok((headers, body).into_response()) +} + +struct ChannelWriter { + tx: mpsc::Sender>, +} + +impl Write for ChannelWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.tx.blocking_send(buf.to_vec()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +struct ChannelReader { + rx: mpsc::Receiver>, +} + +impl Stream for ChannelReader { + type Item = Result, String>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.rx.poll_recv(cx) { + Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/wavehunter/src/server.rs b/wavehunter/src/server.rs index f16e4ef..affa28d 100644 --- a/wavehunter/src/server.rs +++ b/wavehunter/src/server.rs @@ -1,117 +1,22 @@ -use orca::gsmtap_parser::GsmtapParser; -use orca::pcap::GsmtapPcapWriter; -use orca::qmdl::{QmdlReader, QmdlReaderError}; -use orca::diag_reader::DiagReader; - use axum::body::Body; -use axum::http::header::CONTENT_TYPE; +use axum::http::header::{CONTENT_TYPE, self}; use axum::extract::State; -use axum::http::StatusCode; +use axum::http::{StatusCode, HeaderValue}; use axum::response::{Response, IntoResponse}; +use axum::extract::Path; use tokio::io::AsyncReadExt; -use std::fs::File; -use std::io::Write; -use std::pin::Pin; use std::sync::Arc; use tokio::sync::RwLock; use tokio::fs::File as AsyncFile; use tokio_util::io::ReaderStream; -use std::task::{Poll, Context}; -use futures_core::Stream; -use log::error; -use tokio::sync::mpsc; +use include_dir::{include_dir, Dir}; -// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data -// written so far. This is done by spawning a blocking thread (a tokio thread -// capable of handling blocking operations) which streams chunks of pcap data to -// a channel that's piped to the client. -pub async fn serve_pcap(State(state): State>) -> Result { - let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; - if qmdl_bytes_written == 0 { - return Err(( - StatusCode::SERVICE_UNAVAILABLE, - "QMDL file is empty, try again in a bit!".to_string() - )); - } - - let (tx, rx) = mpsc::channel(1); - let channel_reader = ChannelReader { rx }; - let channel_writer = ChannelWriter { tx }; - tokio::task::spawn_blocking(move || { - // the QMDL reader should stop at the last successfully written data - // chunk (qmdl_bytes_written) - let qmdl_file = File::open(&state.qmdl_path).unwrap(); - let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written)); - - let mut gsmtap_parser = GsmtapParser::new(); - let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap(); - pcap_writer.write_iface_header().unwrap(); - loop { - match qmdl_reader.read_response() { - Ok(messages) => { - for maybe_msg in messages { - match maybe_msg { - Ok(msg) => { - let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) - .expect("error parsing gsmtap message"); - if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { - pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) - .expect("error writing pcap packet"); - } - }, - Err(e) => error!("error parsing message: {:?}", e), - } - } - }, - // this is expected, and just means we've reached the end of the - // safely written QMDL data - Err(QmdlReaderError::MaxBytesReached(_)) => break, - Err(e) => { - error!("error reading qmdl file: {:?}", e); - break; - }, - } - } - }); - - let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")]; - let body = Body::from_stream(channel_reader); - Ok((headers, body).into_response()) +pub struct ServerState { + pub qmdl_bytes_written: Arc>, + pub qmdl_path: String, } -struct ChannelWriter { - tx: mpsc::Sender>, -} - -impl Write for ChannelWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.tx.blocking_send(buf.to_vec()) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?; - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -struct ChannelReader { - rx: mpsc::Receiver>, -} - -impl Stream for ChannelReader { - type Item = Result, String>; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.rx.poll_recv(cx) { - Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -pub async fn serve_qmdl(State(state): State>) -> Result { +pub async fn get_qmdl(State(state): State>) -> Result { let qmdl_file = AsyncFile::open(&state.qmdl_path).await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?; let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; @@ -123,7 +28,25 @@ pub async fn serve_qmdl(State(state): State>) -> Result>, - pub qmdl_path: String, +// Bundles the server's static files (html/css/js) into the binary for easy distribution +static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static"); + +pub async fn serve_static(Path(path): Path) -> impl IntoResponse { + let path = path.trim_start_matches('/'); + let mime_type = mime_guess::from_path(path).first_or_text_plain(); + + match STATIC_DIR.get_file(path) { + None => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap(), + Some(file) => Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + HeaderValue::from_str(mime_type.as_ref()).unwrap(), + ) + .body(Body::from(file.contents())) + .unwrap(), + } } diff --git a/wavehunter/src/stats.rs b/wavehunter/src/stats.rs new file mode 100644 index 0000000..9ccc0fd --- /dev/null +++ b/wavehunter/src/stats.rs @@ -0,0 +1,121 @@ +use crate::server::ServerState; + +use axum::Json; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use std::sync::Arc; +use log::error; +use serde::Serialize; +use tokio::process::Command; + +#[derive(Debug, Serialize)] +pub struct SystemStats { + pub disk_stats: DiskStats, + pub memory_stats: MemoryStats, +} + +impl SystemStats { + pub async fn new(qmdl_path: &str) -> Result { + Ok(Self { + disk_stats: DiskStats::new(qmdl_path).await?, + memory_stats: MemoryStats::new().await?, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct DiskStats { + partition: String, + total_size: String, + used_size: String, + available_size: String, + used_percent: String, + mounted_on: String, +} + +impl DiskStats { + // runs "df -h " to get storage statistics for the partition containing + // the QMDL file + pub async fn new(qmdl_path: &str) -> Result { + let mut df_cmd = Command::new("df"); + df_cmd.arg("-h"); + df_cmd.arg(qmdl_path); + let stdout = get_cmd_output(df_cmd).await?; + let mut parts = stdout.split_whitespace().skip(7).to_owned(); + Ok(Self { + partition: parts.next().ok_or("error parsing df output")?.to_string(), + total_size: parts.next().ok_or("error parsing df output")?.to_string(), + used_size: parts.next().ok_or("error parsing df output")?.to_string(), + available_size: parts.next().ok_or("error parsing df output")?.to_string(), + used_percent: parts.next().ok_or("error parsing df output")?.to_string(), + mounted_on: parts.next().ok_or("error parsing df output")?.to_string(), + }) + } +} + +#[derive(Debug, Serialize)] +pub struct MemoryStats { + total: String, + used: String, + free: String, +} + +// runs the given command and returns its stdout as a string +async fn get_cmd_output(mut cmd: Command) -> Result { + let cmd_str = format!("{:?}", &cmd); + let output = cmd.output().await + .map_err(|e| format!("error running command {}: {}", &cmd_str, e))?; + if !output.status.success() { + return Err(format!("command {} failed with exit code {}", &cmd_str, output.status.code().unwrap())); + } + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +impl MemoryStats { + // runs "free -k" and parses the output to retrieve memory stats + pub async fn new() -> Result { + let mut free_cmd = Command::new("free"); + free_cmd.arg("-k"); + let stdout = get_cmd_output(free_cmd).await?; + let mut numbers = stdout.split_whitespace() + .flat_map(|part| part.parse::()); + Ok(Self { + total: humanize_kb(numbers.next().ok_or("error parsing free output")?), + used: humanize_kb(numbers.next().ok_or("error parsing free output")?), + free: humanize_kb(numbers.next().ok_or("error parsing free output")?), + }) + } +} + +// turns a number of kilobytes (like 28293) into a human-readable string (like "28.3M") +fn humanize_kb(kb: usize) -> String { + if kb < 1000{ + return format!("{}K", kb); + } + format!("{:.1}M", kb as f64 / 1024.0) +} + +pub async fn get_system_stats(State(state): State>) -> Result, (StatusCode, String)> { + match SystemStats::new(&state.qmdl_path).await { + Ok(stats) => Ok(Json(stats)), + Err(err) => { + error!("error getting system stats: {}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "error getting system stats".to_string() + )); + }, + } +} + +#[derive(Debug, Serialize)] +pub struct DiagStats { + bytes_written: usize, +} + +pub async fn get_diag_stats(State(state): State>) -> impl IntoResponse { + Json(DiagStats { + bytes_written: *state.qmdl_bytes_written.read().await, + }) +} diff --git a/wavehunter/static/css/style.css b/wavehunter/static/css/style.css new file mode 100644 index 0000000..e69de29 diff --git a/wavehunter/static/index.html b/wavehunter/static/index.html new file mode 100644 index 0000000..e6827c8 --- /dev/null +++ b/wavehunter/static/index.html @@ -0,0 +1,20 @@ + + + ORCA + + + + + + +
Loading...
+
Loading...
+ + diff --git a/wavehunter/static/js/main.js b/wavehunter/static/js/main.js new file mode 100644 index 0000000..de2f9ee --- /dev/null +++ b/wavehunter/static/js/main.js @@ -0,0 +1,24 @@ +async function populateDivs() { + const systemStats = await getSystemStats(); + const diagStats = await getDiagStats(); + + const systemStatsDiv = document.getElementById('system-stats'); + const diagStatsDiv = document.getElementById('diag-stats'); + + systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2); + diagStatsDiv.innerHTML = JSON.stringify(diagStats, null, 2); +} + +async function getSystemStats() { + return await getJson('/api/system-stats'); +} + +async function getDiagStats() { + return await getJson('/api/diag-stats'); +} + +async function getJson(url) { + const response = await fetch(url); + const data = await response.json(); + return data; +}