Capture language server stderr during startup/init and log if failure

Julia created

Change summary

Cargo.lock                      |  2 +
crates/copilot/Cargo.toml       |  1 
crates/copilot/src/copilot.rs   | 12 +++++++++-
crates/language/src/language.rs | 20 +++++++++---------
crates/lsp/src/lsp.rs           | 36 ++++++++++++++++++++++++----------
crates/prettier/Cargo.toml      |  1 
crates/prettier/src/prettier.rs |  1 
crates/project/src/project.rs   | 33 +++++++++++++++++--------------
8 files changed, 68 insertions(+), 38 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1713,6 +1713,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "parking_lot 0.11.2",
  "rpc",
  "serde",
  "serde_derive",
@@ -5562,6 +5563,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "parking_lot 0.11.2",
  "serde",
  "serde_derive",
  "serde_json",

crates/copilot/Cargo.toml 🔗

@@ -36,6 +36,7 @@ serde.workspace = true
 serde_derive.workspace = true
 smol.workspace = true
 futures.workspace = true
+parking_lot.workspace = true
 
 [dev-dependencies]
 clock = { path = "../clock" }

crates/copilot/src/copilot.rs 🔗

@@ -16,6 +16,7 @@ use language::{
 };
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
 use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
 use request::StatusNotification;
 use settings::SettingsStore;
 use smol::{fs, io::BufReader, stream::StreamExt};
@@ -387,8 +388,15 @@ impl Copilot {
                     path: node_path,
                     arguments,
                 };
