Skip to main content

ane/commands/lsp_engine/
engine.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::process::{Child, Command, Stdio};
4use std::sync::mpsc;
5use std::sync::{Arc, Condvar, Mutex};
6use std::thread;
7use std::time::{Duration, Instant};
8
9use anyhow::{Result, bail};
10use serde_json::{Value, json};
11
12use crate::data::lsp::registry;
13use crate::data::lsp::types::{
14    CompletionItem, DocumentSymbol, HoverInfo, Language, Location, LspEvent, LspServerInfo,
15    SelectionRange, SemanticToken, ServerState, SymbolKind, SymbolRange,
16};
17
18use super::installer::InstallProgress;
19
20use super::detector;
21use super::health::{self, HealthStatus};
22use super::installer;
23use super::transport::LspTransport;
24
25const SHUTDOWN_GRACE: Duration = Duration::from_millis(1_500);
26
27pub struct LspEngineConfig {
28    pub auto_install: bool,
29    pub startup_timeout: Duration,
30    binary_name_override: Option<String>,
31    binary_args_override: Vec<String>,
32    check_command_override: Option<String>,
33}
34
35impl Default for LspEngineConfig {
36    fn default() -> Self {
37        Self {
38            auto_install: true,
39            startup_timeout: Duration::from_secs(30),
40            binary_name_override: None,
41            binary_args_override: Vec::new(),
42            check_command_override: None,
43        }
44    }
45}
46
47impl LspEngineConfig {
48    pub fn with_startup_timeout(mut self, timeout: Duration) -> Self {
49        self.startup_timeout = timeout;
50        self
51    }
52
53    pub fn with_auto_install(mut self, auto: bool) -> Self {
54        self.auto_install = auto;
55        self
56    }
57
58    pub fn with_server_override(
59        mut self,
60        binary: impl Into<String>,
61        args: Vec<String>,
62        check_cmd: impl Into<String>,
63    ) -> Self {
64        self.binary_name_override = Some(binary.into());
65        self.binary_args_override = args;
66        self.check_command_override = Some(check_cmd.into());
67        self
68    }
69}
70
71#[derive(Clone)]
72struct ServerOverrides {
73    binary_name: Option<String>,
74    binary_args: Vec<String>,
75    check_command: Option<String>,
76}
77
78struct StartupContext {
79    lang: Language,
80    server_info: &'static LspServerInfo,
81    root_path: PathBuf,
82    auto_install: bool,
83    overrides: ServerOverrides,
84    startup_timeout: Duration,
85    install_progress: Option<Arc<dyn InstallProgress>>,
86    install_lock: Arc<Mutex<()>>,
87}
88
89struct ServerInstance {
90    state: Arc<(Mutex<ServerState>, Condvar)>,
91    server_info: &'static LspServerInfo,
92    /// Shared with startup thread and watchdog so any of them can kill the child.
93    child: Arc<Mutex<Option<Child>>>,
94    transport: Option<LspTransport>,
95    startup_rx: Option<mpsc::Receiver<LspTransport>>,
96    opened_files: HashSet<PathBuf>,
97    root_path: PathBuf,
98}
99
100impl ServerInstance {
101    fn get_state(&self) -> ServerState {
102        *self.state.0.lock().unwrap()
103    }
104}
105
106/// Atomically attempt a state transition. Returns the previous state if the
107/// transition was permitted, or `None` if it was rejected. Emits the
108/// `StateChanged` event on success.
109fn try_transition(
110    state: &Arc<(Mutex<ServerState>, Condvar)>,
111    lang: Language,
112    new_state: ServerState,
113    event_tx: &mpsc::Sender<LspEvent>,
114) -> Option<ServerState> {
115    let (lock, cvar) = &**state;
116    let mut s = lock.lock().unwrap();
117    let old = *s;
118    if !old.can_transition_to(new_state) {
119        return None;
120    }
121    *s = new_state;
122    drop(s);
123    cvar.notify_all();
124    let _ = event_tx.send(LspEvent::StateChanged {
125        language: lang,
126        old,
127        new: new_state,
128    });
129    Some(old)
130}
131
132pub struct LspEngine {
133    servers: HashMap<Language, ServerInstance>,
134    config: LspEngineConfig,
135    event_tx: mpsc::Sender<LspEvent>,
136    event_rx: mpsc::Receiver<LspEvent>,
137    install_progress: Option<Arc<dyn InstallProgress>>,
138    install_lock: Arc<Mutex<()>>,
139    #[cfg(any(test, feature = "test-support"))]
140    test_symbols: HashMap<PathBuf, Vec<DocumentSymbol>>,
141    #[cfg(any(test, feature = "test-support"))]
142    test_selection_ranges: HashMap<(PathBuf, usize, usize), SelectionRange>,
143    #[cfg(any(test, feature = "test-support"))]
144    test_semantic_tokens: HashMap<PathBuf, Vec<SemanticToken>>,
145    #[cfg(any(test, feature = "test-support"))]
146    pub did_open_log: Vec<(PathBuf, String)>,
147    #[cfg(any(test, feature = "test-support"))]
148    pub test_semantic_tokens_delay: Option<Duration>,
149}
150
151impl LspEngine {
152    pub fn new(config: LspEngineConfig) -> Self {
153        let (event_tx, event_rx) = mpsc::channel();
154        Self {
155            servers: HashMap::new(),
156            config,
157            event_tx,
158            event_rx,
159            install_progress: None,
160            install_lock: Arc::new(Mutex::new(())),
161            #[cfg(any(test, feature = "test-support"))]
162            test_symbols: HashMap::new(),
163            #[cfg(any(test, feature = "test-support"))]
164            test_selection_ranges: HashMap::new(),
165            #[cfg(any(test, feature = "test-support"))]
166            test_semantic_tokens: HashMap::new(),
167            #[cfg(any(test, feature = "test-support"))]
168            did_open_log: Vec::new(),
169            #[cfg(any(test, feature = "test-support"))]
170            test_semantic_tokens_delay: None,
171        }
172    }
173
174    #[cfg(any(test, feature = "test-support"))]
175    pub fn inject_test_symbols(&mut self, path: PathBuf, symbols: Vec<DocumentSymbol>) {
176        self.test_symbols.insert(path, symbols);
177    }
178
179    #[cfg(any(test, feature = "test-support"))]
180    pub fn inject_test_selection_range(
181        &mut self,
182        path: PathBuf,
183        line: usize,
184        col: usize,
185        sel_range: SelectionRange,
186    ) {
187        self.test_selection_ranges
188            .insert((path, line, col), sel_range);
189    }
190
191    #[cfg(any(test, feature = "test-support"))]
192    pub fn inject_test_semantic_tokens(&mut self, path: PathBuf, tokens: Vec<SemanticToken>) {
193        self.test_semantic_tokens.insert(path, tokens);
194    }
195
196    pub fn set_install_progress(&mut self, progress: Arc<dyn InstallProgress>) {
197        self.install_progress = Some(progress);
198    }
199
200    pub fn startup_timeout(&self) -> Duration {
201        self.config.startup_timeout
202    }
203
204    pub fn start_for_context(&mut self, root_path: &Path, files: &[&Path]) -> Result<()> {
205        let languages = detector::detect_languages(root_path, files);
206
207        for lang in languages {
208            if !lang.capabilities().has_lsp {
209                continue;
210            }
211
212            if self.servers.contains_key(&lang) {
213                continue;
214            }
215
216            let server_info = match registry::server_for_language(lang) {
217                Some(info) => info,
218                None => continue,
219            };
220
221            let resolved_root = registry::workspace_root_for_language(root_path, lang)
222                .unwrap_or_else(|| root_path.to_path_buf());
223            self.spawn_server(lang, server_info, resolved_root);
224        }
225
226        Ok(())
227    }
228
229    fn spawn_server(
230        &mut self,
231        lang: Language,
232        server_info: &'static LspServerInfo,
233        root_path: PathBuf,
234    ) {
235        let state = Arc::new((Mutex::new(ServerState::Undetected), Condvar::new()));
236        let child: Arc<Mutex<Option<Child>>> = Arc::new(Mutex::new(None));
237        let (transport_tx, transport_rx) = mpsc::channel();
238        let event_tx = self.event_tx.clone();
239        let state_clone = Arc::clone(&state);
240        let child_clone = Arc::clone(&child);
241        let ctx = StartupContext {
242            lang,
243            server_info,
244            root_path: root_path.clone(),
245            auto_install: self.config.auto_install,
246            overrides: ServerOverrides {
247                binary_name: self.config.binary_name_override.clone(),
248                binary_args: self.config.binary_args_override.clone(),
249                check_command: self.config.check_command_override.clone(),
250            },
251            startup_timeout: self.config.startup_timeout,
252            install_progress: self.install_progress.clone(),
253            install_lock: Arc::clone(&self.install_lock),
254        };
255
256        thread::spawn(move || {
257            startup_thread(ctx, state_clone, child_clone, transport_tx, event_tx);
258        });
259
260        self.servers.insert(
261            lang,
262            ServerInstance {
263                state,
264                server_info,
265                child,
266                transport: None,
267                startup_rx: Some(transport_rx),
268                opened_files: HashSet::new(),
269                root_path,
270            },
271        );
272    }
273
274    pub fn server_state(&self, lang: Language) -> ServerState {
275        self.servers
276            .get(&lang)
277            .map(|s| s.get_state())
278            .unwrap_or(ServerState::Undetected)
279    }
280
281    pub fn await_ready(&mut self, lang: Language, timeout: Duration) -> Result<ServerState> {
282        let state_arc = {
283            let server = self
284                .servers
285                .get(&lang)
286                .ok_or_else(|| anyhow::anyhow!("no server registered for {:?}", lang))?;
287            Arc::clone(&server.state)
288        };
289
290        let (lock, cvar) = &*state_arc;
291        let started = Instant::now();
292        let mut state = lock.lock().unwrap();
293
294        while !state.is_terminal() {
295            let elapsed = started.elapsed();
296            if elapsed >= timeout {
297                break;
298            }
299            let remaining = timeout - elapsed;
300            let (new_state, wait_result) = cvar.wait_timeout(state, remaining).unwrap();
301            state = new_state;
302            if wait_result.timed_out() {
303                break;
304            }
305        }
306        let final_state = *state;
307        drop(state);
308
309        if final_state == ServerState::Running {
310            self.try_recv_startup(lang);
311        }
312
313        Ok(final_state)
314    }
315
316    pub fn any_pending(&self) -> bool {
317        self.servers.values().any(|s| s.get_state().is_pending())
318    }
319
320    pub fn poll_events(&mut self) -> Vec<LspEvent> {
321        let mut events = Vec::new();
322        while let Ok(event) = self.event_rx.try_recv() {
323            events.push(event);
324        }
325        events
326    }
327
328    pub fn shutdown_all(&mut self) {
329        let languages: Vec<Language> = self.servers.keys().copied().collect();
330        for lang in languages {
331            self.shutdown_server(lang);
332        }
333    }
334
335    pub fn install_server(&mut self, lang: Language) -> Result<()> {
336        let event_tx = self.event_tx.clone();
337
338        let server = self
339            .servers
340            .get(&lang)
341            .ok_or_else(|| anyhow::anyhow!("no server registered for {:?}", lang))?;
342
343        let current = server.get_state();
344        if current != ServerState::Missing {
345            bail!(
346                "server for {:?} is not in Missing state (current: {:?})",
347                lang,
348                current
349            );
350        }
351
352        let server_info = server.server_info;
353        let state = Arc::clone(&server.state);
354
355        if try_transition(&state, lang, ServerState::Installing, &event_tx).is_none() {
356            bail!("could not transition to Installing");
357        }
358
359        match installer::install(server_info, self.install_progress.as_ref()) {
360            Ok(()) => {
361                try_transition(&state, lang, ServerState::Available, &event_tx);
362                Ok(())
363            }
364            Err(e) => {
365                try_transition(&state, lang, ServerState::Failed, &event_tx);
366                let _ = event_tx.send(LspEvent::Error {
367                    language: lang,
368                    error: e.to_string(),
369                });
370                Err(e)
371            }
372        }
373    }
374
375    /// Restart a server that has Failed (or Stopped). Tears down any lingering
376    /// process, then re-runs the full startup pipeline.
377    pub fn restart_server(&mut self, lang: Language) -> Result<()> {
378        let (server_info, root_path) = {
379            let server = self
380                .servers
381                .get(&lang)
382                .ok_or_else(|| anyhow::anyhow!("no server registered for {:?}", lang))?;
383            let s = server.get_state();
384            if !matches!(s, ServerState::Failed | ServerState::Stopped) {
385                bail!(
386                    "server for {:?} is not Failed or Stopped (current: {:?})",
387                    lang,
388                    s
389                );
390            }
391            (server.server_info, server.root_path.clone())
392        };
393        self.shutdown_server(lang);
394        self.servers.remove(&lang);
395        self.spawn_server(lang, server_info, root_path);
396        Ok(())
397    }
398
399    pub fn status_summary(&self) -> Vec<(Language, ServerState)> {
400        self.servers
401            .iter()
402            .map(|(lang, server)| (*lang, server.get_state()))
403            .collect()
404    }
405
406    // --- LSP Query Methods ---
407
408    pub fn document_symbols(&mut self, file_path: &Path) -> Result<Vec<DocumentSymbol>> {
409        #[cfg(any(test, feature = "test-support"))]
410        if let Some(syms) = self.test_symbols.get(file_path) {
411            return Ok(syms.clone());
412        }
413        let lang = self.language_for_file(file_path)?;
414        self.ensure_open(lang, file_path)?;
415        let uri = path_to_uri(file_path);
416        let params = json!({ "textDocument": { "uri": uri } });
417        let result = self.send_request(lang, "textDocument/documentSymbol", params)?;
418        parse_document_symbols(&result)
419    }
420
421    pub fn symbol_at_position(
422        &mut self,
423        file_path: &Path,
424        line: usize,
425        col: usize,
426    ) -> Result<Option<DocumentSymbol>> {
427        let symbols = self.document_symbols(file_path)?;
428        Ok(find_symbol_at_position(&symbols, line, col))
429    }
430
431    pub fn symbol_range(
432        &mut self,
433        file_path: &Path,
434        symbol_name: &str,
435    ) -> Result<Option<SymbolRange>> {
436        let symbols = self.document_symbols(file_path)?;
437        Ok(find_symbol_by_name(&symbols, symbol_name).map(|s| s.range))
438    }
439
440    pub fn notify_document_open(&mut self, file_path: &Path, content: &str) -> Result<()> {
441        let lang = self.language_for_file(file_path)?;
442        self.do_notify_open(lang, file_path, content)?;
443        if let Some(server) = self.servers.get_mut(&lang) {
444            server.opened_files.insert(file_path.to_path_buf());
445        }
446        Ok(())
447    }
448
449    pub fn notify_document_change(
450        &mut self,
451        file_path: &Path,
452        content: &str,
453        version: i32,
454    ) -> Result<()> {
455        let lang = self.language_for_file(file_path)?;
456        self.ensure_open(lang, file_path)?;
457        let uri = path_to_uri(file_path);
458        let params = json!({
459            "textDocument": { "uri": uri, "version": version },
460            "contentChanges": [{ "text": content }]
461        });
462        self.send_notification(lang, "textDocument/didChange", params)
463    }
464
465    pub fn completions(
466        &mut self,
467        file_path: &Path,
468        line: usize,
469        col: usize,
470    ) -> Result<Vec<CompletionItem>> {
471        let lang = self.language_for_file(file_path)?;
472        self.ensure_open(lang, file_path)?;
473        let uri = path_to_uri(file_path);
474        let params = json!({
475            "textDocument": { "uri": uri },
476            "position": { "line": line, "character": col }
477        });
478        let result = self.send_request(lang, "textDocument/completion", params)?;
479        parse_completions(&result)
480    }
481
482    pub fn hover(
483        &mut self,
484        file_path: &Path,
485        line: usize,
486        col: usize,
487    ) -> Result<Option<HoverInfo>> {
488        let lang = self.language_for_file(file_path)?;
489        self.ensure_open(lang, file_path)?;
490        let uri = path_to_uri(file_path);
491        let params = json!({
492            "textDocument": { "uri": uri },
493            "position": { "line": line, "character": col }
494        });
495        let result = self.send_request(lang, "textDocument/hover", params)?;
496        Ok(parse_hover(&result))
497    }
498
499    pub fn goto_definition(
500        &mut self,
501        file_path: &Path,
502        line: usize,
503        col: usize,
504    ) -> Result<Option<Location>> {
505        let lang = self.language_for_file(file_path)?;
506        self.ensure_open(lang, file_path)?;
507        let uri = path_to_uri(file_path);
508        let params = json!({
509            "textDocument": { "uri": uri },
510            "position": { "line": line, "character": col }
511        });
512        let result = self.send_request(lang, "textDocument/definition", params)?;
513        Ok(parse_location(&result))
514    }
515
516    pub fn semantic_tokens(
517        &mut self,
518        file_path: &Path,
519        content: &str,
520    ) -> Result<Vec<SemanticToken>> {
521        #[cfg(any(test, feature = "test-support"))]
522        if let Some(tokens) = self.test_semantic_tokens.get(file_path) {
523            if let Some(delay) = self.test_semantic_tokens_delay {
524                thread::sleep(delay);
525            }
526            return Ok(tokens.clone());
527        }
528        let lang = self.language_for_file(file_path)?;
529        self.ensure_open(lang, file_path)?;
530        let _ = self.notify_document_change(file_path, content, 2);
531        let uri = path_to_uri(file_path);
532        let params = json!({ "textDocument": { "uri": uri } });
533        let result = self.send_request(lang, "textDocument/semanticTokens/full", params)?;
534        parse_semantic_tokens(&result)
535    }
536
537    pub fn selection_range(
538        &mut self,
539        file_path: &Path,
540        line: usize,
541        col: usize,
542    ) -> Result<SelectionRange> {
543        #[cfg(any(test, feature = "test-support"))]
544        if let Some(sr) = self
545            .test_selection_ranges
546            .get(&(file_path.to_path_buf(), line, col))
547        {
548            return Ok(sr.clone());
549        }
550        let lang = self.language_for_file(file_path)?;
551        self.ensure_open(lang, file_path)?;
552        let uri = path_to_uri(file_path);
553        let params = json!({
554            "textDocument": { "uri": uri },
555            "positions": [{ "line": line, "character": col }]
556        });
557        let result = self.send_request(lang, "textDocument/selectionRange", params)?;
558        parse_selection_range(&result)
559    }
560
561    // --- Internal Methods ---
562
563    fn try_recv_startup(&mut self, lang: Language) {
564        if let Some(server) = self.servers.get_mut(&lang)
565            && let Some(ref rx) = server.startup_rx
566        {
567            match rx.try_recv() {
568                Ok(transport) => {
569                    server.transport = Some(transport);
570                    server.startup_rx = None;
571                }
572                Err(mpsc::TryRecvError::Disconnected) => {
573                    server.startup_rx = None;
574                }
575                Err(mpsc::TryRecvError::Empty) => {}
576            }
577        }
578    }
579
580    fn language_for_file(&self, file_path: &Path) -> Result<Language> {
581        registry::detect_language_from_path(file_path)
582            .ok_or_else(|| anyhow::anyhow!("no language detected for {}", file_path.display()))
583    }
584
585    /// Send a request, surfacing transport errors and translating crashes into
586    /// a `Failed` state transition + event before returning the error.
587    fn send_request(&mut self, lang: Language, method: &str, params: Value) -> Result<Value> {
588        self.try_recv_startup(lang);
589        let result = {
590            let transport = self.get_transport(lang)?;
591            transport.send_request(method, params)
592        };
593        if result.is_err() {
594            self.detect_and_report_crash(lang);
595        }
596        result
597    }
598
599    fn send_notification(&mut self, lang: Language, method: &str, params: Value) -> Result<()> {
600        self.try_recv_startup(lang);
601        let result = {
602            let transport = self.get_transport(lang)?;
603            transport.send_notification(method, params)
604        };
605        if result.is_err() {
606            self.detect_and_report_crash(lang);
607        }
608        result
609    }
610
611    fn get_transport(&mut self, lang: Language) -> Result<&mut LspTransport> {
612        // Quick liveness check first; drop the immutable borrow before reporting.
613        let exited = match self.servers.get(&lang) {
614            Some(server) => {
615                let mut child = server.child.lock().unwrap();
616                child
617                    .as_mut()
618                    .map(|c| matches!(health::check_process(c), HealthStatus::ProcessExited(_)))
619                    .unwrap_or(false)
620            }
621            None => bail!("no server for {:?}", lang),
622        };
623        if exited {
624            let current = self
625                .servers
626                .get(&lang)
627                .map(|s| s.get_state())
628                .unwrap_or(ServerState::Failed);
629            self.report_crash(lang, current, Some(None));
630            bail!("LSP server for {:?} has exited", lang);
631        }
632
633        let server = self
634            .servers
635            .get_mut(&lang)
636            .ok_or_else(|| anyhow::anyhow!("no server for {:?}", lang))?;
637        let current_state = server.get_state();
638        server.transport.as_mut().ok_or_else(|| {
639            anyhow::anyhow!(
640                "LSP server for {:?} is not running (state: {:?})",
641                lang,
642                current_state
643            )
644        })
645    }
646
647    /// Re-check process health after a transport error. If it's dead, transition
648    /// to Failed and emit events.
649    fn detect_and_report_crash(&mut self, lang: Language) {
650        let exited = {
651            if let Some(server) = self.servers.get(&lang) {
652                let mut child = server.child.lock().unwrap();
653                child
654                    .as_mut()
655                    .map(|c| match health::check_process(c) {
656                        HealthStatus::ProcessExited(code) => Some(code),
657                        HealthStatus::Healthy => None,
658                    })
659                    .unwrap_or(Some(None))
660            } else {
661                return;
662            }
663        };
664        if let Some(code) = exited {
665            let current = self.servers.get(&lang).map(|s| s.get_state());
666            if let Some(s) = current {
667                self.report_crash(lang, s, Some(code));
668            }
669        } else {
670            // Process still alive but transport call failed (e.g., serialization
671            // error). Treat as a fatal transport-level error: transition to Failed.
672            let current = self.servers.get(&lang).map(|s| s.get_state());
673            if let Some(s) = current {
674                self.report_crash(lang, s, None);
675            }
676        }
677    }
678
679    fn report_crash(&mut self, lang: Language, current: ServerState, code: Option<Option<i32>>) {
680        if let Some(server) = self.servers.get_mut(&lang) {
681            let state_arc = Arc::clone(&server.state);
682            let event_tx = self.event_tx.clone();
683            // Reset transport so subsequent calls clearly fail.
684            server.transport = None;
685            server.opened_files.clear();
686            // Reap the child so we don't leave a zombie.
687            if let Some(mut c) = server.child.lock().unwrap().take() {
688                let _ = c.wait();
689            }
690            if !current.is_terminal() || current == ServerState::Running {
691                try_transition(&state_arc, lang, ServerState::Failed, &event_tx);
692            }
693            let msg = match code {
694                Some(Some(c)) => format!("LSP process exited with code {}", c),
695                Some(None) => "LSP process exited (no status)".to_string(),
696                None => "LSP transport failure".to_string(),
697            };
698            let _ = event_tx.send(LspEvent::Error {
699                language: lang,
700                error: msg,
701            });
702        }
703    }
704
705    fn shutdown_server(&mut self, lang: Language) {
706        let server = match self.servers.get_mut(&lang) {
707            Some(s) => s,
708            None => return,
709        };
710        let state_arc = Arc::clone(&server.state);
711        let event_tx = self.event_tx.clone();
712
713        // Try a graceful shutdown handshake on a background thread, bounded by
714        // SHUTDOWN_GRACE. Take ownership of the transport so the worker thread
715        // can drive it without an aliased borrow.
716        if let Some(mut transport) = server.transport.take() {
717            let (done_tx, done_rx) = mpsc::channel::<()>();
718            let handle = thread::spawn(move || {
719                let _ = transport.send_request("shutdown", Value::Null);
720                let _ = transport.send_notification("exit", Value::Null);
721                let _ = done_tx.send(());
722            });
723            let _ = done_rx.recv_timeout(SHUTDOWN_GRACE);
724            // Whether the handshake finished or timed out, we proceed to kill
725            // the child. Detach the join handle if not done — the kill below
726            // will unblock any pending pipe I/O.
727            if handle.is_finished() {
728                let _ = handle.join();
729            }
730        }
731
732        // Take and kill the child process (works even mid-startup).
733        if let Some(mut child) = server.child.lock().unwrap().take() {
734            let _ = child.kill();
735            let _ = child.wait();
736        }
737        server.opened_files.clear();
738        server.startup_rx = None;
739
740        // Best-effort transition to Stopped from any state. If the validator
741        // rejects (already Stopped, etc.) we silently move on.
742        try_transition(&state_arc, lang, ServerState::Stopped, &event_tx);
743    }
744
745    fn ensure_open(&mut self, lang: Language, file_path: &Path) -> Result<()> {
746        let already_open = self
747            .servers
748            .get(&lang)
749            .map(|s| s.opened_files.contains(file_path))
750            .unwrap_or(false);
751        if already_open {
752            return Ok(());
753        }
754        let content = std::fs::read_to_string(file_path).unwrap_or_default();
755        self.do_notify_open(lang, file_path, &content)?;
756        if let Some(server) = self.servers.get_mut(&lang) {
757            server.opened_files.insert(file_path.to_path_buf());
758        }
759        Ok(())
760    }
761
762    fn do_notify_open(&mut self, lang: Language, file_path: &Path, content: &str) -> Result<()> {
763        let uri = path_to_uri(file_path);
764        let language_id = Language::language_id_for_path(file_path).unwrap_or(lang.name());
765        #[cfg(any(test, feature = "test-support"))]
766        self.did_open_log
767            .push((file_path.to_path_buf(), language_id.to_string()));
768        let params = json!({
769            "textDocument": {
770                "uri": uri,
771                "languageId": language_id,
772                "version": 1,
773                "text": content
774            }
775        });
776        self.send_notification(lang, "textDocument/didOpen", params)
777    }
778}
779
780impl Drop for LspEngine {
781    fn drop(&mut self) {
782        self.shutdown_all();
783    }
784}
785
786// --- Background Startup Thread ---
787
788fn startup_thread(
789    ctx: StartupContext,
790    state: Arc<(Mutex<ServerState>, Condvar)>,
791    child_slot: Arc<Mutex<Option<Child>>>,
792    transport_tx: mpsc::Sender<LspTransport>,
793    event_tx: mpsc::Sender<LspEvent>,
794) {
795    let lang = ctx.lang;
796    let server_info = ctx.server_info;
797
798    // Detect installation
799    let installed = if let Some(ref check_cmd) = ctx.overrides.check_command {
800        Command::new("sh")
801            .arg("-c")
802            .arg(check_cmd)
803            .output()
804            .map(|o| o.status.success())
805            .unwrap_or(false)
806    } else {
807        installer::is_installed(server_info)
808    };
809
810    if !installed {
811        if ctx.auto_install {
812            let _guard = ctx.install_lock.lock().unwrap();
813            // Re-check inside the lock: a prior install may have satisfied the dep.
814            if !installer::is_installed(server_info) {
815                try_transition(&state, lang, ServerState::Installing, &event_tx);
816                if let Err(e) = installer::install(server_info, ctx.install_progress.as_ref()) {
817                    try_transition(&state, lang, ServerState::Failed, &event_tx);
818                    let _ = event_tx.send(LspEvent::Error {
819                        language: lang,
820                        error: format!("install failed: {}", e),
821                    });
822                    return;
823                }
824            }
825            // _guard drops here, before spawning the server process.
826            drop(_guard);
827            try_transition(&state, lang, ServerState::Available, &event_tx);
828        } else {
829            try_transition(&state, lang, ServerState::Missing, &event_tx);
830            return;
831        }
832    } else {
833        try_transition(&state, lang, ServerState::Available, &event_tx);
834    }
835
836    try_transition(&state, lang, ServerState::Starting, &event_tx);
837
838    // Validate init options up-front so we surface a real error rather than
839    // silently sending an empty object.
840    let init_options: Value = if server_info.init_options_json.is_empty() {
841        Value::Null
842    } else {
843        match serde_json::from_str(server_info.init_options_json) {
844            Ok(v) => v,
845            Err(e) => {
846                try_transition(&state, lang, ServerState::Failed, &event_tx);
847                let _ = event_tx.send(LspEvent::Error {
848                    language: lang,
849                    error: format!(
850                        "invalid init_options_json for {}: {}",
851                        server_info.server_name, e
852                    ),
853                });
854                return;
855            }
856        }
857    };
858
859    let binary = ctx
860        .overrides
861        .binary_name
862        .as_deref()
863        .unwrap_or(server_info.binary_name);
864    let mut cmd = Command::new(binary);
865    if ctx.overrides.binary_name.is_some() {
866        cmd.args(&ctx.overrides.binary_args);
867    } else {
868        cmd.args(server_info.default_args);
869    }
870    cmd.stdin(Stdio::piped())
871        .stdout(Stdio::piped())
872        .stderr(Stdio::null());
873
874    let mut child = match cmd.spawn() {
875        Ok(child) => child,
876        Err(e) => {
877            try_transition(&state, lang, ServerState::Failed, &event_tx);
878            let _ = event_tx.send(LspEvent::Error {
879                language: lang,
880                error: format!("failed to spawn {}: {}", binary, e),
881            });
882            return;
883        }
884    };
885
886    let stdin = match child.stdin.take() {
887        Some(s) => s,
888        None => {
889            try_transition(&state, lang, ServerState::Failed, &event_tx);
890            let _ = child.kill();
891            let _ = child.wait();
892            return;
893        }
894    };
895    let stdout = match child.stdout.take() {
896        Some(s) => s,
897        None => {
898            try_transition(&state, lang, ServerState::Failed, &event_tx);
899            let _ = child.kill();
900            let _ = child.wait();
901            return;
902        }
903    };
904
905    // Stash the child immediately so Drop / shutdown / watchdog can kill it.
906    *child_slot.lock().unwrap() = Some(child);
907
908    // Watchdog: after startup_timeout, if state hasn't reached Running, kill
909    // the child. The handshake's blocking read will then return EOF.
910    let watchdog_state = Arc::clone(&state);
911    let watchdog_child = Arc::clone(&child_slot);
912    let timeout = ctx.startup_timeout;
913    let watchdog_event_tx = event_tx.clone();
914    thread::spawn(move || {
915        let deadline = Instant::now() + timeout;
916        let (lock, cvar) = &*watchdog_state;
917        let mut s = lock.lock().unwrap();
918        while !matches!(
919            *s,
920            ServerState::Running | ServerState::Stopped | ServerState::Failed
921        ) {
922            let now = Instant::now();
923            if now >= deadline {
924                break;
925            }
926            let (new_s, _) = cvar.wait_timeout(s, deadline - now).unwrap();
927            s = new_s;
928        }
929        let still_pending = !matches!(
930            *s,
931            ServerState::Running | ServerState::Stopped | ServerState::Failed
932        );
933        drop(s);
934        if still_pending {
935            if let Some(mut c) = watchdog_child.lock().unwrap().take() {
936                let _ = c.kill();
937                let _ = c.wait();
938            }
939            let _ = watchdog_event_tx.send(LspEvent::Error {
940                language: lang,
941                error: format!("startup timed out after {:?}", timeout),
942            });
943        }
944    });
945
946    let mut transport = LspTransport::new(stdin, stdout);
947
948    let root_uri = path_to_uri(&ctx.root_path);
949    let init_params = json!({
950        "processId": std::process::id(),
951        "rootUri": root_uri,
952        "capabilities": {
953            "textDocument": {
954                "documentSymbol": { "hierarchicalDocumentSymbolSupport": true }
955            }
956        },
957        "initializationOptions": init_options
958    });
959
960    if let Err(e) = transport.send_request("initialize", init_params) {
961        try_transition(&state, lang, ServerState::Failed, &event_tx);
962        if let Some(mut c) = child_slot.lock().unwrap().take() {
963            let _ = c.kill();
964            let _ = c.wait();
965        }
966        let _ = event_tx.send(LspEvent::Error {
967            language: lang,
968            error: format!("initialize handshake failed: {}", e),
969        });
970        return;
971    }
972
973    if let Err(e) = transport.send_notification("initialized", json!({})) {
974        try_transition(&state, lang, ServerState::Failed, &event_tx);
975        if let Some(mut c) = child_slot.lock().unwrap().take() {
976            let _ = c.kill();
977            let _ = c.wait();
978        }
979        let _ = event_tx.send(LspEvent::Error {
980            language: lang,
981            error: format!("initialized notification failed: {}", e),
982        });
983        return;
984    }
985
986    if transport_tx.send(transport).is_err() {
987        // Engine dropped the receiver (engine itself was dropped). Kill child.
988        if let Some(mut c) = child_slot.lock().unwrap().take() {
989            let _ = c.kill();
990            let _ = c.wait();
991        }
992        return;
993    }
994
995    try_transition(&state, lang, ServerState::Running, &event_tx);
996}
997
998// --- Utility Functions ---
999
1000fn path_to_uri(path: &Path) -> String {
1001    let abs = if path.is_absolute() {
1002        path.to_path_buf()
1003    } else {
1004        std::env::current_dir().unwrap_or_default().join(path)
1005    };
1006    let s = abs.to_string_lossy();
1007    let mut encoded = String::with_capacity(s.len() + 7);
1008    encoded.push_str("file://");
1009    for byte in s.bytes() {
1010        match byte {
1011            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' | b':' => {
1012                encoded.push(byte as char)
1013            }
1014            _ => {
1015                use std::fmt::Write;
1016                let _ = write!(&mut encoded, "%{:02X}", byte);
1017            }
1018        }
1019    }
1020    encoded
1021}
1022
1023// --- Response Parsing ---
1024
1025fn parse_document_symbols(value: &Value) -> Result<Vec<DocumentSymbol>> {
1026    match value {
1027        Value::Array(arr) => {
1028            let mut symbols = Vec::new();
1029            for item in arr {
1030                if let Some(sym) = parse_single_symbol(item) {
1031                    symbols.push(sym);
1032                }
1033            }
1034            Ok(symbols)
1035        }
1036        Value::Null => Ok(Vec::new()),
1037        _ => bail!("unexpected documentSymbol response format"),
1038    }
1039}
1040
1041fn parse_single_symbol(value: &Value) -> Option<DocumentSymbol> {
1042    let name = value.get("name")?.as_str()?.to_string();
1043    let kind_num = value.get("kind")?.as_u64()? as u32;
1044    let range = parse_range(value.get("range")?)?;
1045
1046    let children = value
1047        .get("children")
1048        .and_then(|c| c.as_array())
1049        .map(|arr| arr.iter().filter_map(parse_single_symbol).collect())
1050        .unwrap_or_default();
1051
1052    let selection_range = value.get("selectionRange").and_then(parse_range);
1053
1054    Some(DocumentSymbol {
1055        name,
1056        kind: convert_symbol_kind(kind_num),
1057        range,
1058        selection_range,
1059        children,
1060    })
1061}
1062
1063fn parse_range(value: &Value) -> Option<SymbolRange> {
1064    let start = value.get("start")?;
1065    let end = value.get("end")?;
1066    Some(SymbolRange {
1067        start_line: start.get("line")?.as_u64()? as usize,
1068        start_col: start.get("character")?.as_u64()? as usize,
1069        end_line: end.get("line")?.as_u64()? as usize,
1070        end_col: end.get("character")?.as_u64()? as usize,
1071    })
1072}
1073
1074fn convert_symbol_kind(kind: u32) -> SymbolKind {
1075    match kind {
1076        1 | 2 => SymbolKind::Module,
1077        5 => SymbolKind::Struct,
1078        6 => SymbolKind::Method,
1079        8 => SymbolKind::Field,
1080        10 => SymbolKind::Enum,
1081        12 => SymbolKind::Function,
1082        13 => SymbolKind::Variable,
1083        14 => SymbolKind::Const,
1084        23 => SymbolKind::Struct,
1085        _ => SymbolKind::Other(format!("kind_{}", kind)),
1086    }
1087}
1088
1089fn find_symbol_at_position(
1090    symbols: &[DocumentSymbol],
1091    line: usize,
1092    col: usize,
1093) -> Option<DocumentSymbol> {
1094    for sym in symbols {
1095        if contains_position(&sym.range, line, col) {
1096            if let Some(child) = find_symbol_at_position(&sym.children, line, col) {
1097                return Some(child);
1098            }
1099            return Some(sym.clone());
1100        }
1101    }
1102    None
1103}
1104
1105fn contains_position(range: &SymbolRange, line: usize, col: usize) -> bool {
1106    if line < range.start_line || line > range.end_line {
1107        return false;
1108    }
1109    if line == range.start_line && col < range.start_col {
1110        return false;
1111    }
1112    if line == range.end_line && col > range.end_col {
1113        return false;
1114    }
1115    true
1116}
1117
1118fn find_symbol_by_name<'a>(
1119    symbols: &'a [DocumentSymbol],
1120    name: &str,
1121) -> Option<&'a DocumentSymbol> {
1122    for sym in symbols {
1123        if sym.name == name {
1124            return Some(sym);
1125        }
1126        if let Some(found) = find_symbol_by_name(&sym.children, name) {
1127            return Some(found);
1128        }
1129    }
1130    None
1131}
1132
1133fn parse_completions(value: &Value) -> Result<Vec<CompletionItem>> {
1134    let items = if let Some(arr) = value.as_array() {
1135        arr
1136    } else if let Some(items) = value.get("items").and_then(|v| v.as_array()) {
1137        items
1138    } else if value.is_null() {
1139        return Ok(Vec::new());
1140    } else {
1141        bail!("unexpected completion response format");
1142    };
1143
1144    Ok(items
1145        .iter()
1146        .filter_map(|item| {
1147            let label = item.get("label")?.as_str()?.to_string();
1148            let detail = item
1149                .get("detail")
1150                .and_then(|d| d.as_str())
1151                .map(String::from);
1152            let kind = item
1153                .get("kind")
1154                .and_then(|k| k.as_u64())
1155                .map(|k| format!("{}", k));
1156            Some(CompletionItem {
1157                label,
1158                detail,
1159                kind,
1160            })
1161        })
1162        .collect())
1163}
1164
1165fn parse_hover(value: &Value) -> Option<HoverInfo> {
1166    if value.is_null() {
1167        return None;
1168    }
1169
1170    let contents = value.get("contents")?;
1171    let text = if let Some(s) = contents.as_str() {
1172        s.to_string()
1173    } else if let Some(obj) = contents.as_object() {
1174        obj.get("value")?.as_str()?.to_string()
1175    } else if let Some(arr) = contents.as_array() {
1176        arr.iter()
1177            .filter_map(|v| {
1178                v.as_str()
1179                    .map(String::from)
1180                    .or_else(|| v.get("value").and_then(|v| v.as_str()).map(String::from))
1181            })
1182            .collect::<Vec<_>>()
1183            .join("\n")
1184    } else {
1185        return None;
1186    };
1187
1188    Some(HoverInfo { contents: text })
1189}
1190
1191const STANDARD_TOKEN_TYPES: &[&str] = &[
1192    "namespace",
1193    "type",
1194    "class",
1195    "enum",
1196    "interface",
1197    "struct",
1198    "typeParameter",
1199    "parameter",
1200    "variable",
1201    "property",
1202    "enumMember",
1203    "event",
1204    "function",
1205    "method",
1206    "macro",
1207    "keyword",
1208    "modifier",
1209    "comment",
1210    "string",
1211    "number",
1212    "regexp",
1213    "operator",
1214    "decorator",
1215];
1216
1217fn parse_semantic_tokens(value: &Value) -> Result<Vec<SemanticToken>> {
1218    let data = match value.get("data").and_then(|d| d.as_array()) {
1219        Some(arr) => arr,
1220        None => return Ok(Vec::new()),
1221    };
1222
1223    let nums: Vec<usize> = data
1224        .iter()
1225        .filter_map(|v| v.as_u64().map(|n| n as usize))
1226        .collect();
1227
1228    if !nums.len().is_multiple_of(5) {
1229        return Ok(Vec::new());
1230    }
1231
1232    let mut tokens = Vec::new();
1233    let mut current_line: usize = 0;
1234    let mut current_col: usize = 0;
1235
1236    for chunk in nums.chunks(5) {
1237        let delta_line = chunk[0];
1238        let delta_start = chunk[1];
1239        let length = chunk[2];
1240        let token_type_idx = chunk[3];
1241
1242        if delta_line > 0 {
1243            current_line += delta_line;
1244            current_col = delta_start;
1245        } else {
1246            current_col += delta_start;
1247        }
1248
1249        let token_type = STANDARD_TOKEN_TYPES
1250            .get(token_type_idx)
1251            .unwrap_or(&"unknown")
1252            .to_string();
1253
1254        tokens.push(SemanticToken {
1255            line: current_line,
1256            start_col: current_col,
1257            length,
1258            token_type,
1259        });
1260    }
1261
1262    Ok(tokens)
1263}
1264
1265fn parse_location(value: &Value) -> Option<Location> {
1266    if value.is_null() {
1267        return None;
1268    }
1269
1270    let loc = if let Some(arr) = value.as_array() {
1271        arr.first()?
1272    } else {
1273        value
1274    };
1275
1276    let uri = loc.get("uri")?.as_str()?;
1277    let file_path = uri.strip_prefix("file://").map(PathBuf::from)?;
1278    let range = parse_range(loc.get("range")?)?;
1279
1280    Some(Location { file_path, range })
1281}
1282
1283fn parse_selection_range(value: &Value) -> Result<SelectionRange> {
1284    let item = if let Some(arr) = value.as_array() {
1285        arr.first()
1286            .ok_or_else(|| anyhow::anyhow!("empty selectionRange response"))?
1287    } else {
1288        value
1289    };
1290    parse_selection_range_node(item)
1291        .ok_or_else(|| anyhow::anyhow!("failed to parse selectionRange"))
1292}
1293
1294fn parse_selection_range_node(value: &Value) -> Option<SelectionRange> {
1295    let range = parse_range(value.get("range")?)?;
1296    let parent = value
1297        .get("parent")
1298        .filter(|v| !v.is_null())
1299        .and_then(|v| parse_selection_range_node(v).map(Box::new));
1300    Some(SelectionRange { range, parent })
1301}
1302
1303#[cfg(test)]
1304mod tests {
1305    use super::*;
1306    use std::time::Duration;
1307
1308    #[test]
1309    fn auto_install_true_by_default() {
1310        assert!(LspEngineConfig::default().auto_install);
1311    }
1312
1313    #[test]
1314    fn server_state_undetected_when_no_server_registered() {
1315        let engine = LspEngine::new(LspEngineConfig::default());
1316        assert_eq!(engine.server_state(Language::Rust), ServerState::Undetected);
1317    }
1318
1319    #[test]
1320    fn any_pending_false_with_no_servers() {
1321        let engine = LspEngine::new(LspEngineConfig::default());
1322        assert!(!engine.any_pending());
1323    }
1324
1325    #[test]
1326    fn await_ready_errors_for_unregistered_language() {
1327        let mut engine = LspEngine::new(LspEngineConfig::default());
1328        let result = engine.await_ready(Language::Rust, Duration::from_millis(100));
1329        assert!(result.is_err());
1330    }
1331
1332    #[test]
1333    fn install_server_errors_for_unregistered_language() {
1334        let mut engine = LspEngine::new(LspEngineConfig::default());
1335        let result = engine.install_server(Language::Rust);
1336        assert!(result.is_err());
1337        assert!(
1338            result
1339                .unwrap_err()
1340                .to_string()
1341                .contains("no server registered")
1342        );
1343    }
1344
1345    #[test]
1346    fn restart_server_errors_for_unregistered_language() {
1347        let mut engine = LspEngine::new(LspEngineConfig::default());
1348        let result = engine.restart_server(Language::Rust);
1349        assert!(result.is_err());
1350    }
1351
1352    #[test]
1353    fn parse_document_symbols_handles_null() {
1354        let result = parse_document_symbols(&serde_json::Value::Null).unwrap();
1355        assert!(result.is_empty());
1356    }
1357
1358    #[test]
1359    fn parse_document_symbols_handles_array() {
1360        let json = serde_json::json!([{
1361            "name": "my_fn",
1362            "kind": 12,
1363            "range": {
1364                "start": {"line": 0, "character": 0},
1365                "end": {"line": 5, "character": 1}
1366            },
1367            "children": []
1368        }]);
1369        let symbols = parse_document_symbols(&json).unwrap();
1370        assert_eq!(symbols.len(), 1);
1371        assert_eq!(symbols[0].name, "my_fn");
1372        assert_eq!(symbols[0].kind, SymbolKind::Function);
1373    }
1374
1375    #[test]
1376    fn parse_document_symbols_rejects_unexpected_format() {
1377        let result = parse_document_symbols(&serde_json::json!({"not": "an array"}));
1378        assert!(result.is_err());
1379    }
1380
1381    #[test]
1382    fn symbol_at_position_finds_innermost() {
1383        let outer = DocumentSymbol {
1384            name: "outer".to_string(),
1385            kind: SymbolKind::Function,
1386            range: SymbolRange {
1387                start_line: 0,
1388                start_col: 0,
1389                end_line: 10,
1390                end_col: 1,
1391            },
1392            selection_range: None,
1393            children: vec![DocumentSymbol {
1394                name: "inner".to_string(),
1395                kind: SymbolKind::Variable,
1396                range: SymbolRange {
1397                    start_line: 3,
1398                    start_col: 4,
1399                    end_line: 3,
1400                    end_col: 20,
1401                },
1402                selection_range: None,
1403                children: vec![],
1404            }],
1405        };
1406        let result = find_symbol_at_position(&[outer], 3, 10);
1407        assert_eq!(result.unwrap().name, "inner");
1408    }
1409
1410    #[test]
1411    fn symbol_at_position_returns_parent_when_no_child_matches() {
1412        let sym = DocumentSymbol {
1413            name: "parent".to_string(),
1414            kind: SymbolKind::Function,
1415            range: SymbolRange {
1416                start_line: 0,
1417                start_col: 0,
1418                end_line: 10,
1419                end_col: 1,
1420            },
1421            selection_range: None,
1422            children: vec![],
1423        };
1424        let result = find_symbol_at_position(&[sym], 5, 0);
1425        assert_eq!(result.unwrap().name, "parent");
1426    }
1427
1428    #[test]
1429    fn symbol_at_position_returns_none_outside_all_symbols() {
1430        let sym = DocumentSymbol {
1431            name: "fn1".to_string(),
1432            kind: SymbolKind::Function,
1433            range: SymbolRange {
1434                start_line: 0,
1435                start_col: 0,
1436                end_line: 5,
1437                end_col: 1,
1438            },
1439            selection_range: None,
1440            children: vec![],
1441        };
1442        let result = find_symbol_at_position(&[sym], 10, 0);
1443        assert!(result.is_none());
1444    }
1445
1446    #[test]
1447    fn convert_symbol_kind_maps_known_kinds() {
1448        assert_eq!(convert_symbol_kind(12), SymbolKind::Function);
1449        assert_eq!(convert_symbol_kind(5), SymbolKind::Struct);
1450        assert_eq!(convert_symbol_kind(13), SymbolKind::Variable);
1451        assert_eq!(convert_symbol_kind(10), SymbolKind::Enum);
1452        assert_eq!(convert_symbol_kind(6), SymbolKind::Method);
1453        assert_eq!(convert_symbol_kind(8), SymbolKind::Field);
1454        assert_eq!(convert_symbol_kind(14), SymbolKind::Const);
1455        assert!(matches!(convert_symbol_kind(99), SymbolKind::Other(_)));
1456    }
1457
1458    #[test]
1459    fn path_to_uri_encodes_spaces_and_specials() {
1460        let p = std::path::Path::new("/tmp/has space/foo#bar.rs");
1461        let uri = path_to_uri(p);
1462        assert!(uri.starts_with("file:///tmp/has%20space/foo%23bar.rs"));
1463    }
1464
1465    #[test]
1466    fn path_to_uri_passes_through_safe_chars() {
1467        let p = std::path::Path::new("/tmp/normal_path-1.rs");
1468        let uri = path_to_uri(p);
1469        assert_eq!(uri, "file:///tmp/normal_path-1.rs");
1470    }
1471
1472    #[test]
1473    fn do_notify_open_tsx_sends_typescriptreact_language_id() {
1474        let tmp = tempfile::tempdir().unwrap();
1475        let tsx_file = tmp.path().join("app.tsx");
1476        let mut engine = LspEngine::new(LspEngineConfig::default());
1477        // semantic_tokens triggers ensure_open → do_notify_open before failing (no server)
1478        let _ = engine.semantic_tokens(&tsx_file, "export default function App() {}");
1479        assert!(
1480            engine
1481                .did_open_log
1482                .iter()
1483                .any(|(p, id)| p == &tsx_file && id == "typescriptreact"),
1484            "did_open_log should contain typescriptreact for .tsx; got: {:?}",
1485            engine.did_open_log
1486        );
1487    }
1488
1489    #[test]
1490    fn install_lock_serializes_concurrent_installs() {
1491        use std::time::Instant;
1492
1493        let lock = Arc::new(Mutex::new(()));
1494        let log: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
1495
1496        let start_epoch = Instant::now();
1497        let threads: Vec<_> = (0..2)
1498            .map(|_| {
1499                let lock = Arc::clone(&lock);
1500                let log = Arc::clone(&log);
1501                let epoch = start_epoch;
1502                thread::spawn(move || {
1503                    let _guard = lock.lock().unwrap();
1504                    let start = epoch.elapsed().as_millis() as u64;
1505                    thread::sleep(Duration::from_millis(30));
1506                    let end = epoch.elapsed().as_millis() as u64;
1507                    log.lock().unwrap().push((start, end));
1508                })
1509            })
1510            .collect();
1511
1512        for t in threads {
1513            t.join().unwrap();
1514        }
1515
1516        let log = log.lock().unwrap();
1517        assert_eq!(log.len(), 2);
1518        let (s0, e0) = log[0];
1519        let (s1, e1) = log[1];
1520        let overlaps = s0 < e1 && s1 < e0;
1521        assert!(
1522            !overlaps,
1523            "install intervals overlap: [{s0},{e0}) and [{s1},{e1})"
1524        );
1525    }
1526}