Rename directories (again)

This commit is contained in:
Will Greenberg
2024-01-30 09:58:05 -08:00
parent c3d9fb742e
commit 87f18c0c8b
24 changed files with 0 additions and 0 deletions

54
bin/src/config.rs Normal file
View File

@@ -0,0 +1,54 @@
use crate::error::RayhunterError;
use serde::Deserialize;
#[derive(Deserialize)]
struct ConfigFile {
qmdl_store_path: Option<String>,
port: Option<u16>,
readonly_mode: Option<bool>,
}
#[derive(Debug)]
pub struct Config {
pub qmdl_store_path: String,
pub port: u16,
pub readonly_mode: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
port: 8080,
readonly_mode: false,
}
}
}
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef<std::path::Path> {
let mut config = Config::default();
if let Ok(config_file) = std::fs::read_to_string(&path) {
let parsed_config: ConfigFile = toml::from_str(&config_file)
.map_err(RayhunterError::ConfigFileParsingError)?;
if let Some(path) = parsed_config.qmdl_store_path { config.qmdl_store_path = path }
if let Some(port) = parsed_config.port { config.port = port }
if let Some(readonly_mode) = parsed_config.readonly_mode { config.readonly_mode = readonly_mode }
}
Ok(config)
}
pub struct Args {
pub config_path: String,
}
pub fn parse_args() -> Args {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
println!("Usage: {} /path/to/config/file", args[0]);
std::process::exit(1);
}
Args {
config_path: args[1].clone(),
}
}

110
bin/src/diag.rs Normal file
View File

@@ -0,0 +1,110 @@
use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use orca::diag_device::DiagDevice;
use orca::diag_reader::DiagReader;
use tokio::sync::RwLock;
use tokio::sync::mpsc::{Receiver, self};
use orca::qmdl::QmdlWriter;
use log::{debug, info};
use tokio::sync::mpsc::error::TryRecvError;
use tokio::task::JoinHandle;
use tokio_util::task::TaskTracker;
use crate::error::RayhunterError;
use crate::qmdl_store::QmdlStore;
use crate::server::ServerState;
pub enum DiagDeviceCtrlMessage {
StopRecording,
StartRecording(QmdlWriter<std::fs::File>),
Exit,
}
pub fn run_diag_read_thread(task_tracker: &TaskTracker, mut dev: DiagDevice, mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>, qmdl_store_lock: Arc<RwLock<QmdlStore>>) -> JoinHandle<Result<(), RayhunterError>> {
// mpsc channel for updating QmdlStore entry filesizes. First usize is the
// index, second is the size in bytes
let (tx, mut rx) = mpsc::channel::<(usize, usize)>(1);
// Spawn a thread to monitor the (usize, usize) channel for updates,
// triggering QmdlStore updates
let qmdl_store_lock_clone = qmdl_store_lock.clone();
task_tracker.spawn(async move {
while let Some((entry_idx, new_size)) = rx.recv().await {
let mut qmdl_store = qmdl_store_lock_clone.write().await;
qmdl_store.update_entry_size(entry_idx, new_size).await
.expect("failed to update qmdl file size");
}
info!("QMDL store size updater thread exiting...");
});
// Spawn a thread to drive the DiagDevice reading loop. Since DiagDevice
// works via synchronous I/O, we have to spawn a "blocking" thread to avoid
// gumming up tokio's event loop.
task_tracker.spawn_blocking(move || {
loop {
// First check if we've gotten any control meesages
match qmdl_file_rx.try_recv() {
Ok(DiagDeviceCtrlMessage::StartRecording(qmdl_writer)) => {
dev.qmdl_writer = Some(qmdl_writer);
},
Ok(DiagDeviceCtrlMessage::StopRecording) => dev.qmdl_writer = None,
// Disconnected means all the Senders have been dropped, so it's
// time to go
Ok(DiagDeviceCtrlMessage::Exit) | Err(TryRecvError::Disconnected) => {
info!("Diag reader thread exiting...");
return Ok(())
},
// empty just means there's no message for us, so continue as normal
Err(TryRecvError::Empty) => {},
}
// remember the QmdlStore current entry index so we can update its size later
let qmdl_store_index = qmdl_store_lock.blocking_read().current_entry;
// TODO: once we're actually doing analysis, we'll wanna use the messages
// returned here. Until then, the DiagDevice has already written those messages
// to the QMDL file, so we can just ignore them.
debug!("reading response from diag device...");
let _messages = dev.read_response().map_err(RayhunterError::DiagReadError)?;
debug!("got diag response ({} messages)", _messages.len());
// keep track of how many bytes were written to the QMDL file so we can read
// a valid block of data from it in the HTTP server
if let Some(qmdl_writer) = dev.qmdl_writer.as_ref() {
debug!("total QMDL bytes written: {}, sending update...", qmdl_writer.total_written);
let index = qmdl_store_index.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
tx.blocking_send((index, qmdl_writer.total_written)).unwrap();
debug!("done!");
} else {
debug!("no qmdl_writer set, continuing...");
}
}
})
}
pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.readonly_mode {
return Err((StatusCode::FORBIDDEN, format!("server is in readonly mode")));
}
let mut qmdl_store = state.qmdl_store_lock.write().await;
let qmdl_file = qmdl_store.new_entry().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't create new qmdl entry: {}", e)))?;
let qmdl_writer = QmdlWriter::new(qmdl_file.into_std().await);
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording(qmdl_writer)).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
Ok((StatusCode::ACCEPTED, format!("ok")))
}
pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.readonly_mode {
return Err((StatusCode::FORBIDDEN, format!("server is in readonly mode")));
}
let mut qmdl_store = state.qmdl_store_lock.write().await;
qmdl_store.close_current_entry().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?;
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
Ok((StatusCode::ACCEPTED, format!("ok")))
}