-                let server =
-                    LanguageServer::new(new_server_id, binary, Path::new("/"), None, cx.clone())?;
+
+                let server = LanguageServer::new(
+                    Arc::new(Mutex::new(None)),
+                    new_server_id,
+                    binary,
+                    Path::new("/"),
+                    None,
+                    cx.clone(),
+                )?;
 
                 server
                     .on_notification::<StatusNotification, _>(

crates/language/src/language.rs 🔗

@@ -645,7 +645,7 @@ struct LanguageRegistryState {
 
 pub struct PendingLanguageServer {
     pub server_id: LanguageServerId,
-    pub task: Task<Result<Option<lsp::LanguageServer>>>,
+    pub task: Task<Result<lsp::LanguageServer>>,
     pub container_dir: Option<Arc<Path>>,
 }
 
@@ -884,6 +884,7 @@ impl LanguageRegistry {
 
     pub fn create_pending_language_server(
         self: &Arc<Self>,
+        stderr_capture: Arc<Mutex<Option<String>>>,
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
         root_path: Arc<Path>,
@@ -923,7 +924,7 @@ impl LanguageRegistry {
                     })
                     .detach();
 
-                Ok(Some(server))
+                Ok(server)
             });
 
             return Some(PendingLanguageServer {
@@ -971,24 +972,23 @@ impl LanguageRegistry {
                     .clone();
                 drop(lock);
 
-                let binary = match entry.clone().await.log_err() {
-                    Some(binary) => binary,
-                    None => return Ok(None),
+                let binary = match entry.clone().await {
+                    Ok(binary) => binary,
+                    Err(err) => anyhow::bail!("{err}"),
                 };
 
                 if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
-                    if task.await.log_err().is_none() {
-                        return Ok(None);
-                    }
+                    task.await?;
                 }
 
-                Ok(Some(lsp::LanguageServer::new(
+                lsp::LanguageServer::new(
+                    stderr_capture,
                     server_id,
                     binary,
                     &root_path,
                     adapter.code_action_kinds(),
                     cx,
-                )?))
+                )
             })
         };
 

crates/lsp/src/lsp.rs 🔗

@@ -136,6 +136,7 @@ struct Error {
 
 impl LanguageServer {
     pub fn new(
+        stderr_capture: Arc<Mutex<Option<String>>>,
         server_id: LanguageServerId,
         binary: LanguageServerBinary,
         root_path: &Path,
@@ -165,6 +166,7 @@ impl LanguageServer {
             stdin,
             stdout,
             Some(stderr),
+            stderr_capture,
             Some(server),
             root_path,
             code_action_kinds,
@@ -197,6 +199,7 @@ impl LanguageServer {
         stdin: Stdin,
         stdout: Stdout,
         stderr: Option<Stderr>,
+        stderr_capture: Arc<Mutex<Option<String>>>,
         server: Option<Child>,
         root_path: &Path,
         code_action_kinds: Option<Vec<CodeActionKind>>,
@@ -218,20 +221,23 @@ impl LanguageServer {
         let io_handlers = Arc::new(Mutex::new(HashMap::default()));
 
         let stdout_input_task = cx.spawn(|cx| {
-            {
-                Self::handle_input(
-                    stdout,
-                    on_unhandled_notification.clone(),
-                    notification_handlers.clone(),
-                    response_handlers.clone(),
-                    io_handlers.clone(),
-                    cx,
-                )
-            }
+            Self::handle_input(
+                stdout,
+                on_unhandled_notification.clone(),
+                notification_handlers.clone(),
+                response_handlers.clone(),
+                io_handlers.clone(),
+                cx,
+            )
             .log_err()
         });
         let stderr_input_task = stderr
-            .map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()))
+            .map(|stderr| {
+                cx.spawn(|_| {
+                    Self::handle_stderr(stderr, io_handlers.clone(), stderr_capture.clone())
+                        .log_err()
+                })
+            })
             .unwrap_or_else(|| Task::Ready(Some(None)));
         let input_task = cx.spawn(|_| async move {
             let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
@@ -353,12 +359,14 @@ impl LanguageServer {
     async fn handle_stderr<Stderr>(
         stderr: Stderr,
         io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
+        stderr_capture: Arc<Mutex<Option<String>>>,
     ) -> anyhow::Result<()>
     where
         Stderr: AsyncRead + Unpin + Send + 'static,
     {
         let mut stderr = BufReader::new(stderr);
         let mut buffer = Vec::new();
+
         loop {
             buffer.clear();
             stderr.read_until(b'\n', &mut buffer).await?;
@@ -367,6 +375,10 @@ impl LanguageServer {
                 for handler in io_handlers.lock().values_mut() {
                     handler(IoKind::StdErr, message);
                 }
+
+                if let Some(stderr) = stderr_capture.lock().as_mut() {
+                    stderr.push_str(message);
+                }
             }
 
             // Don't starve the main thread when receiving lots of messages at once.
@@ -938,6 +950,7 @@ impl LanguageServer {
             stdin_writer,
             stdout_reader,
             None::<async_pipe::PipeReader>,
+            Arc::new(Mutex::new(None)),
             None,
             Path::new("/"),
             None,
@@ -950,6 +963,7 @@ impl LanguageServer {
                 stdout_writer,
                 stdin_reader,
                 None::<async_pipe::PipeReader>,
+                Arc::new(Mutex::new(None)),
                 None,
                 Path::new("/"),
                 None,

crates/prettier/Cargo.toml 🔗

@@ -27,6 +27,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 anyhow.workspace = true
 futures.workspace = true
+parking_lot.workspace = true
 
 [dev-dependencies]
 language = { path = "../language", features = ["test-support"] }

crates/prettier/src/prettier.rs 🔗

@@ -210,6 +210,7 @@ impl Prettier {
             .spawn(async move { node.binary_path().await })
             .await?;
         let server = LanguageServer::new(
+            Arc::new(parking_lot::Mutex::new(None)),
             server_id,
             LanguageServerBinary {
                 path: node_path,

crates/project/src/project.rs 🔗

@@ -52,6 +52,7 @@ use lsp::{
 };
 use lsp_command::*;
 use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
 use postage::watch;
 use prettier::{LocateStart, Prettier};
 use project_settings::{LspSettings, ProjectSettings};
@@ -2726,7 +2727,9 @@ impl Project {
             return;
         }
 
+        let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
         let pending_server = match self.languages.create_pending_language_server(
+            stderr_capture.clone(),
             language.clone(),
             adapter.clone(),
             worktree_path,
@@ -2763,10 +2766,14 @@ impl Project {
                 .await;
 
                 match result {
-                    Ok(server) => server,
+                    Ok(server) => {
+                        stderr_capture.lock().take();
+                        Some(server)
+                    }
 
                     Err(err) => {
                         log::error!("failed to start language server {:?}: {}", server_name, err);
+                        log::error!("server stderr: {:?}", stderr_capture.lock().take());
 
                         if let Some(this) = this.upgrade(&cx) {
                             if let Some(container_dir) = container_dir {
@@ -2862,20 +2869,17 @@ impl Project {
         server_id: LanguageServerId,
         key: (WorktreeId, LanguageServerName),
         cx: &mut AsyncAppContext,
-    ) -> Result<Option<Arc<LanguageServer>>> {
-        let setup = Self::setup_pending_language_server(
+    ) -> Result<Arc<LanguageServer>> {
+        let language_server = Self::setup_pending_language_server(
             this,
             override_initialization_options,
             pending_server,
             adapter.clone(),
             server_id,
             cx,
-        );
+        )
+        .await?;
 
-        let language_server = match setup.await? {
-            Some(language_server) => language_server,
-            None => return Ok(None),
-        };
         let this = match this.upgrade(cx) {
             Some(this) => this,
             None => return Err(anyhow!("failed to upgrade project handle")),
@@ -2892,7 +2896,7 @@ impl Project {
             )
         })?;
 
-        Ok(Some(language_server))
+        Ok(language_server)
     }
 
     async fn setup_pending_language_server(
@@ -2902,12 +2906,9 @@ impl Project {
         adapter: Arc<CachedLspAdapter>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
-    ) -> Result<Option<Arc<LanguageServer>>> {
+    ) -> Result<Arc<LanguageServer>> {
         let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await;
-        let language_server = match pending_server.task.await? {
-            Some(server) => server,
-            None => return Ok(None),
-        };
+        let language_server = pending_server.task.await?;
 
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
@@ -2978,6 +2979,7 @@ impl Project {
                 },
             )
             .detach();
+
         language_server
             .on_request::<lsp::request::RegisterCapability, _, _>({
                 move |params, mut cx| async move {
@@ -3043,6 +3045,7 @@ impl Project {
                 }
             })
             .detach();
+
         let mut initialization_options = adapter.adapter.initialization_options().await;
         match (&mut initialization_options, override_options) {
             (Some(initialization_options), Some(override_options)) => {
@@ -3062,7 +3065,7 @@ impl Project {
             )
             .ok();
 
-        Ok(Some(language_server))
+        Ok(language_server)
     }
 
     fn insert_newly_running_language_server(