ricecoder_external_lsp/process/
manager.rs

1//! Process lifecycle management
2
3use 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
10/// Manages LSP server process lifecycle
11pub struct ProcessManager {
12    /// Configuration for the LSP server
13    config: LspServerConfig,
14    /// Current process handle
15    process: Option<Child>,
16    /// Current state
17    state: ClientState,
18    /// Number of restart attempts
19    restart_count: u32,
20    /// Time of last restart attempt
21    last_restart_attempt: Option<Instant>,
22}
23
24impl ProcessManager {
25    /// Create a new process manager
26    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    /// Get the current state
37    pub fn state(&self) -> ClientState {
38        self.state
39    }
40
41    /// Get the restart count
42    pub fn restart_count(&self) -> u32 {
43        self.restart_count
44    }
45
46    /// Spawn the LSP server process
47    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        // Build the command
62        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        // Set environment variables
69        for (key, value) in &self.config.env {
70            cmd.env(key, value);
71        }
72
73        // Spawn the process
74        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                // Store the process handle
84                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    /// Gracefully shutdown the process
103    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            // Try graceful shutdown first
116            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            // Wait for process to exit
125            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    /// Check if process is still running
153    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                    // Process has exited
158                    self.process = None;
159                    self.state = ClientState::Crashed;
160                    false
161                }
162                Ok(None) => {
163                    // Process is still running
164                    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    /// Mark the process as unhealthy
181    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    /// Check if restart is allowed
190    pub fn can_restart(&self) -> bool {
191        self.restart_count < self.config.max_restarts
192    }
193
194    /// Prepare for restart with exponential backoff
195    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    /// Get the process stdin if available
220    pub fn stdin(&mut self) -> Option<tokio::process::ChildStdin> {
221        self.process.as_mut().and_then(|child| child.stdin.take())
222    }
223
224    /// Get the process stdout if available
225    pub fn stdout(&mut self) -> Option<tokio::process::ChildStdout> {
226        self.process.as_mut().and_then(|child| child.stdout.take())
227    }
228
229    /// Get the process stderr if available
230    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
253/// Calculate exponential backoff duration
254/// Formula: min(base * 2^attempt, max_backoff)
255fn calculate_exponential_backoff(attempt: u32) -> Duration {
256    const BASE_BACKOFF_MS: u64 = 100;
257    const MAX_BACKOFF_MS: u64 = 30000; // 30 seconds
258
259    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        // Should cap at max
277        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        // Simulate restart attempts
325        let _ = manager.prepare_restart();
326        assert!(manager.can_restart());
327
328        let _ = manager.prepare_restart();
329        assert!(!manager.can_restart());
330    }
331}