Skip to main content

runmat_plot/gui/
thread_manager.rs

1//! Robust GUI thread management system
2//!
3//! This module provides cross-platform GUI thread management that properly
4//! handles platform-specific requirements (especially macOS EventLoop main thread requirement)
5//! while maintaining high performance and reliability.
6
7use crate::gui::lifecycle::CloseSignal;
8use crate::plots::Figure;
9use runmat_time::Instant;
10use std::sync::{mpsc, Arc, Mutex, OnceLock};
11use std::thread::{self, ThreadId};
12
13/// Thread-safe message passing system for GUI operations
14#[derive(Debug)]
15pub enum GuiThreadMessage {
16    /// Request to show an interactive plot with response channel
17    ShowPlot {
18        figure: Box<Figure>,
19        response: mpsc::Sender<GuiOperationResult>,
20        close_signal: Option<CloseSignal>,
21        window_title: Option<String>,
22    },
23    /// Request to close all GUI windows
24    CloseAll {
25        response: mpsc::Sender<GuiOperationResult>,
26    },
27    /// Health check for GUI thread
28    HealthCheck {
29        response: mpsc::Sender<GuiOperationResult>,
30    },
31    /// Graceful shutdown request
32    Shutdown,
33}
34
35/// Result of GUI operations with comprehensive error information
36#[derive(Debug, Clone)]
37pub enum GuiOperationResult {
38    /// Operation completed successfully
39    Success(String),
40    /// Operation failed with detailed error information
41    Error {
42        message: String,
43        error_code: GuiErrorCode,
44        recoverable: bool,
45    },
46    /// Operation was cancelled or timed out
47    Cancelled(String),
48}
49
50/// Error codes for GUI operations
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum GuiErrorCode {
53    /// EventLoop creation failed (platform-specific)
54    EventLoopCreationFailed,
55    /// Window creation failed
56    WindowCreationFailed,
57    /// WGPU initialization failed
58    WgpuInitializationFailed,
59    /// Thread communication failure
60    ThreadCommunicationFailed,
61    /// Main thread requirement violation
62    MainThreadViolation,
63    /// Resource exhaustion
64    ResourceExhaustion,
65    /// Invalid operation state
66    InvalidState,
67    /// Platform-specific error
68    PlatformError,
69    /// Unknown error
70    Unknown,
71}
72
73impl std::fmt::Display for GuiOperationResult {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            GuiOperationResult::Success(msg) => write!(f, "Success: {msg}"),
77            GuiOperationResult::Error {
78                message,
79                error_code,
80                recoverable,
81            } => {
82                write!(
83                    f,
84                    "Error [{error_code:?}]: {message} (recoverable: {recoverable})"
85                )
86            }
87            GuiOperationResult::Cancelled(msg) => write!(f, "Cancelled: {msg}"),
88        }
89    }
90}
91
92impl std::error::Error for GuiOperationResult {}
93
94/// Main thread detection and validation
95pub struct MainThreadDetector {
96    main_thread_id: OnceLock<ThreadId>,
97}
98
99impl Default for MainThreadDetector {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl MainThreadDetector {
106    /// Create a new main thread detector
107    pub const fn new() -> Self {
108        Self {
109            main_thread_id: OnceLock::new(),
110        }
111    }
112
113    /// Register the current thread as the main thread
114    /// This should be called from main() before any GUI operations
115    pub fn register_main_thread(&self) {
116        let current_id = thread::current().id();
117        if self.main_thread_id.set(current_id).is_err() {
118            // Already set - this is fine, multiple calls are safe
119        }
120    }
121
122    /// Check if the current thread is the main thread
123    pub fn is_main_thread(&self) -> bool {
124        if let Some(main_id) = self.main_thread_id.get() {
125            return thread::current().id() == *main_id;
126        }
127
128        #[cfg(target_os = "macos")]
129        {
130            if is_macos_main_thread() {
131                self.register_main_thread();
132                true
133            } else {
134                false
135            }
136        }
137
138        #[cfg(not(target_os = "macos"))]
139        {
140            // Other platforms do not require the GUI to live on the OS main thread.
141            self.register_main_thread();
142            true
143        }
144    }
145
146    /// Get the main thread ID if registered
147    pub fn main_thread_id(&self) -> Option<ThreadId> {
148        self.main_thread_id.get().copied()
149    }
150}
151
152#[cfg(target_os = "macos")]
153fn is_macos_main_thread() -> bool {
154    unsafe { libc::pthread_main_np() != 0 }
155}
156
157/// Global main thread detector instance
158static MAIN_THREAD_DETECTOR: MainThreadDetector = MainThreadDetector::new();
159
160/// Register the current thread as the main thread
161pub fn register_main_thread() {
162    MAIN_THREAD_DETECTOR.register_main_thread();
163}
164
165/// Check if the current thread is the main thread
166pub fn is_main_thread() -> bool {
167    MAIN_THREAD_DETECTOR.is_main_thread()
168}
169
170/// GUI thread manager with robust error handling and health monitoring
171pub struct GuiThreadManager {
172    /// Message sender to GUI thread
173    sender: mpsc::Sender<GuiThreadMessage>,
174    /// Thread handle for the GUI thread
175    thread_handle: Option<thread::JoinHandle<Result<(), GuiOperationResult>>>,
176    /// Health check state
177    health_state: Arc<Mutex<GuiHealthState>>,
178}
179
180/// GUI thread health monitoring
181#[derive(Debug, Clone)]
182struct GuiHealthState {
183    last_response: Instant,
184    response_count: u64,
185    error_count: u64,
186    is_healthy: bool,
187}
188
189impl GuiThreadManager {
190    /// Create a new GUI thread manager
191    ///
192    /// This must be called from the main thread on macOS, or it will return an error.
193    pub fn new() -> Result<Self, GuiOperationResult> {
194        // Verify we're on the main thread for platforms that require it
195        #[cfg(target_os = "macos")]
196        if !is_main_thread() {
197            return Err(GuiOperationResult::Error {
198                message: "GuiThreadManager must be created on the main thread on macOS".to_string(),
199                error_code: GuiErrorCode::MainThreadViolation,
200                recoverable: false,
201            });
202        }
203
204        let (sender, receiver) = mpsc::channel();
205        let health_state = Arc::new(Mutex::new(GuiHealthState {
206            last_response: Instant::now(),
207            response_count: 0,
208            error_count: 0,
209            is_healthy: true,
210        }));
211
212        let health_state_clone = Arc::clone(&health_state);
213
214        // Start the GUI thread
215        let thread_handle = thread::Builder::new()
216            .name("runmat-gui".to_string())
217            .spawn(move || Self::gui_thread_main(receiver, health_state_clone))
218            .map_err(|e| GuiOperationResult::Error {
219                message: format!("Failed to spawn GUI thread: {e}"),
220                error_code: GuiErrorCode::ThreadCommunicationFailed,
221                recoverable: false,
222            })?;
223
224        Ok(Self {
225            sender,
226            thread_handle: Some(thread_handle),
227            health_state,
228        })
229    }
230
231    /// Main loop for the GUI thread
232    fn gui_thread_main(
233        receiver: mpsc::Receiver<GuiThreadMessage>,
234        health_state: Arc<Mutex<GuiHealthState>>,
235    ) -> Result<(), GuiOperationResult> {
236        log::info!("GUI thread started successfully");
237
238        // Initialize GUI subsystems on this thread
239        #[cfg(feature = "gui")]
240        let gui_context = Self::initialize_gui_context()?;
241
242        loop {
243            match receiver.recv() {
244                Ok(message) => {
245                    let result = Self::handle_gui_message(message, &gui_context);
246
247                    // Update health state
248                    if let Ok(mut health) = health_state.lock() {
249                        health.last_response = Instant::now();
250                        health.response_count += 1;
251
252                        if let Some(GuiOperationResult::Error { .. }) = &result {
253                            health.error_count += 1;
254                            health.is_healthy = health.error_count < 10; // Allow some errors
255                        }
256                    }
257
258                    // If this was a shutdown message, break the loop
259                    if result.is_none() {
260                        break;
261                    }
262                }
263                Err(_) => {
264                    // Channel closed, exit gracefully
265                    log::info!("GUI thread channel closed, shutting down");
266                    break;
267                }
268            }
269        }
270
271        log::info!("GUI thread exiting gracefully");
272        Ok(())
273    }
274
275    /// Initialize GUI context (platform-specific)
276    #[cfg(feature = "gui")]
277    fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
278        use crate::gui::window::WindowConfig;
279
280        // This will create the EventLoop on the correct thread
281        Ok(GuiContext {
282            _default_config: WindowConfig::default(),
283            _active_windows: Vec::new(),
284        })
285    }
286
287    #[cfg(not(feature = "gui"))]
288    fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
289        Err(GuiOperationResult::Error {
290            message: "GUI feature not enabled".to_string(),
291            error_code: GuiErrorCode::InvalidState,
292            recoverable: false,
293        })
294    }
295
296    /// Handle a GUI message and return the result
297    fn handle_gui_message(
298        message: GuiThreadMessage,
299        gui_context: &GuiContext,
300    ) -> Option<GuiOperationResult> {
301        match message {
302            GuiThreadMessage::ShowPlot {
303                figure,
304                response,
305                close_signal,
306                window_title,
307            } => {
308                let result =
309                    Self::handle_show_plot(figure, close_signal, window_title, gui_context);
310                let _ = response.send(result.clone());
311                Some(result)
312            }
313            GuiThreadMessage::CloseAll { response } => {
314                let result = Self::handle_close_all(gui_context);
315                let _ = response.send(result.clone());
316                Some(result)
317            }
318            GuiThreadMessage::HealthCheck { response } => {
319                let result = GuiOperationResult::Success("GUI thread healthy".to_string());
320                let _ = response.send(result.clone());
321                Some(result)
322            }
323            GuiThreadMessage::Shutdown => {
324                log::info!("GUI thread received shutdown signal");
325                None // Signal to exit the loop
326            }
327        }
328    }
329
330    /// Handle show plot request
331    #[cfg(feature = "gui")]
332    fn handle_show_plot(
333        figure: Box<Figure>,
334        close_signal: Option<CloseSignal>,
335        window_title: Option<String>,
336        _gui_context: &GuiContext,
337    ) -> GuiOperationResult {
338        use crate::gui::{window::WindowConfig, PlotWindow};
339
340        // Create a new runtime for this async operation
341        let rt = match tokio::runtime::Runtime::new() {
342            Ok(rt) => rt,
343            Err(e) => {
344                return GuiOperationResult::Error {
345                    message: format!("Failed to create async runtime: {e}"),
346                    error_code: GuiErrorCode::ResourceExhaustion,
347                    recoverable: true,
348                }
349            }
350        };
351
352        rt.block_on(async {
353            let mut config = WindowConfig::default();
354            if let Some(title) = window_title {
355                config.title = title;
356            }
357            let mut window = match PlotWindow::new(config).await {
358                Ok(window) => window,
359                Err(e) => {
360                    return GuiOperationResult::Error {
361                        message: format!("Failed to create window: {e}"),
362                        error_code: GuiErrorCode::WindowCreationFailed,
363                        recoverable: true,
364                    }
365                }
366            };
367
368            if let Some(sig) = close_signal {
369                window.install_close_signal(sig);
370            }
371
372            // Set the figure data
373            window.set_figure(*figure);
374
375            // Run the window (this will block until the window is closed)
376            match window.run().await {
377                Ok(_) => GuiOperationResult::Success("Plot window closed".to_string()),
378                Err(e) => GuiOperationResult::Error {
379                    message: format!("Window runtime error: {e}"),
380                    error_code: GuiErrorCode::PlatformError,
381                    recoverable: true,
382                },
383            }
384        })
385    }
386
387    #[cfg(not(feature = "gui"))]
388    fn handle_show_plot(
389        _figure: Box<Figure>,
390        _close_signal: Option<CloseSignal>,
391        _gui_context: &GuiContext,
392    ) -> GuiOperationResult {
393        GuiOperationResult::Error {
394            message: "GUI feature not enabled".to_string(),
395            error_code: GuiErrorCode::InvalidState,
396            recoverable: false,
397        }
398    }
399
400    /// Handle close all windows request
401    fn handle_close_all(_gui_context: &GuiContext) -> GuiOperationResult {
402        // Implementation would close all active windows
403        GuiOperationResult::Success("All windows closed".to_string())
404    }
405
406    /// Show a plot using the GUI thread manager
407    pub fn show_plot(&self, figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
408        self.show_plot_with_signal(figure, None)
409    }
410
411    pub fn show_plot_with_signal(
412        &self,
413        figure: Figure,
414        close_signal: Option<CloseSignal>,
415    ) -> Result<GuiOperationResult, GuiOperationResult> {
416        self.show_plot_with_signal_and_title(figure, close_signal, None)
417    }
418
419    pub fn show_plot_with_signal_and_title(
420        &self,
421        figure: Figure,
422        close_signal: Option<CloseSignal>,
423        window_title: Option<String>,
424    ) -> Result<GuiOperationResult, GuiOperationResult> {
425        let (response_tx, response_rx) = mpsc::channel();
426
427        let message = GuiThreadMessage::ShowPlot {
428            figure: Box::new(figure),
429            response: response_tx,
430            close_signal,
431            window_title,
432        };
433
434        // Send message to GUI thread
435        self.sender
436            .send(message)
437            .map_err(|_| GuiOperationResult::Error {
438                message: "GUI thread is not responding".to_string(),
439                error_code: GuiErrorCode::ThreadCommunicationFailed,
440                recoverable: false,
441            })?;
442
443        // Wait for response with timeout
444        match response_rx.recv_timeout(std::time::Duration::from_secs(30)) {
445            Ok(result) => Ok(result),
446            Err(mpsc::RecvTimeoutError::Timeout) => Err(GuiOperationResult::Cancelled(
447                "GUI operation timed out after 30 seconds".to_string(),
448            )),
449            Err(mpsc::RecvTimeoutError::Disconnected) => Err(GuiOperationResult::Error {
450                message: "GUI thread disconnected unexpectedly".to_string(),
451                error_code: GuiErrorCode::ThreadCommunicationFailed,
452                recoverable: false,
453            }),
454        }
455    }
456
457    /// Perform a health check on the GUI thread
458    pub fn health_check(&self) -> Result<GuiOperationResult, GuiOperationResult> {
459        let (response_tx, response_rx) = mpsc::channel();
460
461        let message = GuiThreadMessage::HealthCheck {
462            response: response_tx,
463        };
464
465        self.sender
466            .send(message)
467            .map_err(|_| GuiOperationResult::Error {
468                message: "GUI thread is not responding".to_string(),
469                error_code: GuiErrorCode::ThreadCommunicationFailed,
470                recoverable: false,
471            })?;
472
473        match response_rx.recv_timeout(std::time::Duration::from_secs(5)) {
474            Ok(result) => Ok(result),
475            Err(_) => Err(GuiOperationResult::Error {
476                message: "GUI thread health check failed".to_string(),
477                error_code: GuiErrorCode::ThreadCommunicationFailed,
478                recoverable: true,
479            }),
480        }
481    }
482
483    /// Get the current health state of the GUI thread
484    pub fn get_health_state(&self) -> Option<(u64, u64, bool)> {
485        self.health_state
486            .lock()
487            .ok()
488            .map(|health| (health.response_count, health.error_count, health.is_healthy))
489    }
490
491    /// Gracefully shutdown the GUI thread
492    pub fn shutdown(mut self) -> Result<(), GuiOperationResult> {
493        // Send shutdown signal
494        let _ = self.sender.send(GuiThreadMessage::Shutdown);
495
496        // Wait for thread to complete
497        if let Some(handle) = self.thread_handle.take() {
498            match handle.join() {
499                Ok(Ok(())) => Ok(()),
500                Ok(Err(e)) => Err(e),
501                Err(_) => Err(GuiOperationResult::Error {
502                    message: "GUI thread panicked during shutdown".to_string(),
503                    error_code: GuiErrorCode::PlatformError,
504                    recoverable: false,
505                }),
506            }
507        } else {
508            Ok(())
509        }
510    }
511}
512
513/// Drop implementation for graceful cleanup
514impl Drop for GuiThreadManager {
515    fn drop(&mut self) {
516        if self.thread_handle.is_some() {
517            log::warn!("GuiThreadManager dropped without explicit shutdown");
518            let _ = self.sender.send(GuiThreadMessage::Shutdown);
519
520            // Give the thread a moment to shut down gracefully
521            if let Some(handle) = self.thread_handle.take() {
522                let _ = handle.join();
523            }
524        }
525    }
526}
527
528/// GUI context for managing windows and resources
529#[cfg(feature = "gui")]
530struct GuiContext {
531    _default_config: crate::gui::window::WindowConfig,
532    _active_windows: Vec<String>, // Window IDs for tracking
533}
534
535#[cfg(not(feature = "gui"))]
536struct GuiContext {
537    // Stub for non-GUI builds
538}
539
540/// Global GUI thread manager instance
541static GUI_MANAGER: OnceLock<Arc<Mutex<Option<GuiThreadManager>>>> = OnceLock::new();
542
543/// Initialize the global GUI thread manager
544pub fn initialize_gui_manager() -> Result<(), GuiOperationResult> {
545    let manager_mutex = GUI_MANAGER.get_or_init(|| Arc::new(Mutex::new(None)));
546
547    let mut manager_guard = manager_mutex
548        .lock()
549        .map_err(|_| GuiOperationResult::Error {
550            message: "Failed to acquire GUI manager lock".to_string(),
551            error_code: GuiErrorCode::ThreadCommunicationFailed,
552            recoverable: false,
553        })?;
554
555    if manager_guard.is_some() {
556        return Ok(()); // Already initialized
557    }
558
559    let manager = GuiThreadManager::new()?;
560    *manager_guard = Some(manager);
561
562    log::info!("Global GUI thread manager initialized successfully");
563    Ok(())
564}
565
566/// Get a reference to the global GUI thread manager
567pub fn get_gui_manager() -> Result<Arc<Mutex<Option<GuiThreadManager>>>, GuiOperationResult> {
568    GUI_MANAGER
569        .get()
570        .ok_or_else(|| GuiOperationResult::Error {
571            message: "GUI manager not initialized. Call initialize_gui_manager() first."
572                .to_string(),
573            error_code: GuiErrorCode::InvalidState,
574            recoverable: true,
575        })
576        .map(Arc::clone)
577}
578
579/// Show a plot using the global GUI manager
580pub fn show_plot_global(figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
581    show_plot_global_with_signal(figure, None)
582}
583
584pub fn show_plot_global_with_signal(
585    figure: Figure,
586    close_signal: Option<CloseSignal>,
587) -> Result<GuiOperationResult, GuiOperationResult> {
588    show_plot_global_with_signal_and_title(figure, close_signal, None)
589}
590
591pub fn show_plot_global_with_signal_and_title(
592    figure: Figure,
593    close_signal: Option<CloseSignal>,
594    window_title: Option<String>,
595) -> Result<GuiOperationResult, GuiOperationResult> {
596    let manager_mutex = get_gui_manager()?;
597    let manager_guard = manager_mutex
598        .lock()
599        .map_err(|_| GuiOperationResult::Error {
600            message: "Failed to acquire GUI manager lock".to_string(),
601            error_code: GuiErrorCode::ThreadCommunicationFailed,
602            recoverable: false,
603        })?;
604
605    match manager_guard.as_ref() {
606        Some(manager) => {
607            manager.show_plot_with_signal_and_title(figure, close_signal, window_title)
608        }
609        None => Err(GuiOperationResult::Error {
610            message: "GUI manager not initialized".to_string(),
611            error_code: GuiErrorCode::InvalidState,
612            recoverable: true,
613        }),
614    }
615}
616
617/// Perform a health check on the global GUI manager
618pub fn health_check_global() -> Result<GuiOperationResult, GuiOperationResult> {
619    let manager_mutex = get_gui_manager()?;
620    let manager_guard = manager_mutex
621        .lock()
622        .map_err(|_| GuiOperationResult::Error {
623            message: "Failed to acquire GUI manager lock".to_string(),
624            error_code: GuiErrorCode::ThreadCommunicationFailed,
625            recoverable: false,
626        })?;
627
628    match manager_guard.as_ref() {
629        Some(manager) => manager.health_check(),
630        None => Err(GuiOperationResult::Error {
631            message: "GUI manager not initialized".to_string(),
632            error_code: GuiErrorCode::InvalidState,
633            recoverable: true,
634        }),
635    }
636}