From 188bf812b454273b7448ba4e4f79188aac19a58a Mon Sep 17 00:00:00 2001 From: Jack Lund Date: Thu, 9 Apr 2026 19:02:32 -0500 Subject: [PATCH] Add notification timeout Adds a default timeout of 10 seconds for sending notifications so they don't hang indefinitely. This can happen if the server connected to is not responding or the case where there's a SIM card in the device, but it's unactivated so that DNS works but the connection doesn't --- daemon/src/notifications.rs | 70 +++++++++++++++++++++++++++++++++++-- daemon/src/server.rs | 34 ++++++++++-------- 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/daemon/src/notifications.rs b/daemon/src/notifications.rs index a5345c4..e96f632 100644 --- a/daemon/src/notifications.rs +++ b/daemon/src/notifications.rs @@ -10,6 +10,8 @@ use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; use tokio_util::task::TaskTracker; +pub const DEFAULT_NOTIFICATION_TIMEOUT: u64 = 10; //seconds + #[derive(Error, Debug)] pub enum NotificationError { #[error("HTTP request failed: {0}")] @@ -56,6 +58,7 @@ struct NotificationStatus { pub struct NotificationService { url: Option, + timeout: u64, tx: mpsc::Sender, rx: mpsc::Receiver, } @@ -63,7 +66,12 @@ pub struct NotificationService { impl NotificationService { pub fn new(url: Option) -> Self { let (tx, rx) = mpsc::channel(10); - Self { url, tx, rx } + Self { + url, + timeout: DEFAULT_NOTIFICATION_TIMEOUT, + tx, + rx, + } } pub fn new_handler(&self) -> mpsc::Sender { @@ -76,8 +84,14 @@ pub async fn send_notification( http_client: &reqwest::Client, url: &str, message: String, + timeout: u64, ) -> Result<(), NotificationError> { - let response = http_client.post(url).body(message).send().await?; + let response = http_client + .post(url) + .body(message) + .timeout(Duration::from_secs(timeout)) + .send() + .await?; if response.status().is_success() { Ok(()) @@ -151,7 +165,13 @@ pub fn run_notification_worker( } } - match send_notification(&http_client, &url, notification.message.clone()).await + match send_notification( + &http_client, + &url, + notification.message.clone(), + notification_service.timeout, + ) + .await { Ok(()) => { notification.last_sent = Some(Instant::now()); @@ -230,12 +250,56 @@ mod tests { (received_messages, url) } + async fn setup_timeout_server(timeout: u64) -> String { + #[cfg(feature = "rustcrypto-tls")] + { + let _ = rustls_rustcrypto::provider().install_default(); + } + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let url = format!("http://{}", addr); + + tokio::spawn(async move { + // Accept the connection but don't respond in the timeout + let (_socket, _addr) = listener.accept().await.unwrap(); + tokio::time::sleep(Duration::from_secs(timeout * 2)).await; + }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + url + } + async fn cleanup_worker(sender: mpsc::Sender, tracker: TaskTracker) { drop(sender); tracker.close(); tracker.wait().await; } + #[tokio::test] + async fn test_send_notification_times_out() { + let timeout: u64 = 2; + let url = setup_timeout_server(timeout).await; + + let http_client = reqwest::Client::new(); + let result = send_notification( + &http_client, + &url, + "test warning message".to_string(), + timeout, + ) + .await; + + match result { + Err(NotificationError::RequestFailed(reqwest_error)) => { + println!("error = {:?}", reqwest_error); + assert!(reqwest_error.is_timeout()); + } + _ => assert!(false), + } + } + #[tokio::test] async fn test_notification_worker_sends_message() { let (received_messages, url) = setup_test_server().await; diff --git a/daemon/src/server.rs b/daemon/src/server.rs index 82696db..648eef3 100644 --- a/daemon/src/server.rs +++ b/daemon/src/server.rs @@ -25,6 +25,7 @@ use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; use crate::config::Config; use crate::diag::DiagDeviceCtrlMessage; use crate::display::DisplayState; +use crate::notifications::DEFAULT_NOTIFICATION_TIMEOUT; use crate::pcap::generate_pcap_data; use crate::qmdl_store::RecordingStore; @@ -209,20 +210,25 @@ pub async fn test_notification( let http_client = reqwest::Client::new(); let message = "Test notification from Rayhunter".to_string(); - crate::notifications::send_notification(&http_client, url, message) - .await - .map(|()| { - ( - StatusCode::OK, - "Test notification sent successfully".to_string(), - ) - }) - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to send test notification: {e}"), - ) - }) + crate::notifications::send_notification( + &http_client, + url, + message, + DEFAULT_NOTIFICATION_TIMEOUT, + ) + .await + .map(|()| { + ( + StatusCode::OK, + "Test notification sent successfully".to_string(), + ) + }) + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to send test notification: {e}"), + ) + }) } /// Response for GET /api/time