1use std::collections::{HashMap, HashSet, VecDeque};
2use std::io::{self, BufRead, BufReader, BufWriter};
3use std::path::{Path, PathBuf};
4#[cfg(windows)]
5use std::process::Command;
6use std::process::{Child, Stdio};
7use std::str::FromStr;
8use std::sync::atomic::{AtomicI64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::{Duration, Instant};
12
13use crossbeam_channel::{bounded, RecvTimeoutError, Sender};
14use serde::de::DeserializeOwned;
15use serde_json::{json, Value};
16
17use crate::lsp::child_registry::LspChildRegistry;
18use crate::lsp::jsonrpc::{
19 Notification, Request, RequestId, Response as JsonRpcResponse, ServerMessage,
20};
21use crate::lsp::position::path_to_uri;
22use crate::lsp::registry::ServerKind;
23use crate::lsp::{transport, LspError};
24
25const INTERACTIVE_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
27const HANDSHAKE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
29const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
30const EXIT_POLL_INTERVAL: Duration = Duration::from_millis(25);
31const STDERR_TAIL_LINES: usize = 64;
32const STDERR_LINE_BYTES: usize = 4 * 1024;
33
34type PendingMap = HashMap<RequestId, Sender<JsonRpcResponse>>;
35type WatchedFileRegistrations = Arc<Mutex<HashSet<String>>>;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ServerState {
40 Starting,
41 Initializing,
42 Ready,
43 ShuttingDown,
44 Exited,
45}
46
47#[derive(Debug)]
49pub enum LspEvent {
50 Notification {
52 server_kind: ServerKind,
53 root: PathBuf,
54 method: String,
55 params: Option<Value>,
56 },
57 ServerRequest {
59 server_kind: ServerKind,
60 root: PathBuf,
61 id: RequestId,
62 method: String,
63 params: Option<Value>,
64 },
65 ServerExited {
67 server_kind: ServerKind,
68 root: PathBuf,
69 reason: ServerExitReason,
70 },
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ServerExitReason {
80 Eof,
82 ReadError(String),
86 PendingLockPoisoned,
88}
89
90impl std::fmt::Display for ServerExitReason {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Self::Eof => write!(f, "eof"),
94 Self::ReadError(err) => write!(f, "read error: {err}"),
95 Self::PendingLockPoisoned => write!(f, "pending lock poisoned"),
96 }
97 }
98}
99
100impl ServerExitReason {
101 pub(crate) fn from_read_result(
103 result: io::Result<Option<crate::lsp::jsonrpc::ServerMessage>>,
104 ) -> Self {
105 match result {
106 Ok(None) => Self::Eof,
107 Err(err) => Self::ReadError(err.to_string()),
108 Ok(Some(_)) => {
109 debug_assert!(false, "from_read_result called on a live message");
110 Self::ReadError("unexpected live message".to_string())
111 }
112 }
113 }
114}
115
116#[derive(Debug)]
118pub(crate) enum ReaderExitReap {
119 AlreadyExited(std::process::ExitStatus),
120 KilledWhileAlive,
121}
122
123#[derive(Debug, Clone, Default)]
131pub struct ServerDiagnosticCapabilities {
132 pub pull_diagnostics: bool,
134 pub workspace_diagnostics: bool,
136 pub identifier: Option<String>,
139 pub refresh_support: bool,
143}
144
145pub struct LspClient {
147 kind: ServerKind,
148 root: PathBuf,
149 state: ServerState,
150 child: Child,
151 child_pid: u32,
155 writer: Arc<Mutex<BufWriter<std::process::ChildStdin>>>,
156
157 pending: Arc<Mutex<PendingMap>>,
159 next_id: AtomicI64,
161 diagnostic_caps: Option<ServerDiagnosticCapabilities>,
165 rust_analyzer_quiescent: bool,
169 supports_watched_files: bool,
174 watched_file_registrations: WatchedFileRegistrations,
179 child_registry: LspChildRegistry,
183 stderr_tail: Arc<Mutex<VecDeque<String>>>,
184 #[cfg(test)]
187 suppress_kill_on_drop: bool,
188}
189
190impl LspClient {
191 pub fn spawn(
197 kind: ServerKind,
198 root: PathBuf,
199 binary: &Path,
200 args: &[String],
201 env: &HashMap<String, String>,
202 event_tx: Sender<LspEvent>,
203 child_registry: LspChildRegistry,
204 ) -> io::Result<Self> {
205 Self::spawn_with_reclaim_root(
206 kind,
207 root,
208 binary,
209 args,
210 env,
211 event_tx,
212 child_registry,
213 None,
214 )
215 }
216
217 pub(crate) fn spawn_with_reclaim_root(
219 kind: ServerKind,
220 root: PathBuf,
221 binary: &Path,
222 args: &[String],
223 env: &HashMap<String, String>,
224 event_tx: Sender<LspEvent>,
225 child_registry: LspChildRegistry,
226 reclaim_root: Option<&Path>,
227 ) -> io::Result<Self> {
228 #[cfg(windows)]
229 let is_batch_file = crate::windows_command::is_batch_file(binary);
230 #[cfg(windows)]
231 let mut command = if is_batch_file {
232 crate::windows_command::batch_command(binary, args.iter())?
233 } else {
234 Command::new(binary)
235 };
236 #[cfg(not(windows))]
237 let mut command = crate::effective_path::new_command(binary);
238 #[cfg(windows)]
239 if !is_batch_file {
240 command.args(args);
241 }
242 #[cfg(not(windows))]
243 command.args(args);
244 command
245 .current_dir(&root)
246 .stdin(Stdio::piped())
247 .stdout(Stdio::piped())
248 .stderr(Stdio::piped());
251 for (key, value) in env {
252 #[cfg(windows)]
253 if is_batch_file && crate::windows_command::is_batch_internal_env(key, args.len()) {
254 crate::slog_warn!(
255 "ignoring reserved batch-shim environment variable {key} for LSP server"
256 );
257 continue;
258 }
259 command.env(key, value);
260 }
261
262 #[cfg(unix)]
268 unsafe {
269 use std::os::unix::process::CommandExt;
270 command.pre_exec(|| {
271 #[cfg(target_os = "linux")]
272 {
273 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
279 return Err(io::Error::last_os_error());
280 }
281 if libc::getppid() == 1 {
282 return Err(io::Error::other("parent died before LSP spawn completed"));
283 }
284 }
285 if libc::setsid() == -1 {
286 return Err(io::Error::last_os_error());
287 }
288 Ok(())
289 });
290 }
291
292 let mut child = child_registry.spawn_tracked_child(
293 &mut command,
294 reclaim_root,
295 Some(&root),
296 Some(&kind),
297 )?;
298 let child_pid = child.id();
299
300 let stdout = child
301 .stdout
302 .take()
303 .ok_or_else(|| io::Error::other("language server missing stdout pipe"))?;
304 let stdin = child
305 .stdin
306 .take()
307 .ok_or_else(|| io::Error::other("language server missing stdin pipe"))?;
308 let stderr = child
309 .stderr
310 .take()
311 .ok_or_else(|| io::Error::other("language server missing stderr pipe"))?;
312 let stderr_tail = Arc::new(Mutex::new(VecDeque::with_capacity(STDERR_TAIL_LINES)));
313 spawn_stderr_drain_thread(stderr, Arc::clone(&stderr_tail));
314
315 let writer = Arc::new(Mutex::new(BufWriter::new(stdin)));
316 let pending = Arc::new(Mutex::new(PendingMap::new()));
317 let watched_file_registrations = Arc::new(Mutex::new(HashSet::new()));
318 let reader_pending = Arc::clone(&pending);
319 let reader_writer = Arc::clone(&writer);
320 let reader_watched_file_registrations = Arc::clone(&watched_file_registrations);
321 let reader_kind = kind.clone();
322 let reader_root = root.clone();
323
324 thread::spawn(move || {
325 let mut reader = BufReader::new(stdout);
326 loop {
327 match transport::read_message(&mut reader) {
328 Ok(Some(ServerMessage::Response(response))) => {
329 if let Ok(mut guard) = reader_pending.lock() {
330 if let Some(tx) = guard.remove(&response.id) {
331 if tx.send(response).is_err() {
332 log::debug!("response channel closed");
333 }
334 }
335 } else {
336 let _ = event_tx.send(LspEvent::ServerExited {
337 server_kind: reader_kind.clone(),
338 root: reader_root.clone(),
339 reason: ServerExitReason::PendingLockPoisoned,
340 });
341 break;
342 }
343 }
344 Ok(Some(ServerMessage::Notification { method, params })) => {
345 let _ = event_tx.send(LspEvent::Notification {
346 server_kind: reader_kind.clone(),
347 root: reader_root.clone(),
348 method,
349 params,
350 });
351 }
352 Ok(Some(ServerMessage::Request { id, method, params })) => {
353 record_watched_file_registration(
354 &reader_watched_file_registrations,
355 &method,
356 params.as_ref(),
357 );
358 let response_value = if method == "workspace/configuration" {
368 workspace_configuration_response(
369 &reader_kind,
370 &reader_root,
371 params.as_ref(),
372 )
373 } else {
374 serde_json::Value::Null
375 };
376 if let Ok(mut w) = reader_writer.lock() {
377 let response = super::jsonrpc::OutgoingResponse::success(
378 id.clone(),
379 response_value,
380 );
381 let _ = transport::write_response(&mut *w, &response);
382 }
383 let _ = event_tx.send(LspEvent::ServerRequest {
385 server_kind: reader_kind.clone(),
386 root: reader_root.clone(),
387 id,
388 method,
389 params,
390 });
391 }
392 terminal @ (Ok(None) | Err(_)) => {
393 if let Ok(mut guard) = reader_pending.lock() {
394 guard.clear();
395 }
396 let _ = event_tx.send(LspEvent::ServerExited {
397 server_kind: reader_kind.clone(),
398 root: reader_root.clone(),
399 reason: ServerExitReason::from_read_result(terminal),
400 });
401 break;
402 }
403 }
404 }
405 });
406
407 let rust_analyzer_quiescent = !matches!(&kind, ServerKind::Rust);
408 child_registry.mark_client_live(child_pid);
409 Ok(Self {
410 kind,
411 root,
412 state: ServerState::Starting,
413 child,
414 child_pid,
415 writer,
416 pending,
417 next_id: AtomicI64::new(1),
418 diagnostic_caps: None,
419 rust_analyzer_quiescent,
420 supports_watched_files: false,
421 watched_file_registrations,
422 child_registry,
423 stderr_tail,
424 #[cfg(test)]
425 suppress_kill_on_drop: false,
426 })
427 }
428
429 pub fn initialize(
431 &mut self,
432 workspace_root: &Path,
433 initialization_options: Option<serde_json::Value>,
434 ) -> Result<lsp_types::InitializeResult, LspError> {
435 self.initialize_with_timeout(
436 workspace_root,
437 initialization_options,
438 HANDSHAKE_REQUEST_TIMEOUT,
439 )
440 }
441
442 pub(crate) fn initialize_with_timeout(
445 &mut self,
446 workspace_root: &Path,
447 initialization_options: Option<serde_json::Value>,
448 timeout: Duration,
449 ) -> Result<lsp_types::InitializeResult, LspError> {
450 self.ensure_can_send()?;
451 self.state = ServerState::Initializing;
452
453 let root_url = path_to_uri(workspace_root)?;
454 let root_uri = lsp_types::Uri::from_str(root_url.as_str()).map_err(|_| {
455 LspError::NotFound(format!(
456 "failed to convert workspace root '{}' to file URI",
457 workspace_root.display()
458 ))
459 })?;
460
461 let mut params_value = json!({
462 "processId": std::process::id(),
463 "rootUri": root_uri,
464 "capabilities": {
465 "experimental": {
466 "serverStatusNotification": true
467 },
468 "workspace": {
469 "workspaceFolders": true,
470 "configuration": true,
471 "didChangeWatchedFiles": {
472 "dynamicRegistration": true
473 },
474 "diagnostic": {
479 "refreshSupport": false
480 }
481 },
482 "textDocument": {
483 "synchronization": {
484 "dynamicRegistration": false,
485 "didSave": true,
486 "willSave": false,
487 "willSaveWaitUntil": false
488 },
489 "publishDiagnostics": {
490 "relatedInformation": true,
491 "versionSupport": true,
492 "codeDescriptionSupport": true,
493 "dataSupport": true
494 },
495 "diagnostic": {
500 "dynamicRegistration": false,
501 "relatedDocumentSupport": true
502 }
503 }
504 },
505 "clientInfo": {
506 "name": "aft",
507 "version": env!("CARGO_PKG_VERSION")
508 },
509 "workspaceFolders": [
510 {
511 "uri": root_uri,
512 "name": workspace_root
513 .file_name()
514 .and_then(|name| name.to_str())
515 .unwrap_or("workspace")
516 }
517 ]
518 });
519 if let Some(initialization_options) = initialization_options {
520 params_value["initializationOptions"] = initialization_options;
521 }
522
523 let params = serde_json::from_value::<lsp_types::InitializeParams>(params_value)?;
524
525 let result_value = self.send_request_value_with_timeout(
526 <lsp_types::request::Initialize as lsp_types::request::Request>::METHOD,
527 params,
528 timeout.min(HANDSHAKE_REQUEST_TIMEOUT),
529 )?;
530 let result: lsp_types::InitializeResult = serde_json::from_value(result_value.clone())?;
531
532 let caps_value = result_value
537 .get("capabilities")
538 .cloned()
539 .unwrap_or_else(|| serde_json::to_value(&result.capabilities).unwrap_or(Value::Null));
540 self.diagnostic_caps = Some(parse_diagnostic_capabilities(&caps_value));
541
542 self.supports_watched_files = caps_value
548 .pointer("/workspace/didChangeWatchedFiles/dynamicRegistration")
549 .and_then(|v| v.as_bool())
550 .unwrap_or(false)
551 || caps_value
552 .pointer("/workspace/didChangeWatchedFiles")
553 .map(|v| v.is_object() || v.as_bool() == Some(true))
554 .unwrap_or(false);
555
556 self.send_notification::<lsp_types::notification::Initialized>(serde_json::from_value(
557 json!({}),
558 )?)?;
559 self.state = ServerState::Ready;
560 Ok(result)
561 }
562
563 pub fn diagnostic_capabilities(&self) -> Option<&ServerDiagnosticCapabilities> {
567 self.diagnostic_caps.as_ref()
568 }
569
570 pub fn diagnostics_are_provisional(&self) -> bool {
573 matches!(&self.kind, ServerKind::Rust) && !self.rust_analyzer_quiescent
574 }
575
576 pub fn set_rust_analyzer_quiescent(&mut self, quiescent: bool) -> bool {
580 if !matches!(&self.kind, ServerKind::Rust) || !quiescent || self.rust_analyzer_quiescent {
581 return false;
582 }
583 self.rust_analyzer_quiescent = true;
584 true
585 }
586
587 pub fn supports_watched_files(&self) -> bool {
591 self.supports_watched_files
592 }
593
594 pub fn has_watched_file_registration(&self) -> bool {
598 self.watched_file_registrations
599 .lock()
600 .map(|registrations| !registrations.is_empty())
601 .unwrap_or(false)
602 }
603
604 pub fn send_request<R>(&mut self, params: R::Params) -> Result<R::Result, LspError>
606 where
607 R: lsp_types::request::Request,
608 R::Params: serde::Serialize,
609 R::Result: DeserializeOwned,
610 {
611 self.ensure_can_send()?;
612
613 let value = self.send_request_value(R::METHOD, params)?;
614 serde_json::from_value(value).map_err(Into::into)
615 }
616
617 pub fn send_request_with_timeout<R>(
621 &mut self,
622 params: R::Params,
623 timeout: Duration,
624 ) -> Result<R::Result, LspError>
625 where
626 R: lsp_types::request::Request,
627 R::Params: serde::Serialize,
628 R::Result: DeserializeOwned,
629 {
630 self.ensure_can_send()?;
631
632 let value = self.send_request_value_with_timeout(R::METHOD, params, timeout)?;
633 serde_json::from_value(value).map_err(Into::into)
634 }
635
636 fn send_request_value<P>(&mut self, method: &'static str, params: P) -> Result<Value, LspError>
637 where
638 P: serde::Serialize,
639 {
640 self.send_request_value_with_timeout(method, params, INTERACTIVE_REQUEST_TIMEOUT)
641 }
642
643 fn send_request_value_with_timeout<P>(
644 &mut self,
645 method: &'static str,
646 params: P,
647 timeout: Duration,
648 ) -> Result<Value, LspError>
649 where
650 P: serde::Serialize,
651 {
652 self.ensure_can_send()?;
653
654 let id = RequestId::Int(self.next_id.fetch_add(1, Ordering::Relaxed));
655 let (tx, rx) = bounded(1);
656 {
657 let mut pending = self.lock_pending()?;
658 pending.insert(id.clone(), tx);
659 }
660
661 let request = Request::new(id.clone(), method, Some(serde_json::to_value(params)?));
662 {
663 let mut writer = self
664 .writer
665 .lock()
666 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
667 if let Err(err) = transport::write_request(&mut *writer, &request) {
668 self.remove_pending(&id);
669 return Err(err.into());
670 }
671 }
672
673 let response = match rx.recv_timeout(timeout) {
674 Ok(response) => response,
675 Err(RecvTimeoutError::Timeout) => {
676 self.remove_pending(&id);
677 self.send_cancel_request(&id)?;
678 return Err(LspError::Timeout(format!(
679 "timed out waiting for '{}' response from {:?}",
680 method, self.kind
681 )));
682 }
683 Err(RecvTimeoutError::Disconnected) => {
684 self.remove_pending(&id);
685 return Err(LspError::ServerNotReady(format!(
686 "language server {:?} disconnected while waiting for '{}'",
687 self.kind, method
688 )));
689 }
690 };
691
692 if let Some(error) = response.error {
693 return Err(LspError::ServerError {
694 code: error.code,
695 message: error.message,
696 });
697 }
698
699 Ok(response.result.unwrap_or(Value::Null))
700 }
701
702 pub fn send_notification<N>(&mut self, params: N::Params) -> Result<(), LspError>
704 where
705 N: lsp_types::notification::Notification,
706 N::Params: serde::Serialize,
707 {
708 self.ensure_can_send()?;
709 let notification = Notification::new(N::METHOD, Some(serde_json::to_value(params)?));
710 let mut writer = self
711 .writer
712 .lock()
713 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
714 transport::write_notification(&mut *writer, ¬ification)?;
715 Ok(())
716 }
717
718 pub fn shutdown(&mut self) -> Result<(), LspError> {
720 self.shutdown_with_request_timeout(HANDSHAKE_REQUEST_TIMEOUT)
721 }
722
723 pub(crate) fn shutdown_for_idle_reap(&mut self) -> Result<(), LspError> {
727 self.shutdown_with_request_timeout(EXIT_POLL_INTERVAL)
728 }
729
730 fn shutdown_with_request_timeout(&mut self, request_timeout: Duration) -> Result<(), LspError> {
731 if self.state == ServerState::Exited {
732 self.child_registry.untrack(self.child_pid);
733 return Ok(());
734 }
735
736 if self.child.try_wait()?.is_some() {
737 self.state = ServerState::Exited;
738 self.child_registry.untrack(self.child_pid);
739 return Ok(());
740 }
741
742 if let Err(err) =
743 self.send_request_with_timeout::<lsp_types::request::Shutdown>((), request_timeout)
744 {
745 self.state = ServerState::ShuttingDown;
746 return self.abort_live_child_after_shutdown_error(err);
747 }
748
749 if let Err(err) = self.send_notification::<lsp_types::notification::Exit>(()) {
750 return self.abort_live_child_after_shutdown_error(err);
751 }
752 self.state = ServerState::ShuttingDown;
753
754 let deadline = Instant::now() + SHUTDOWN_TIMEOUT;
755 loop {
756 if self.child.try_wait()?.is_some() {
757 self.state = ServerState::Exited;
758 return Ok(());
759 }
760 if Instant::now() >= deadline {
761 kill_lsp_child_group(&mut self.child);
765 self.state = ServerState::Exited;
766 self.child_registry.untrack(self.child_pid);
767 return Err(LspError::Timeout(format!(
768 "timed out waiting for {:?} to exit",
769 self.kind
770 )));
771 }
772 thread::sleep(EXIT_POLL_INTERVAL);
773 }
774 }
775
776 pub fn stderr_tail(&self) -> String {
777 self.stderr_tail
778 .lock()
779 .map(|tail| stderr_tail_to_string(&tail))
780 .unwrap_or_default()
781 }
782
783 pub fn child_exited(&mut self) -> bool {
784 self.child.try_wait().ok().flatten().is_some()
785 }
786
787 pub fn child_exit_status(&mut self) -> Option<std::process::ExitStatus> {
788 self.child.try_wait().ok().flatten()
789 }
790
791 pub(crate) fn child_pid(&self) -> u32 {
792 self.child_pid
793 }
794
795 pub(crate) fn reap_after_reader_exit(&mut self, _reason: &ServerExitReason) -> ReaderExitReap {
799 let outcome = match self.child.try_wait() {
800 Ok(Some(status)) => ReaderExitReap::AlreadyExited(status),
801 Ok(None) | Err(_) => {
802 kill_lsp_child_group(&mut self.child);
803 self.wait_for_child_exit_bounded();
804 ReaderExitReap::KilledWhileAlive
805 }
806 };
807 self.state = ServerState::Exited;
808 self.child_registry.untrack(self.child_pid);
809 outcome
810 }
811
812 fn abort_live_child_after_shutdown_error(&mut self, err: LspError) -> Result<(), LspError> {
813 if self.child.try_wait()?.is_some() {
814 self.state = ServerState::Exited;
815 self.child_registry.untrack(self.child_pid);
816 return Ok(());
817 }
818 kill_lsp_child_group(&mut self.child);
819 self.wait_for_child_exit_bounded();
820 self.state = ServerState::Exited;
821 self.child_registry.untrack(self.child_pid);
822 Err(err)
823 }
824
825 fn wait_for_child_exit_bounded(&mut self) {
826 let deadline = Instant::now() + SHUTDOWN_TIMEOUT;
827 loop {
828 if self.child.try_wait().ok().flatten().is_some() {
829 return;
830 }
831 if Instant::now() >= deadline {
832 return;
833 }
834 thread::sleep(EXIT_POLL_INTERVAL);
835 }
836 }
837
838 #[cfg(all(test, unix))]
840 pub(crate) fn suppress_kill_on_drop_for_test(&mut self) {
841 self.suppress_kill_on_drop = true;
842 }
843
844 #[cfg(all(test, unix))]
846 pub(crate) fn poison_writer_for_test(&self) {
847 let writer = Arc::clone(&self.writer);
848 let _ = thread::spawn(move || {
849 let _guard = writer.lock().expect("writer lock");
850 panic!("poison lsp writer for test");
851 })
852 .join();
853 }
854
855 pub fn state(&self) -> ServerState {
856 self.state
857 }
858
859 pub fn kind(&self) -> ServerKind {
860 self.kind.clone()
861 }
862
863 pub fn root(&self) -> &Path {
864 &self.root
865 }
866
867 fn ensure_can_send(&self) -> Result<(), LspError> {
868 if matches!(self.state, ServerState::ShuttingDown | ServerState::Exited) {
869 return Err(LspError::ServerNotReady(format!(
870 "language server {:?} is not ready (state: {:?})",
871 self.kind, self.state
872 )));
873 }
874 Ok(())
875 }
876
877 fn lock_pending(&self) -> Result<std::sync::MutexGuard<'_, PendingMap>, LspError> {
878 self.pending
879 .lock()
880 .map_err(|_| io::Error::other("pending response map poisoned").into())
881 }
882
883 fn remove_pending(&self, id: &RequestId) {
884 if let Ok(mut pending) = self.pending.lock() {
885 pending.remove(id);
886 }
887 }
888
889 fn send_cancel_request(&mut self, id: &RequestId) -> Result<(), LspError> {
890 let notification = Notification::new("$/cancelRequest", Some(json!({ "id": id })));
891 let mut writer = self
892 .writer
893 .lock()
894 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
895 transport::write_notification(&mut *writer, ¬ification)?;
896 Ok(())
897 }
898}
899
900impl Drop for LspClient {
901 fn drop(&mut self) {
902 #[cfg(test)]
903 if self.suppress_kill_on_drop {
904 self.child_registry.mark_client_gone(self.child_pid);
908 return;
909 }
910 self.child_registry.mark_client_gone(self.child_pid);
914 self.child_registry.untrack(self.child_pid);
917 kill_lsp_child_group(&mut self.child);
918 }
919}
920
921fn spawn_stderr_drain_thread(
922 stderr: std::process::ChildStderr,
923 stderr_tail: Arc<Mutex<VecDeque<String>>>,
924) {
925 thread::spawn(move || {
926 let mut reader = BufReader::new(stderr);
927 let mut line = String::new();
928
929 loop {
930 line.clear();
931 match reader.read_line(&mut line) {
932 Ok(0) => break,
933 Ok(_) => {
934 if let Ok(mut tail) = stderr_tail.lock() {
935 append_stderr_tail(&mut tail, &line);
936 } else {
937 break;
938 }
939 }
940 Err(_) => break,
941 }
942 }
943 });
944}
945
946fn append_stderr_tail(tail: &mut VecDeque<String>, line: &str) {
947 if tail.len() == STDERR_TAIL_LINES {
948 tail.pop_front();
949 }
950 tail.push_back(trim_stderr_line(line));
951}
952
953fn trim_stderr_line(line: &str) -> String {
954 let line = line.trim_end_matches(|ch| ch == '\r' || ch == '\n');
955 if line.len() <= STDERR_LINE_BYTES {
956 return line.to_string();
957 }
958
959 let mut start = line.len() - STDERR_LINE_BYTES;
960 while start < line.len() && !line.is_char_boundary(start) {
961 start += 1;
962 }
963 format!("...{}", &line[start..])
964}
965
966fn stderr_tail_to_string(tail: &VecDeque<String>) -> String {
967 tail.iter()
968 .map(String::as_str)
969 .collect::<Vec<_>>()
970 .join("\n")
971}
972
973fn kill_lsp_child_group(child: &mut std::process::Child) {
980 #[cfg(unix)]
981 {
982 let pgid = child.id() as i32;
983 crate::bash_background::process::terminate_pgid(pgid, Some(child));
984 let _ = child.wait();
985 }
986 #[cfg(not(unix))]
987 {
988 crate::bash_background::process::terminate_process(child);
989 let _ = child.wait();
990 }
991}
992
993fn record_watched_file_registration(
994 registrations: &WatchedFileRegistrations,
995 method: &str,
996 params: Option<&Value>,
997) {
998 match method {
999 "client/registerCapability" => {
1000 let Some(items) = params
1001 .and_then(|params| params.get("registrations"))
1002 .and_then(|registrations| registrations.as_array())
1003 else {
1004 return;
1005 };
1006 if let Ok(mut guard) = registrations.lock() {
1007 for item in items {
1008 if item.get("method").and_then(Value::as_str)
1009 == Some("workspace/didChangeWatchedFiles")
1010 {
1011 if let Some(id) = item.get("id").and_then(Value::as_str) {
1012 guard.insert(id.to_string());
1013 }
1014 }
1015 }
1016 }
1017 }
1018 "client/unregisterCapability" => {
1019 let Some(items) = params
1020 .and_then(|params| params.get("unregisterations"))
1021 .and_then(|registrations| registrations.as_array())
1022 else {
1023 return;
1024 };
1025 if let Ok(mut guard) = registrations.lock() {
1026 for item in items {
1027 if item.get("method").and_then(Value::as_str)
1028 == Some("workspace/didChangeWatchedFiles")
1029 {
1030 if let Some(id) = item.get("id").and_then(Value::as_str) {
1031 guard.remove(id);
1032 }
1033 }
1034 }
1035 }
1036 }
1037 _ => {}
1038 }
1039}
1040
1041fn workspace_configuration_response(
1042 kind: &ServerKind,
1043 root: &Path,
1044 params: Option<&Value>,
1045) -> Value {
1046 let items = params
1047 .and_then(|params| params.get("items"))
1048 .and_then(Value::as_array);
1049 let python_path = (kind == &ServerKind::Python)
1050 .then(|| project_python_path(root))
1051 .flatten()
1052 .map(|path| path.to_string_lossy().into_owned());
1056
1057 Value::Array(match items {
1058 Some(items) => items
1059 .iter()
1060 .map(|item| {
1061 if item.get("section").and_then(Value::as_str) == Some("python") {
1062 if let Some(path) = &python_path {
1063 return json!({ "pythonPath": path });
1064 }
1065 }
1066 Value::Null
1067 })
1068 .collect(),
1069 None => vec![Value::Null],
1070 })
1071}
1072
1073fn project_python_path(root: &Path) -> Option<PathBuf> {
1074 [root.join(".venv"), root.join("venv")]
1075 .into_iter()
1076 .find_map(|virtualenv| {
1077 if cfg!(windows) {
1078 let candidate = virtualenv.join("Scripts").join("python.exe");
1079 return candidate.is_file().then_some(candidate);
1080 }
1081
1082 ["python", "python3"]
1083 .into_iter()
1084 .map(|binary| virtualenv.join("bin").join(binary))
1085 .find(|candidate| candidate.is_file())
1086 })
1087}
1088
1089fn parse_diagnostic_capabilities(value: &Value) -> ServerDiagnosticCapabilities {
1104 let mut caps = ServerDiagnosticCapabilities::default();
1105
1106 if let Some(provider) = value.get("diagnosticProvider") {
1107 if provider.is_object() || provider.as_bool() == Some(true) {
1110 caps.pull_diagnostics = true;
1111 }
1112
1113 if let Some(obj) = provider.as_object() {
1114 if obj
1115 .get("workspaceDiagnostics")
1116 .and_then(|v| v.as_bool())
1117 .unwrap_or(false)
1118 {
1119 caps.workspace_diagnostics = true;
1120 }
1121 if let Some(identifier) = obj.get("identifier").and_then(|v| v.as_str()) {
1122 caps.identifier = Some(identifier.to_string());
1123 }
1124 }
1125 }
1126
1127 if let Some(refresh) = value
1130 .get("workspace")
1131 .and_then(|w| w.get("diagnostic"))
1132 .and_then(|d| d.get("refreshSupport"))
1133 .and_then(|r| r.as_bool())
1134 {
1135 caps.refresh_support = refresh;
1136 }
1137
1138 caps
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144 #[cfg(unix)]
1146 use std::collections::HashMap;
1147 use std::io::{BufReader, Cursor};
1148 #[cfg(unix)]
1149 use std::path::{Path, PathBuf};
1150
1151 #[test]
1152 fn parse_caps_no_diagnostic_provider() {
1153 let value = json!({});
1154 let caps = parse_diagnostic_capabilities(&value);
1155 assert!(!caps.pull_diagnostics);
1156 assert!(!caps.workspace_diagnostics);
1157 assert!(caps.identifier.is_none());
1158 }
1159
1160 #[test]
1161 fn parse_caps_basic_pull_only() {
1162 let value = json!({
1163 "diagnosticProvider": {
1164 "interFileDependencies": false,
1165 "workspaceDiagnostics": false
1166 }
1167 });
1168 let caps = parse_diagnostic_capabilities(&value);
1169 assert!(caps.pull_diagnostics);
1170 assert!(!caps.workspace_diagnostics);
1171 }
1172
1173 #[test]
1174 fn parse_caps_full_pull_with_workspace() {
1175 let value = json!({
1176 "diagnosticProvider": {
1177 "interFileDependencies": true,
1178 "workspaceDiagnostics": true,
1179 "identifier": "rust-analyzer"
1180 }
1181 });
1182 let caps = parse_diagnostic_capabilities(&value);
1183 assert!(caps.pull_diagnostics);
1184 assert!(caps.workspace_diagnostics);
1185 assert_eq!(caps.identifier.as_deref(), Some("rust-analyzer"));
1186 }
1187
1188 #[test]
1189 fn parse_caps_provider_as_bare_true() {
1190 let value = json!({
1192 "diagnosticProvider": true
1193 });
1194 let caps = parse_diagnostic_capabilities(&value);
1195 assert!(caps.pull_diagnostics);
1196 assert!(!caps.workspace_diagnostics);
1197 }
1198
1199 #[test]
1200 fn interactive_request_timeout_is_eight_seconds() {
1201 assert_eq!(INTERACTIVE_REQUEST_TIMEOUT, Duration::from_secs(8));
1202 }
1203
1204 #[test]
1205 fn handshake_request_timeout_remains_thirty_seconds() {
1206 assert_eq!(HANDSHAKE_REQUEST_TIMEOUT, Duration::from_secs(30));
1207 }
1208
1209 #[test]
1210 fn parse_caps_workspace_refresh_support() {
1211 let value = json!({
1212 "workspace": {
1213 "diagnostic": {
1214 "refreshSupport": true
1215 }
1216 }
1217 });
1218 let caps = parse_diagnostic_capabilities(&value);
1219 assert!(caps.refresh_support);
1220 assert!(!caps.pull_diagnostics);
1222 }
1223
1224 #[test]
1225 fn pyright_configuration_uses_workspace_virtualenv_interpreter() {
1226 let tmp = tempfile::tempdir().unwrap();
1227 let root = tmp.path();
1228 let python = if cfg!(windows) {
1229 root.join(".venv").join("Scripts").join("python.exe")
1230 } else {
1231 root.join(".venv").join("bin").join("python")
1232 };
1233 std::fs::create_dir_all(python.parent().unwrap()).unwrap();
1234 std::fs::write(&python, []).unwrap();
1235 let params = json!({
1236 "items": [
1237 { "section": "python" },
1238 { "section": "pyright" }
1239 ]
1240 });
1241
1242 let response = workspace_configuration_response(&ServerKind::Python, root, Some(¶ms));
1243
1244 assert_eq!(response[0]["pythonPath"], python.display().to_string());
1245 assert!(response[1].is_null());
1246 }
1247
1248 #[test]
1249 fn ty_configuration_does_not_receive_pyright_interpreter_settings() {
1250 let tmp = tempfile::tempdir().unwrap();
1251 let root = tmp.path();
1252 let python = if cfg!(windows) {
1253 root.join(".venv").join("Scripts").join("python.exe")
1254 } else {
1255 root.join(".venv").join("bin").join("python")
1256 };
1257 std::fs::create_dir_all(python.parent().unwrap()).unwrap();
1258 std::fs::write(python, []).unwrap();
1259 let params = json!({ "items": [{ "section": "python" }] });
1260
1261 let response = workspace_configuration_response(&ServerKind::Ty, root, Some(¶ms));
1262
1263 assert!(response[0].is_null());
1264 }
1265
1266 #[test]
1267 fn eof_read_maps_to_eof_reason() {
1268 let mut reader = BufReader::new(Cursor::new([]));
1269 let result = transport::read_message(&mut reader);
1270 assert!(matches!(result, Ok(None)));
1271 assert_eq!(
1272 ServerExitReason::from_read_result(result),
1273 ServerExitReason::Eof
1274 );
1275 }
1276
1277 #[test]
1278 fn malformed_frame_maps_to_read_error_reason() {
1279 let mut reader = BufReader::new(Cursor::new(b"Content-Length: 3\r\n\r\n{{{"));
1280 let result = transport::read_message(&mut reader);
1281 assert!(result.is_err());
1282 match ServerExitReason::from_read_result(result) {
1283 ServerExitReason::ReadError(message) => assert!(
1284 !message.is_empty(),
1285 "ReadError must carry the concrete framing error"
1286 ),
1287 other => panic!("expected ReadError, got {other:?}"),
1288 }
1289 }
1290
1291 #[cfg(unix)]
1292 fn spawn_long_lived_client(
1293 script: &str,
1294 event_tx: Sender<LspEvent>,
1295 registry: LspChildRegistry,
1296 root: PathBuf,
1297 ) -> LspClient {
1298 LspClient::spawn(
1299 ServerKind::TypeScript,
1300 root,
1301 Path::new("sh"),
1302 &["-c".to_string(), script.to_string()],
1303 &HashMap::new(),
1304 event_tx,
1305 registry,
1306 )
1307 .expect("spawn long-lived LSP stand-in")
1308 }
1309
1310 #[cfg(unix)]
1311 #[test]
1312 fn reader_emits_read_error_for_malformed_frame() {
1313 let (tx, rx) = crossbeam_channel::unbounded();
1314 let registry = LspChildRegistry::new();
1315 let tmp = tempfile::tempdir().unwrap();
1316 let client = spawn_long_lived_client(
1317 "printf 'Content-Length: 3\r\n\r\n{{{'; exec sleep 60",
1318 tx,
1319 registry,
1320 tmp.path().to_path_buf(),
1321 );
1322 let event = rx
1323 .recv_timeout(Duration::from_secs(5))
1324 .expect("reader should emit ServerExited");
1325 match event {
1326 LspEvent::ServerExited {
1327 reason: ServerExitReason::ReadError(message),
1328 ..
1329 } => assert!(!message.is_empty()),
1330 other => panic!("expected ReadError ServerExited, got {other:?}"),
1331 }
1332 drop(client);
1333 }
1334
1335 #[cfg(unix)]
1336 #[test]
1337 fn reader_emits_eof_when_child_closes_stdout() {
1338 let (tx, rx) = crossbeam_channel::unbounded();
1339 let registry = LspChildRegistry::new();
1340 let tmp = tempfile::tempdir().unwrap();
1341 let client = spawn_long_lived_client("exit 0", tx, registry, tmp.path().to_path_buf());
1342 let event = rx
1343 .recv_timeout(Duration::from_secs(5))
1344 .expect("reader should emit ServerExited on EOF");
1345 match event {
1346 LspEvent::ServerExited {
1347 reason: ServerExitReason::Eof,
1348 ..
1349 } => {}
1350 other => panic!("expected Eof ServerExited, got {other:?}"),
1351 }
1352 drop(client);
1353 }
1354
1355 #[cfg(unix)]
1356 #[test]
1357 fn shutdown_error_kills_and_untracks_live_child() {
1358 let (tx, _rx) = crossbeam_channel::unbounded();
1359 let registry = LspChildRegistry::new();
1360 let tmp = tempfile::tempdir().unwrap();
1361 let mut client = spawn_long_lived_client(
1362 "exec sleep 60",
1363 tx,
1364 registry.clone(),
1365 tmp.path().to_path_buf(),
1366 );
1367 let pid = client.child_pid();
1368 assert!(
1369 registry.pids().contains(&pid),
1370 "child must be tracked before shutdown"
1371 );
1372 assert!(
1373 crate::bash_background::process::is_process_alive(pid),
1374 "child must still be running"
1375 );
1376 client.poison_writer_for_test();
1377 let result = client.shutdown();
1378 assert!(result.is_err(), "shutdown must return Err, got {result:?}");
1379 assert!(
1380 !crate::bash_background::process::is_process_alive(pid),
1381 "shutdown Err must not leave a live child"
1382 );
1383 assert!(
1384 !registry.pids().contains(&pid),
1385 "shutdown Err must untrack the child"
1386 );
1387 }
1388}