ricecoder_ide/
lsp_monitor.rs1use 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
14type AvailabilityCallback = Arc<dyn Fn(&str, bool) + Send + Sync>;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LspHealthStatus {
20 Healthy,
22 Unhealthy,
24 Unknown,
26}
27
28pub struct LspMonitor {
30 servers: HashMap<String, LspServerConfig>,
32 health_status: Arc<RwLock<HashMap<String, LspHealthStatus>>>,
34 availability_callbacks: Arc<RwLock<Vec<AvailabilityCallback>>>,
36}
37
38impl LspMonitor {
39 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 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 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 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 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 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 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 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 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 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}