20
bin/src/error.rs Normal file
View File

@@ -0,0 +1,20 @@
use thiserror::Error;
use orca::diag_device::DiagDeviceError;
use crate::qmdl_store::QmdlStoreError;
#[derive(Error, Debug)]
pub enum RayhunterError{
#[error("Config file parsing error: {0}")]
ConfigFileParsingError(#[from] toml::de::Error),
#[error("Diag intialization error: {0}")]
DiagInitError(DiagDeviceError),
#[error("Diag read error: {0}")]
DiagReadError(DiagDeviceError),
#[error("Tokio error: {0}")]
TokioError(#[from] tokio::io::Error),
#[error("QmdlStore error: {0}")]
QmdlStoreError(#[from] QmdlStoreError),
#[error("No QMDL store found at path {0}, but can't create a new one due to readonly mode")]
NoStoreReadonlyMode(String),
}

148
bin/src/main.rs Normal file
View File

@@ -0,0 +1,148 @@
mod config;
mod error;
mod pcap;
mod server;
mod stats;
mod qmdl_store;
mod diag;
use crate::config::{parse_config, parse_args};
use crate::diag::run_diag_read_thread;
use crate::qmdl_store::QmdlStore;
use crate::server::{ServerState, get_qmdl, serve_static};
use crate::pcap::get_pcap;
use crate::stats::get_system_stats;
use crate::error::RayhunterError;
use axum::response::Redirect;
use diag::{DiagDeviceCtrlMessage, start_recording, stop_recording};
use log::{info, error};
use orca::diag_device::DiagDevice;
use axum::routing::{get, post};
use axum::Router;
use orca::qmdl::QmdlWriter;
use stats::get_qmdl_manifest;
use tokio::sync::mpsc::{self, Sender};
use tokio::task::JoinHandle;
use tokio_util::task::TaskTracker;
use std::net::SocketAddr;
use tokio::net::TcpListener;
use tokio::sync::{RwLock, oneshot};
use std::sync::Arc;
// Runs the axum server, taking all the elements needed to build up our
// ServerState and a oneshot Receiver that'll fire when it's time to shutdown
// (i.e. user hit ctrl+c)
async fn run_server(
task_tracker: &TaskTracker,
config: &config::Config,
qmdl_store_lock: Arc<RwLock<QmdlStore>>,
server_shutdown_rx: oneshot::Receiver<()>,
diag_device_sender: Sender<DiagDeviceCtrlMessage>
) -> JoinHandle<()> {
let state = Arc::new(ServerState {
qmdl_store_lock,
diag_device_ctrl_sender: diag_device_sender,
readonly_mode: config.readonly_mode,
});
let app = Router::new()
.route("/api/pcap/*name", get(get_pcap))
.route("/api/qmdl/*name", get(get_qmdl))
.route("/api/system-stats", get(get_system_stats))
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
.route("/api/start-recording", post(start_recording))
.route("/api/stop-recording", post(stop_recording))
.route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/*path", get(serve_static))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
let listener = TcpListener::bind(&addr).await.unwrap();
task_tracker.spawn(async move {
info!("The orca is hunting for stingrays...");
axum::serve(listener, app)
.with_graceful_shutdown(server_shutdown_signal(server_shutdown_rx))
.await.unwrap();
})
}
async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
server_shutdown_rx.await.unwrap();
info!("Server received shutdown signal, exiting...");
}
// Loads a QmdlStore if one exists, and if not, only create one if we're not in
// readonly mode.
async fn init_qmdl_store(config: &config::Config) -> Result<QmdlStore, RayhunterError> {
match (QmdlStore::exists(&config.qmdl_store_path).await?, config.readonly_mode) {
(true, _) => Ok(QmdlStore::load(&config.qmdl_store_path).await?),
(false, false) => Ok(QmdlStore::create(&config.qmdl_store_path).await?),
(false, true) => Err(RayhunterError::NoStoreReadonlyMode(config.qmdl_store_path.clone())),
}
}
// Start a thread that'll track when user hits ctrl+c. When that happens,
// trigger various cleanup tasks, including sending signals to other threads to
// shutdown
fn run_ctrl_c_thread(
task_tracker: &TaskTracker,
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
server_shutdown_tx: oneshot::Sender<()>,
qmdl_store_lock: Arc<RwLock<QmdlStore>>
) -> JoinHandle<Result<(), RayhunterError>> {
task_tracker.spawn(async move {
match tokio::signal::ctrl_c().await {
Ok(()) => {
let mut qmdl_store = qmdl_store_lock.write().await;
if qmdl_store.current_entry.is_some() {
info!("Closing current QMDL entry...");
qmdl_store.close_current_entry().await?;
info!("Done!");
}
server_shutdown_tx.send(())
.expect("couldn't send server shutdown signal");
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
.expect("couldn't send Exit message to diag thread");
},
Err(err) => {
error!("Unable to listen for shutdown signal: {}", err);
}
}
Ok(())
})
}
#[tokio::main]
async fn main() -> Result<(), RayhunterError> {
env_logger::init();
let args = parse_args();
let config = parse_config(&args.config_path)?;
// TaskTrackers give us an interface to spawn tokio threads, and then
// eventually await all of them ending
let task_tracker = TaskTracker::new();
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
if !config.readonly_mode {
let qmdl_file = qmdl_store_lock.write().await.new_entry().await?;
let qmdl_writer = QmdlWriter::new(qmdl_file.into_std().await);
let mut dev = DiagDevice::new(Some(qmdl_writer))
.map_err(RayhunterError::DiagInitError)?;
dev.config_logs()
.map_err(RayhunterError::DiagInitError)?;
run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone());
}
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, qmdl_store_lock.clone());
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await;
task_tracker.close();
task_tracker.wait().await;
Ok(())
}

112
bin/src/pcap.rs Normal file
View File

@@ -0,0 +1,112 @@
use crate::ServerState;
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::extract::{State, Path};
use axum::http::StatusCode;
use axum::response::{Response, IntoResponse};
use std::io::Write;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Poll, Context};
use futures_core::Stream;
use log::error;
use tokio::sync::mpsc;
// 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 get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
let entry = qmdl_store.entry_for_name(&qmdl_name)
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
if entry.size_bytes == 0 {
return Err((
StatusCode::SERVICE_UNAVAILABLE,
"QMDL file is empty, try again in a bit!".to_string()
));
}
let qmdl_file = qmdl_store.open_entry(&entry).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?
.into_std().await;
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 mut qmdl_reader = QmdlReader::new(qmdl_file, Some(entry.size_bytes));
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<Vec<u8>>,
}
impl Write for ChannelWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<Vec<u8>>,
}
impl Stream for ChannelReader {
type Item = Result<Vec<u8>, String>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
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,
}
}
}

213
bin/src/qmdl_store.rs Normal file
View File

@@ -0,0 +1,213 @@
use std::path::{PathBuf, Path};
use thiserror::Error;
use tokio::{fs::{self, File, try_exists}, io::AsyncWriteExt};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Local};
#[derive(Debug, Error)]
pub enum QmdlStoreError {
#[error("Can't close an entry when there's no current entry")]
NoCurrentEntry,
#[error("Couldn't create file: {0}")]
CreateFileError(tokio::io::Error),
#[error("Couldn't read file: {0}")]
ReadFileError(tokio::io::Error),
#[error("Couldn't open directory at path: {0}")]
OpenDirError(tokio::io::Error),
#[error("Couldn't read manifest file: {0}")]
ReadManifestError(tokio::io::Error),
#[error("Couldn't write manifest file: {0}")]
WriteManifestError(tokio::io::Error),
#[error("Couldn't parse QMDL store manifest file: {0}")]
ParseManifestError(toml::de::Error)
}
pub struct QmdlStore {
pub path: PathBuf,
pub manifest: Manifest,
pub current_entry: Option<usize>, // index into manifest
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)]
pub struct Manifest {
pub entries: Vec<ManifestEntry>,
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)]
pub struct ManifestEntry {
pub name: String,
pub start_time: DateTime<Local>,
pub end_time: Option<DateTime<Local>>,
pub size_bytes: usize,
}
impl ManifestEntry {
fn new() -> Self {
let now = Local::now();
ManifestEntry {
name: format!("{}", now.timestamp()),
start_time: now,
end_time: None,
size_bytes: 0,
}
}
}
impl QmdlStore {
// Returns whether a directory with a "manifest.toml" exists at the given
// path (though doesn't check if that manifest is valid)
pub async fn exists<P>(path: P) -> Result<bool, QmdlStoreError> where P: AsRef<Path> {
let manifest_path = path.as_ref().join("manifest.toml");
let dir_exists = try_exists(path).await.map_err(QmdlStoreError::OpenDirError)?;
let manifest_exists = try_exists(manifest_path).await.map_err(QmdlStoreError::ReadManifestError)?;
Ok(dir_exists && manifest_exists)
}
// Loads an existing QmdlStore at the given path. Errors if no store exists,
// or if it's malformed.
pub async fn load<P>(path: P) -> Result<Self, QmdlStoreError> where P: AsRef<Path> {
let path: PathBuf = path.as_ref().to_path_buf();
let manifest = QmdlStore::read_manifest(&path).await?;
Ok(QmdlStore {
path,
manifest,
current_entry: None,
})
}
// Creates a new QmdlStore at the given path. This involves creating a dir
// and writing an empty manifest.
pub async fn create<P>(path: P) -> Result<Self, QmdlStoreError> where P: AsRef<Path> {
let manifest_path = path.as_ref().join("manifest.toml");
fs::create_dir_all(&path).await
.map_err(QmdlStoreError::OpenDirError)?;
let mut manifest_file = File::create(&manifest_path).await
.map_err(QmdlStoreError::WriteManifestError)?;
let empty_manifest = Manifest { entries: Vec::new() };
let empty_manifest_contents = toml::to_string_pretty(&empty_manifest)
.expect("failed to serialize manifest");
manifest_file.write_all(empty_manifest_contents.as_bytes()).await
.map_err(QmdlStoreError::WriteManifestError)?;
QmdlStore::load(path).await
}
async fn read_manifest<P>(path: P) -> Result<Manifest, QmdlStoreError> where P: AsRef<Path> {
let manifest_path = path.as_ref().join("manifest.toml");
let file_contents = fs::read_to_string(&manifest_path).await
.map_err(QmdlStoreError::ReadManifestError)?;
toml::from_str(&file_contents)
.map_err(QmdlStoreError::ParseManifestError)
}
// Closes the current entry (if needed), creates a new entry based on the
// current time, and updates the manifest
pub async fn new_entry(&mut self) -> Result<File, QmdlStoreError> {
// if we've already got an entry open, close it
if self.current_entry.is_some() {
self.close_current_entry().await?;
}
let new_entry = ManifestEntry::new();
let mut file_path = self.path.join(&new_entry.name);
file_path.set_extension("qmdl");
let file = File::options()
.create(true)
.write(true)
.open(&file_path).await
.map_err(QmdlStoreError::CreateFileError)?;
self.manifest.entries.push(new_entry);
self.current_entry = Some(self.manifest.entries.len() - 1);
self.write_manifest().await?;
Ok(file)
}
// Returns the corresponding QMDL file for a given entry
pub async fn open_entry(&self, entry: &ManifestEntry) -> Result<File, QmdlStoreError> {
let mut file_path = self.path.join(&entry.name);
file_path.set_extension("qmdl");
File::open(file_path).await
.map_err(QmdlStoreError::ReadFileError)
}
// Sets the current entry's end_time, updates the manifest, and unsets the
// current entry
pub async fn close_current_entry(&mut self) -> Result<(), QmdlStoreError> {
let entry_index = self.current_entry.take()
.ok_or(QmdlStoreError::NoCurrentEntry)?;
self.manifest.entries[entry_index].end_time = Some(Local::now());
self.write_manifest().await
}
// Sets the given entry's size, updating the manifest
pub async fn update_entry_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), QmdlStoreError> {
self.manifest.entries[entry_index].size_bytes = size_bytes;
self.write_manifest().await
}
async fn write_manifest(&mut self) -> Result<(), QmdlStoreError> {
let mut manifest_file = File::options()
.write(true)
.open(self.path.join("manifest.toml")).await
.map_err(QmdlStoreError::WriteManifestError)?;
let manifest_contents = toml::to_string_pretty(&self.manifest)
.expect("failed to serialize manifest");
manifest_file.write_all(manifest_contents.as_bytes()).await
.map_err(QmdlStoreError::WriteManifestError)?;
Ok(())
}
// Finds an entry by filename
pub fn entry_for_name(&self, name: &str) -> Option<ManifestEntry> {
self.manifest.entries.iter()
.find(|entry| entry.name == name)
.cloned()
}
}
#[cfg(test)]
mod tests {
use tempdir::TempDir;
use super::*;
#[tokio::test]
async fn test_load_from_empty_dir() {
let dir = TempDir::new("qmdl_store_test").unwrap();
assert!(!QmdlStore::exists(dir.path()).await.unwrap());
let _created_store = QmdlStore::create(dir.path()).await.unwrap();
assert!(QmdlStore::exists(dir.path()).await.unwrap());
let loaded_store = QmdlStore::load(dir.path()).await.unwrap();
assert_eq!(loaded_store.manifest.entries.len(), 0);
}
#[tokio::test]
async fn test_creating_updating_and_closing_entries() {
let dir = TempDir::new("qmdl_store_test").unwrap();
let mut store = QmdlStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let entry_index = store.current_entry.unwrap();
assert_eq!(QmdlStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
store.update_entry_size(entry_index, 1000).await.unwrap();
assert_eq!(store.manifest.entries[entry_index].size_bytes, 1000);
assert_eq!(QmdlStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
assert!(store.manifest.entries[entry_index].end_time.is_none());
store.close_current_entry().await.unwrap();
let entry = store.entry_for_name(&store.manifest.entries[entry_index].name).unwrap();
assert!(entry.end_time.is_some());
assert_eq!(QmdlStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
assert!(matches!(store.close_current_entry().await, Err(QmdlStoreError::NoCurrentEntry)));
}
#[tokio::test]
async fn test_repeated_new_entries() {
let dir = TempDir::new("qmdl_store_test").unwrap();
let mut store = QmdlStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let entry_index = store.current_entry.unwrap();
let _ = store.new_entry().await.unwrap();
let new_entry_index = store.current_entry.unwrap();
assert_ne!(entry_index, new_entry_index);
assert_eq!(store.manifest.entries.len(), 2);
}
}

58
bin/src/server.rs Normal file
View File

@@ -0,0 +1,58 @@
use axum::body::Body;
use axum::http::header::{CONTENT_TYPE, self};
use axum::extract::State;
use axum::http::{StatusCode, HeaderValue};
use axum::response::{Response, IntoResponse};
use axum::extract::Path;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_util::io::ReaderStream;
use include_dir::{include_dir, Dir};
use crate::DiagDeviceCtrlMessage;
use crate::qmdl_store::QmdlStore;
pub struct ServerState {
pub qmdl_store_lock: Arc<RwLock<QmdlStore>>,
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
pub readonly_mode: bool,
}
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
let entry = qmdl_store.entry_for_name(&qmdl_name)
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
let qmdl_file = qmdl_store.open_entry(&entry).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
let limited_qmdl_file = qmdl_file.take(entry.size_bytes as u64);
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
let headers = [(CONTENT_TYPE, "application/octet-stream")];
let body = Body::from_stream(qmdl_stream);
Ok((headers, body).into_response())
}
// 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<String>) -> 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(),
}
}

128
bin/src/stats.rs Normal file
View File

@@ -0,0 +1,128 @@
use std::sync::Arc;
use crate::qmdl_store::ManifestEntry;
use crate::server::ServerState;
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
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<Self, String> {
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 <qmdl_path>" to get storage statistics for the partition containing
// the QMDL file
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
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<String, String> {
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<Self, String> {
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::<usize>());
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<Arc<ServerState>>) -> Result<Json<SystemStats>, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
match SystemStats::new(qmdl_store.path.to_str().unwrap()).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(Serialize)]
pub struct ManifestStats {
pub entries: Vec<ManifestEntry>,
pub current_entry: Option<ManifestEntry>,
}
pub async fn get_qmdl_manifest(State(state): State<Arc<ServerState>>) -> Result<Json<ManifestStats>, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
let mut entries = qmdl_store.manifest.entries.clone();
let current_entry = qmdl_store.current_entry.map(|index| entries.remove(index));
Ok(Json(ManifestStats {
entries,
current_entry,
}))
}