Skip to content

Commit 80a9e16

Browse files
fix(lsp): classify initialize-request write failures with stderr (#270)
When an LSP server dies immediately after spawn, the initialize *request* write can fail with a broken pipe before the initialize *response* wait ever starts — on Windows under CI load this raced often enough to flake broker_marks_initialize_exit_crashed_without_message_ classification on a changelog-only release PR (run 28690268431): the bare write error propagated without the captured stderr, so the crash reason ("unknown binary ...") was dropped. Route the request-write failure through the same kill + stderr-enrichment path as the response failure, making the classification deterministic on both platforms. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 8a95a9a commit 80a9e16

1 file changed

Lines changed: 24 additions & 14 deletions

File tree

src/diagnostics/lsp/client.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ impl StdioLspClient {
103103
let stderr_capture = Arc::new(Mutex::new(Vec::new()));
104104
let stderr_task = spawn_stderr_capture(stderr, Arc::clone(&stderr_capture));
105105

106-
write_message_with_timeout(
106+
let send_initialize = write_message_with_timeout(
107107
&mut stdin,
108108
json!({
109109
"jsonrpc": "2.0",
@@ -128,20 +128,30 @@ impl StdioLspClient {
128128
}),
129129
timeouts.message_io,
130130
)
131-
.await?;
132-
let initialize_result = tokio::time::timeout(
133-
timeouts.initialize_response,
134-
wait_for_initialize(&mut reader),
135-
)
136131
.await;
137-
if let Err(err) = initialize_result.unwrap_or_else(|_| {
138-
Err(TraceDecayError::Config {
139-
message: format!(
140-
"LSP server '{command}' initialize timed out after {} ms",
141-
timeouts.initialize_response.as_millis()
142-
),
143-
})
144-
}) {
132+
// A server that dies immediately can fail the initialize *request*
133+
// write (broken pipe — on Windows this races the spawn under load)
134+
// just as easily as the initialize *response* wait. Route both
135+
// failures through the same stderr-enriched classification so the
136+
// crash reason (e.g. a toolchain's "unknown binary" complaint) is
137+
// never dropped.
138+
let initialize_result = match send_initialize {
139+
Ok(()) => tokio::time::timeout(
140+
timeouts.initialize_response,
141+
wait_for_initialize(&mut reader),
142+
)
143+
.await
144+
.unwrap_or_else(|_| {
145+
Err(TraceDecayError::Config {
146+
message: format!(
147+
"LSP server '{command}' initialize timed out after {} ms",
148+
timeouts.initialize_response.as_millis()
149+
),
150+
})
151+
}),
152+
Err(err) => Err(err),
153+
};
154+
if let Err(err) = initialize_result {
145155
let _ = child.start_kill();
146156
let _ = child.wait().await;
147157
let _ = stderr_task.await;

0 commit comments

Comments
 (0)