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