ricecoder_ide/
lsp_monitor.rs

1//! LSP server availability monitoring
2//!
3//! This module monitors external LSP server availability and detects when servers
4//! become available or unavailable. It supports periodic health checks and automatic
5//! provider switching based on availability changes.
6
7use crate::error::{IdeError, IdeResult};
8use crate::types::LspServerConfig;
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12use tracing::{debug, info, warn};
13
14/// Type alias for availability change callback
15type AvailabilityCallback = Arc<dyn Fn(&str, bool) + Send + Sync>;
16
17/// LSP server health status
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LspHealthStatus {
20    /// Server is healthy and responding
21    Healthy,
22    /// Server is unhealthy or not responding
23    Unhealthy,
24    /// Server status is unknown
25    Unknown,
26}
27
28/// LSP server availability monitor
29pub struct LspMonitor {
30    /// Server configurations by language
31    servers: HashMap<String, LspServerConfig>,
32    /// Current health status by language
33    health_status: Arc<RwLock<HashMap<String, LspHealthStatus>>>,
34    /// Availability change callbacks
35    availability_callbacks: Arc<RwLock<Vec<AvailabilityCallback>>>,
36}
37
38impl LspMonitor {
39    /// Create a new LSP monitor
40    pub fn new(servers: HashMap<String, LspServerConfig>) -> Self {
41        let mut health_status = HashMap::new();
42        for language in servers.keys() {
43            health_status.insert(language.clone(), LspHealthStatus::Unknown);
44        }
45
46        LspMonitor {
47            servers,
48            health_status: Arc::new(RwLock::new(health_status)),
49            availability_callbacks: Arc::new(RwLock::new(Vec::new())),
50        }
51    }
52
53    /// Register a callback for availability changes
54    pub async fn on_availability_changed(
55        &self,
56        callback: AvailabilityCallback,
57    ) -> IdeResult<()> {
58        debug!("Registering LSP availability change callback");
59        let mut callbacks = self.availability_callbacks.write().await;
60        callbacks.push(callback);
61        Ok(())
62    }
63
64    /// Check health of a specific LSP server
65    pub async fn check_server_health(&self, language: &str) -> IdeResult<LspHealthStatus> {
66        debug!("Checking health of LSP server for language: {}", language);
67
68        let server_config = self
69            .servers
70            .get(language)
71            .ok_or_else(|| IdeError::config_error(format!("No LSP server configured for {}", language)))?;
72
73        // Simulate health check by attempting to spawn the server process
74        // In a real implementation, this would send a health check request to the LSP server
75        let status = match tokio::process::Command::new(&server_config.command)
76            .args(&server_config.args)
77            .arg("--version")
78            .output()
79            .await
80        {
81            Ok(output) => {
82                if output.status.success() {
83                    debug!("LSP server for {} is healthy", language);
84                    LspHealthStatus::Healthy
85                } else {
86                    warn!("LSP server for {} returned non-zero exit code", language);
87                    LspHealthStatus::Unhealthy
88                }
89            }
90            Err(e) => {
91                warn!("Failed to check LSP server health for {}: {}", language, e);
92                LspHealthStatus::Unhealthy
93            }
94        };
95
96        // Update status and notify if changed
97        let mut health_status = self.health_status.write().await;
98        let old_status = health_status.get(language).copied().unwrap_or(LspHealthStatus::Unknown);
99
100        if old_status != status {
101            health_status.insert(language.to_string(), status);
102            let is_available = status == LspHealthStatus::Healthy;
103            info!(
104                "LSP server availability changed for {}: {}",
105                language,
106                if is_available { "available" } else { "unavailable" }
107            );
108
109            // Notify callbacks
110            let callbacks = self.availability_callbacks.read().await;
111            for callback in callbacks.iter() {
112                callback(language, is_available);
113            }
114        }
115
116        Ok(status)
117    }
118
119    /// Get current health status of a server
120    pub async fn get_server_status(&self, language: &str) -> IdeResult<LspHealthStatus> {
121        let health_status = self.health_status.read().await;
122        Ok(health_status
123            .get(language)
124            .copied()
125            .unwrap_or(LspHealthStatus::Unknown))
126    }
127
128    /// Check health of all configured servers
129    pub async fn check_all_servers(&self) -> IdeResult<HashMap<String, LspHealthStatus>> {
130        debug!("Checking health of all LSP servers");
131        let mut results = HashMap::new();
132
133        for language in self.servers.keys() {
134            let status = self.check_server_health(language).await?;
135            results.insert(language.clone(), status);
136        }
137
138        Ok(results)
139    }
140
141    /// Start periodic health checks
142    pub async fn start_health_checks(&self, interval_ms: u64) -> IdeResult<()> {
143        info!("Starting LSP health checks with {}ms interval", interval_ms);
144
145        let servers = self.servers.clone();
146        let health_status = self.health_status.clone();
147        let availability_callbacks = self.availability_callbacks.clone();
148
149        tokio::spawn(async move {
150            loop {
151                tokio::time::sleep(tokio::time::Duration::from_millis(interval_ms)).await;
152
153                for (language, server_config) in &servers {
154                    match tokio::process::Command::new(&server_config.command)
155                        .args(&server_config.args)
156                        .arg("--version")
157                        .output()
158                        .await
159                    {
160                        Ok(output) => {
161                            let new_status = if output.status.success() {
162                                LspHealthStatus::Healthy
163                            } else {
164                                LspHealthStatus::Unhealthy
165                            };
166
167                            let mut status = health_status.write().await;
168                            let old_status = status.get(language).copied().unwrap_or(LspHealthStatus::Unknown);
169
170                            if old_status != new_status {
171                                status.insert(language.clone(), new_status);
172                                let is_available = new_status == LspHealthStatus::Healthy;
173                                info!(
174                                    "LSP server availability changed for {}: {}",
175                                    language,
176                                    if is_available { "available" } else { "unavailable" }
177                                );
178
179                                let callbacks = availability_callbacks.read().await;
180                                for callback in callbacks.iter() {
181                                    callback(language, is_available);
182                                }
183                            }
184                        }
185                        Err(e) => {
186                            warn!("Failed to check LSP server health for {}: {}", language, e);
187                            let mut status = health_status.write().await;
188                            let old_status = status.get(language).copied().unwrap_or(LspHealthStatus::Unknown);
189
190                            if old_status != LspHealthStatus::Unhealthy {
191                                status.insert(language.clone(), LspHealthStatus::Unhealthy);
192                                let callbacks = availability_callbacks.read().await;
193                                for callback in callbacks.iter() {
194                                    callback(language, false);
195                                }
196                            }
197                        }
198                    }
199                }
200            }
201        });
202
203        Ok(())
204    }
205
206    /// Get all available languages
207    pub fn available_languages(&self) -> Vec<String> {
208        self.servers.keys().cloned().collect()
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    fn create_test_server_config(language: &str) -> LspServerConfig {
217        LspServerConfig {
218            language: language.to_string(),
219            command: "echo".to_string(),
220            args: vec!["test".to_string()],
221            timeout_ms: 5000,
222        }
223    }
224
225    #[tokio::test]
226    async fn test_lsp_monitor_creation() {
227        let mut servers = HashMap::new();
228        servers.insert("rust".to_string(), create_test_server_config("rust"));
229
230        let monitor = LspMonitor::new(servers);
231        assert_eq!(monitor.available_languages().len(), 1);
232    }
233
234    #[tokio::test]
235    async fn test_get_server_status_unknown() {
236        let mut servers = HashMap::new();
237        servers.insert("rust".to_string(), create_test_server_config("rust"));
238
239        let monitor = LspMonitor::new(servers);
240        let status = monitor.get_server_status("rust").await.unwrap();
241        assert_eq!(status, LspHealthStatus::Unknown);
242    }
243
244    #[tokio::test]
245    async fn test_register_availability_callback() {
246        let mut servers = HashMap::new();
247        servers.insert("rust".to_string(), create_test_server_config("rust"));
248
249        let monitor = LspMonitor::new(servers);
250        let callback = Arc::new(|_: &str, _: bool| {});
251        assert!(monitor.on_availability_changed(callback).await.is_ok());
252    }
253
254    #[tokio::test]
255    async fn test_check_all_servers() {
256        let mut servers = HashMap::new();
257        servers.insert("rust".to_string(), create_test_server_config("rust"));
258        servers.insert("typescript".to_string(), create_test_server_config("typescript"));
259
260        let monitor = LspMonitor::new(servers);
261        let results = monitor.check_all_servers().await.unwrap();
262        assert_eq!(results.len(), 2);
263    }
264
265    #[tokio::test]
266    async fn test_check_nonexistent_server() {
267        let servers = HashMap::new();
268        let monitor = LspMonitor::new(servers);
269        let result = monitor.check_server_health("rust").await;
270        assert!(result.is_err());
271    }
272}