diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-24 18:03:24 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-24 18:03:24 +0300 |
| commit | 5ce738cf5397d0d8a50ed1aa6335297073cc0f0f (patch) | |
| tree | bed3b4f720c58f76562709d22ce30aabf734eaf2 /src/server | |
| parent | 26205a2ac5e21053bd94a0925efcfca44c6f549b (diff) | |
fix: avoid app event queue stalls
refs #265
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 72 |
1 files changed, 68 insertions, 4 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index 632f205d..a6d3f6a5 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -213,7 +213,8 @@ impl HeadlessServer { needs_render = true; } - // 2. Drain internal events. + // 2. Drain a bounded internal-event batch. API handlers perform an + // exhaustive forwarding-aware drain before reading pane/runtime state. if self.drain_internal_events_with_forwarding() { needs_render = true; } @@ -974,13 +975,36 @@ impl HeadlessServer { /// - Detect when a toast is set on AppState and forward as /// `ServerMessage::Notify` to the foreground client for terminal/system delivery. fn drain_internal_events_with_forwarding(&mut self) -> bool { + self.drain_internal_events_with_forwarding_up_to(crate::app::APP_EVENT_DRAIN_LIMIT) + .1 + } + + fn drain_all_internal_events_with_forwarding(&mut self) -> bool { let mut changed = false; - while let Ok(ev) = self.app.event_rx.try_recv() { - changed |= self.handle_internal_event_with_forwarding(ev); + loop { + let (had_event, batch_changed) = + self.drain_internal_events_with_forwarding_up_to(crate::app::APP_EVENT_DRAIN_LIMIT); + changed |= batch_changed; + if !had_event { + break; + } } changed } + fn drain_internal_events_with_forwarding_up_to(&mut self, limit: usize) -> (bool, bool) { + let mut had_event = false; + let mut changed = false; + for _ in 0..limit { + let Ok(ev) = self.app.event_rx.try_recv() else { + break; + }; + had_event = true; + changed |= self.handle_internal_event_with_forwarding(ev); + } + (had_event, changed) + } + fn drain_client_sound_config_reload_request(&mut self) { if !self.app.state.request_client_sound_config_reload { return; @@ -1440,6 +1464,7 @@ impl HeadlessServer { } let changed = api::request_changes_ui(&msg.request); + let changed = self.drain_all_internal_events_with_forwarding() || changed; // Capture toast and effective pane states before the API call so we can // forward resulting client-local notifications. API requests like @@ -1490,7 +1515,8 @@ impl HeadlessServer { .unwrap_or_else(|_| "{}".to_string()) }) } else { - self.app.handle_api_request(msg.request) + self.app + .handle_api_request_after_internal_events_drained(msg.request) }; let _ = msg.respond_to.send(response); @@ -2241,6 +2267,44 @@ mod tests { } } + #[test] + fn headless_api_request_drains_all_pending_internal_events_before_reading_state() { + let mut server = test_headless_server(); + for i in 0..=crate::app::APP_EVENT_DRAIN_LIMIT { + server + .app + .event_tx + .try_send(AppEvent::UpdateReady { + version: format!("4.0.{i}"), + install_command: "herdr install".into(), + }) + .unwrap(); + } + + let (respond_to, response_rx) = std::sync::mpsc::channel(); + assert!( + server.handle_api_request_with_shutdown_check(api::ApiRequestMessage { + request: api::schema::Request { + id: "headless_stop_after_events".into(), + method: api::schema::Method::ServerStop(api::schema::EmptyParams::default()), + }, + respond_to, + }) + ); + let response = response_rx + .recv_timeout(Duration::from_millis(100)) + .unwrap(); + let response: serde_json::Value = serde_json::from_str(&response).unwrap(); + + assert_eq!(response["result"]["type"], "ok"); + let expected_version = format!("4.0.{}", crate::app::APP_EVENT_DRAIN_LIMIT); + assert_eq!( + server.app.state.update_available.as_deref(), + Some(expected_version.as_str()) + ); + assert!(server.app.event_rx.try_recv().is_err()); + } + fn test_client_writer() -> ( ClientWriter, std::sync::mpsc::Receiver<Vec<u8>>, |
