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