ricecoder_external_lsp/process/
manager.rs1use crate::error::{ExternalLspError, Result};
4use crate::types::{ClientState, LspServerConfig};
5use std::process::Stdio;
6use std::time::{Duration, Instant};
7use tokio::process::{Child, Command};
8use tracing::{debug, error, info, warn};
9
10pub struct ProcessManager {
12 config: LspServerConfig,
14 process: Option<Child>,
16 state: ClientState,
18 restart_count: u32,
20 last_restart_attempt: Option<Instant>,
22}
23
24impl ProcessManager {
25 pub fn new(config: LspServerConfig) -> Self {
27 Self {
28 config,
29 process: None,
30 state: ClientState::Stopped,
31 restart_count: 0,
32 last_restart_attempt: None,
33 }
34 }
35
36 pub fn state(&self) -> ClientState {
38 self.state
39 }
40
41 pub fn restart_count(&self) -> u32 {
43 self.restart_count
44 }
45
46 pub async fn spawn(&mut self) -> Result<()> {
48 if self.state != ClientState::Stopped {
49 return Err(ExternalLspError::ProtocolError(
50 format!("Cannot spawn process in state: {:?}", self.state),
51 ));
52 }
53
54 self.state = ClientState::Starting;
55 debug!(
56 language = %self.config.language,
57 executable = %self.config.executable,
58 "Starting LSP server process"
59 );
60
61 let mut cmd = Command::new(&self.config.executable);
63 cmd.args(&self.config.args)
64 .stdin(Stdio::piped())
65 .stdout(Stdio::piped())
66 .stderr(Stdio::piped());
67
68 for (key, value) in &self.config.env {
70 cmd.env(key, value);
71 }
72
73 match cmd.spawn() {
75 Ok(child) => {
76 info!(
77 language = %self.config.language,
78 executable = %self.config.executable,
79 pid = ?child.id(),
80 "LSP server process spawned successfully"
81 );
82
83 self.process = Some(child);
85 self.state = ClientState::Running;
86 self.restart_count = 0;
87 Ok(())
88 }
89 Err(e) => {
90 error!(
91 language = %self.config.language,
92 executable = %self.config.executable,
93 error = %e,
94 "Failed to spawn LSP server process"
95 );
96 self.state = ClientState::Stopped;
97 Err(ExternalLspError::SpawnFailed(e))
98 }
99 }
100 }
101
102 pub async fn shutdown(&mut self) -> Result<()> {
104 if self.state == ClientState::Stopped {
105 return Ok(());
106 }
107
108 self.state = ClientState::ShuttingDown;
109 debug!(
110 language = %self.config.language,
111 "Shutting down LSP server process"
112 );
113
114 if let Some(mut child) = self.process.take() {
115 if let Err(e) = child.kill().await {
117 warn!(
118 language = %self.config.language,
119 error = %e,
120 "Failed to kill LSP server process"
121 );
122 }
123
124 match tokio::time::timeout(Duration::from_secs(5), child.wait()).await {
126 Ok(Ok(_)) => {
127 info!(
128 language = %self.config.language,
129 "LSP server process shut down gracefully"
130 );
131 }
132 Ok(Err(e)) => {
133 warn!(
134 language = %self.config.language,
135 error = %e,
136 "Error waiting for LSP server process to exit"
137 );
138 }
139 Err(_) => {
140 warn!(
141 language = %self.config.language,
142 "Timeout waiting for LSP server process to exit"
143 );
144 }
145 }
146 }
147
148 self.state = ClientState::Stopped;
149 Ok(())
150 }
151
152 pub fn is_running(&mut self) -> bool {
154 if let Some(ref mut child) = self.process {
155 match child.try_wait() {
156 Ok(Some(_)) => {
157 self.process = None;
159 self.state = ClientState::Crashed;
160 false
161 }
162 Ok(None) => {
163 true
165 }
166 Err(e) => {
167 error!(
168 language = %self.config.language,
169 error = %e,
170 "Error checking process status"
171 );
172 false
173 }
174 }
175 } else {
176 false
177 }
178 }
179
180 pub fn mark_unhealthy(&mut self) {
182 self.state = ClientState::Unhealthy;
183 debug!(
184 language = %self.config.language,
185 "Marked LSP server as unhealthy"
186 );
187 }
188
189 pub fn can_restart(&self) -> bool {
191 self.restart_count < self.config.max_restarts
192 }
193
194 pub fn prepare_restart(&mut self) -> Result<Duration> {
196 if !self.can_restart() {
197 return Err(ExternalLspError::ServerCrashed {
198 reason: format!(
199 "Max restart attempts ({}) exceeded",
200 self.config.max_restarts
201 ),
202 });
203 }
204
205 self.restart_count += 1;
206 let backoff = calculate_exponential_backoff(self.restart_count);
207 self.last_restart_attempt = Some(Instant::now());
208
209 debug!(
210 language = %self.config.language,
211 restart_count = self.restart_count,
212 backoff_ms = backoff.as_millis(),
213 "Preparing to restart LSP server with exponential backoff"
214 );
215
216 Ok(backoff)
217 }
218
219 pub fn stdin(&mut self) -> Option<tokio::process::ChildStdin> {
221 self.process.as_mut().and_then(|child| child.stdin.take())
222 }
223
224 pub fn stdout(&mut self) -> Option<tokio::process::ChildStdout> {
226 self.process.as_mut().and_then(|child| child.stdout.take())
227 }
228
229 pub fn stderr(&mut self) -> Option<tokio::process::ChildStderr> {
231 self.process.as_mut().and_then(|child| child.stderr.take())
232 }
233}
234
235impl Default for ProcessManager {
236 fn default() -> Self {
237 Self::new(LspServerConfig {
238 language: "unknown".to_string(),
239 extensions: vec![],
240 executable: String::new(),
241 args: vec![],
242 env: Default::default(),
243 init_options: None,
244 enabled: true,
245 timeout_ms: 5000,
246 max_restarts: 3,
247 idle_timeout_ms: 300000,
248 output_mapping: None,
249 })
250 }
251}
252
253fn calculate_exponential_backoff(attempt: u32) -> Duration {
256 const BASE_BACKOFF_MS: u64 = 100;
257 const MAX_BACKOFF_MS: u64 = 30000; let backoff_ms = BASE_BACKOFF_MS
260 .saturating_mul(2_u64.saturating_pow(attempt))
261 .min(MAX_BACKOFF_MS);
262
263 Duration::from_millis(backoff_ms)
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_exponential_backoff_calculation() {
272 assert_eq!(calculate_exponential_backoff(0), Duration::from_millis(100));
273 assert_eq!(calculate_exponential_backoff(1), Duration::from_millis(200));
274 assert_eq!(calculate_exponential_backoff(2), Duration::from_millis(400));
275 assert_eq!(calculate_exponential_backoff(3), Duration::from_millis(800));
276 assert_eq!(
278 calculate_exponential_backoff(20),
279 Duration::from_millis(30000)
280 );
281 }
282
283 #[test]
284 fn test_process_manager_creation() {
285 let config = LspServerConfig {
286 language: "rust".to_string(),
287 extensions: vec![".rs".to_string()],
288 executable: "rust-analyzer".to_string(),
289 args: vec![],
290 env: Default::default(),
291 init_options: None,
292 enabled: true,
293 timeout_ms: 5000,
294 max_restarts: 3,
295 idle_timeout_ms: 300000,
296 output_mapping: None,
297 };
298
299 let manager = ProcessManager::new(config);
300 assert_eq!(manager.state(), ClientState::Stopped);
301 assert_eq!(manager.restart_count(), 0);
302 assert!(manager.can_restart());
303 }
304
305 #[test]
306 fn test_restart_limit() {
307 let config = LspServerConfig {
308 language: "rust".to_string(),
309 extensions: vec![".rs".to_string()],
310 executable: "rust-analyzer".to_string(),
311 args: vec![],
312 env: Default::default(),
313 init_options: None,
314 enabled: true,
315 timeout_ms: 5000,
316 max_restarts: 2,
317 idle_timeout_ms: 300000,
318 output_mapping: None,
319 };
320
321 let mut manager = ProcessManager::new(config);
322 assert!(manager.can_restart());
323
324 let _ = manager.prepare_restart();
326 assert!(manager.can_restart());
327
328 let _ = manager.prepare_restart();
329 assert!(!manager.can_restart());
330 }
331}