address most of wills review feedback, fix serialization and stringly error handling in DiagTask::start

This commit is contained in:
Markus Unterwaditzer
2026-05-15 21:13:46 +02:00
committed by Will Greenberg
parent a58bad09fc
commit bd5dfb1a75
5 changed files with 48 additions and 60 deletions
+31 -43
View File
@@ -40,7 +40,7 @@ const DISK_CHECK_BYTES_INTERVAL: usize = 256 * 1024;
pub enum DiagDeviceCtrlMessage { pub enum DiagDeviceCtrlMessage {
StopRecording, StopRecording,
StartRecording { StartRecording {
response_tx: Option<oneshot::Sender<Result<(), String>>>, response_tx: Option<oneshot::Sender<Result<(), RecordingStoreError>>>,
}, },
DeleteEntry { DeleteEntry {
name: String, name: String,
@@ -129,7 +129,7 @@ impl DiagTask {
} }
/// Start recording, returning an error if disk space is too low. /// Start recording, returning an error if disk space is too low.
async fn start(&mut self, qmdl_store: &mut RecordingStore) -> Result<(), String> { async fn start(&mut self, qmdl_store: &mut RecordingStore) -> Result<(), RecordingStoreError> {
self.max_type_seen = EventType::Informational; self.max_type_seen = EventType::Informational;
self.bytes_since_space_check = 0; self.bytes_since_space_check = 0;
self.low_space_warned = false; self.low_space_warned = false;
@@ -140,12 +140,10 @@ impl DiagTask {
self.min_space_to_continue_mb, self.min_space_to_continue_mb,
) { ) {
DiskSpaceCheck::Critical(mb) | DiskSpaceCheck::Warning(mb) => { DiskSpaceCheck::Critical(mb) | DiskSpaceCheck::Warning(mb) => {
let msg = format!( return Err(RecordingStoreError::InsufficientDiskSpace(
"Insufficient disk space: {}MB available, {}MB required", mb,
mb, self.min_space_to_start_mb self.min_space_to_start_mb,
); ));
error!("{msg}");
return Err(msg);
} }
DiskSpaceCheck::Ok(mb) => { DiskSpaceCheck::Ok(mb) => {
info!("Starting recording with {}MB disk space available", mb); info!("Starting recording with {}MB disk space available", mb);
@@ -153,14 +151,8 @@ impl DiagTask {
DiskSpaceCheck::Failed => {} DiskSpaceCheck::Failed => {}
} }
let (qmdl_file, analysis_file) = match qmdl_store.new_entry(self.gps_mode).await { let (qmdl_file, analysis_file) = qmdl_store.new_entry(self.gps_mode).await?;
Ok(files) => files,
Err(e) => {
let msg = format!("failed creating QMDL file entry: {e}");
error!("{msg}");
return Err(msg);
}
};
// For fixed-mode sessions, write the configured coordinates to the sidecar // For fixed-mode sessions, write the configured coordinates to the sidecar
// immediately so the per-session GPS is stored durably and isn't affected // immediately so the per-session GPS is stored durably and isn't affected
// by future config changes or GPS API calls. // by future config changes or GPS API calls.
@@ -168,38 +160,34 @@ impl DiagTask {
&& let Some((lat, lon)) = self.gps_fixed_coords && let Some((lat, lon)) = self.gps_fixed_coords
&& let Some((entry_idx, _)) = qmdl_store.get_current_entry() && let Some((entry_idx, _)) = qmdl_store.get_current_entry()
{ {
match qmdl_store.open_entry_gps_for_append(entry_idx).await { let mut gps_file = qmdl_store
Ok(Some(mut gps_file)) => { .open_entry_gps_for_append(entry_idx)
let record = GpsRecord { .await?
unix_ts: 0, .ok_or(RecordingStoreError::GpsSidecarNotFound)?;
lat,
lon, let record = GpsRecord {
}; unix_ts: chrono::Utc::now().timestamp(),
if let Ok(json) = serde_json::to_string(&record) { lat,
let _ = gps_file.write_all(format!("{json}\n").as_bytes()).await; lon,
} };
} let json = serde_json::to_string(&record)?;
Ok(None) => { gps_file
error!("GPS sidecar directory not found, cannot write fixed-mode coordinates") .write_all(format!("{json}\n").as_bytes())
} .await
Err(e) => error!("failed to open GPS sidecar for fixed-mode entry: {e}"), .map_err(RecordingStoreError::WriteFileError)?;
}
} }
self.stop_current_recording().await; self.stop_current_recording().await;
let qmdl_writer = QmdlWriter::new(qmdl_file); let qmdl_writer = QmdlWriter::new(qmdl_file);
let analysis_writer = match AnalysisWriter::new(analysis_file, &self.analyzer_config).await let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
{ .await
Ok(writer) => Box::new(writer), .map_err(RecordingStoreError::WriteFileError)?;
Err(e) => {
let msg = format!("failed to create analysis writer: {e}");
error!("{msg}");
return Err(msg);
}
};
self.state = DiagState::Recording { self.state = DiagState::Recording {
qmdl_writer, qmdl_writer,
analysis_writer, analysis_writer: Box::new(analysis_writer),
}; };
if let Err(e) = self if let Err(e) = self
.ui_update_sender .ui_update_sender
.send(display::DisplayState::Recording) .send(display::DisplayState::Recording)
@@ -530,7 +518,7 @@ pub async fn start_recording(
match response_rx.await { match response_rx.await {
Ok(Ok(())) => Ok((StatusCode::ACCEPTED, "ok".to_string())), Ok(Ok(())) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
Ok(Err(reason)) => Err((StatusCode::INSUFFICIENT_STORAGE, reason)), Ok(Err(reason)) => Err((StatusCode::INSUFFICIENT_STORAGE, reason.to_string())),
Err(e) => Err(( Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to receive start recording response: {e}"), format!("failed to receive start recording response: {e}"),
+1 -3
View File
@@ -306,14 +306,12 @@ async fn run_with_config(
config.webdav.clone().into(), config.webdav.clone().into(),
); );
} }
// For fixed configuration, we use timestamp 0 to not break other
// the GET request for GPS but user won't see the 0 in PCAPs
let initial_gps = if config.gps_mode == GpsMode::Fixed { let initial_gps = if config.gps_mode == GpsMode::Fixed {
match (config.gps_fixed_latitude, config.gps_fixed_longitude) { match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
(Some(lat), Some(lon)) => Some(gps::GpsData { (Some(lat), Some(lon)) => Some(gps::GpsData {
latitude: lat, latitude: lat,
longitude: lon, longitude: lon,
timestamp: 0, timestamp: chrono::Utc::now().timestamp(),
}), }),
_ => { _ => {
warn!( warn!(
+8
View File
@@ -23,6 +23,8 @@ pub enum RecordingStoreError {
CreateFileError(tokio::io::Error), CreateFileError(tokio::io::Error),
#[error("Couldn't read file: {0}")] #[error("Couldn't read file: {0}")]
ReadFileError(tokio::io::Error), ReadFileError(tokio::io::Error),
#[error("Couldn't write file: {0}")]
WriteFileError(tokio::io::Error),
#[error("Couldn't delete file: {0}")] #[error("Couldn't delete file: {0}")]
DeleteFileError(tokio::io::Error), DeleteFileError(tokio::io::Error),
#[error("Couldn't open directory at path: {0}")] #[error("Couldn't open directory at path: {0}")]
@@ -33,6 +35,12 @@ pub enum RecordingStoreError {
WriteManifestError(tokio::io::Error), WriteManifestError(tokio::io::Error),
#[error("Couldn't parse QMDL store manifest file: {0}")] #[error("Couldn't parse QMDL store manifest file: {0}")]
ParseManifestError(toml::de::Error), ParseManifestError(toml::de::Error),
#[error("Insufficient disk space: {0}MB available, {1}MB required")]
InsufficientDiskSpace(u64, u64),
#[error("GPS sidecar directory not found")]
GpsSidecarNotFound,
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
} }
pub struct RecordingStore { pub struct RecordingStore {
+1
View File
@@ -245,6 +245,7 @@ pub fn run_webdav_upload_worker(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::config::GpsMode;
use axum::{ use axum::{
Router, Router,
body::Bytes, body::Bytes,
+7 -14
View File
@@ -10,6 +10,7 @@ use pcap_file_tokio::pcapng::blocks::enhanced_packet::{EnhancedPacketBlock, Enha
use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock; use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock;
use pcap_file_tokio::pcapng::blocks::section_header::{SectionHeaderBlock, SectionHeaderOption}; use pcap_file_tokio::pcapng::blocks::section_header::{SectionHeaderBlock, SectionHeaderOption};
use pcap_file_tokio::{Endianness, PcapError}; use pcap_file_tokio::{Endianness, PcapError};
use serde::Serialize;
use std::borrow::Cow; use std::borrow::Cow;
use thiserror::Error; use thiserror::Error;
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
@@ -26,11 +27,13 @@ pub enum GsmtapPcapError {
Deku(#[from] DekuError), Deku(#[from] DekuError),
} }
#[derive(Serialize)]
pub struct GpsPoint { pub struct GpsPoint {
pub latitude: f64,
pub longitude: f64,
/// Unix timestamp of the GPS fix. 0 means fixed/synthetic (no real GPS time).
pub unix_ts: i64, pub unix_ts: i64,
#[serde(rename = "lat")]
pub latitude: f64,
#[serde(rename = "lon")]
pub longitude: f64,
} }
pub struct GsmtapPcapWriter<T> pub struct GsmtapPcapWriter<T>
@@ -148,17 +151,7 @@ where
let mut options = vec![]; let mut options = vec![];
if let Some(p) = gps { if let Some(p) = gps {
let comment = if p.unix_ts == 0 { let comment = serde_json::to_string(p).expect("GpsPoint serialization cannot fail");
format!(
r#"{{"latitude":{:.7},"longitude":{:.7}}}"#,
p.latitude, p.longitude
)
} else {
format!(
r#"{{"latitude":{:.7},"longitude":{:.7},"timestamp":{}}}"#,
p.latitude, p.longitude, p.unix_ts
)
};
options.push(EnhancedPacketOption::Comment(Cow::Owned(comment))); options.push(EnhancedPacketOption::Comment(Cow::Owned(comment)));
} }
let packet = EnhancedPacketBlock { let packet = EnhancedPacketBlock {