Skip to content

Commit 27edade

Browse files
authored
Make server panic hook more error resilient (#12610)
1 parent 2e2b1b4 commit 27edade

File tree

3 files changed

+65
-41
lines changed

3 files changed

+65
-41
lines changed

crates/ruff/src/main.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,19 @@ pub fn main() -> ExitCode {
8787
Err(err) => {
8888
#[allow(clippy::print_stderr)]
8989
{
90+
use std::io::Write;
91+
92+
// Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
93+
let mut stderr = std::io::stderr().lock();
94+
9095
// This communicates that this isn't a linter error but ruff itself hard-errored for
9196
// some reason (e.g. failed to resolve the configuration)
92-
eprintln!("{}", "ruff failed".red().bold());
97+
writeln!(stderr, "{}", "ruff failed".red().bold()).ok();
9398
// Currently we generally only see one error, but e.g. with io errors when resolving
9499
// the configuration it is help to chain errors ("resolving configuration failed" ->
95100
// "failed to read file: subdir/pyproject.toml")
96101
for cause in err.chain() {
97-
eprintln!(" {} {cause}", "Cause:".bold());
102+
writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
98103
}
99104
}
100105
ExitStatus::Error.into()

crates/ruff_server/src/message.rs

+14-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use std::sync::OnceLock;
2-
1+
use anyhow::Context;
32
use lsp_types::notification::Notification;
3+
use std::sync::OnceLock;
44

55
use crate::server::ClientSender;
66

@@ -10,53 +10,31 @@ pub(crate) fn init_messenger(client_sender: ClientSender) {
1010
MESSENGER
1111
.set(client_sender)
1212
.expect("messenger should only be initialized once");
13-
14-
// unregister any previously registered panic hook
15-
let _ = std::panic::take_hook();
16-
17-
// When we panic, try to notify the client.
18-
std::panic::set_hook(Box::new(move |panic_info| {
19-
if let Some(messenger) = MESSENGER.get() {
20-
let _ = messenger.send(lsp_server::Message::Notification(
21-
lsp_server::Notification {
22-
method: lsp_types::notification::ShowMessage::METHOD.into(),
23-
params: serde_json::to_value(lsp_types::ShowMessageParams {
24-
typ: lsp_types::MessageType::ERROR,
25-
message: String::from(
26-
"The Ruff language server exited with a panic. See the logs for more details."
27-
),
28-
})
29-
.unwrap_or_default(),
30-
},
31-
));
32-
}
33-
34-
let backtrace = std::backtrace::Backtrace::force_capture();
35-
tracing::error!("{panic_info}\n{backtrace}");
36-
#[allow(clippy::print_stderr)]
37-
{
38-
// we also need to print to stderr directly in case tracing hasn't
39-
// been initialized.
40-
eprintln!("{panic_info}\n{backtrace}");
41-
}
42-
}));
4313
}
4414

4515
pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) {
16+
try_show_message(message, message_type).unwrap();
17+
}
18+
19+
pub(super) fn try_show_message(
20+
message: String,
21+
message_type: lsp_types::MessageType,
22+
) -> crate::Result<()> {
4623
MESSENGER
4724
.get()
48-
.expect("messenger should be initialized")
25+
.ok_or_else(|| anyhow::anyhow!("messenger not initialized"))?
4926
.send(lsp_server::Message::Notification(
5027
lsp_server::Notification {
5128
method: lsp_types::notification::ShowMessage::METHOD.into(),
5229
params: serde_json::to_value(lsp_types::ShowMessageParams {
5330
typ: message_type,
5431
message,
55-
})
56-
.unwrap(),
32+
})?,
5733
},
5834
))
59-
.expect("message should send");
35+
.context("Failed to send message")?;
36+
37+
Ok(())
6038
}
6139

6240
/// Sends an error to the client with a formatted message. The error is sent in a

crates/ruff_server/src/server.rs

+44-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
//! Scheduling, I/O, and API endpoints.
22
3-
use std::num::NonZeroUsize;
4-
use std::str::FromStr;
5-
63
use lsp_server as lsp;
74
use lsp_types as types;
5+
use std::num::NonZeroUsize;
6+
use std::panic::PanicInfo;
7+
use std::str::FromStr;
88
use types::ClientCapabilities;
99
use types::CodeActionKind;
1010
use types::CodeActionOptions;
@@ -36,6 +36,7 @@ mod client;
3636
mod connection;
3737
mod schedule;
3838

39+
use crate::message::try_show_message;
3940
pub(crate) use connection::ClientSender;
4041

4142
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
@@ -133,6 +134,46 @@ impl Server {
133134
}
134135

135136
pub fn run(self) -> crate::Result<()> {
137+
type PanicHook = Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>;
138+
struct RestorePanicHook {
139+
hook: Option<PanicHook>,
140+
}
141+
142+
impl Drop for RestorePanicHook {
143+
fn drop(&mut self) {
144+
if let Some(hook) = self.hook.take() {
145+
std::panic::set_hook(hook);
146+
}
147+
}
148+
}
149+
150+
// unregister any previously registered panic hook
151+
// The hook will be restored when this function exits.
152+
let _ = RestorePanicHook {
153+
hook: Some(std::panic::take_hook()),
154+
};
155+
156+
// When we panic, try to notify the client.
157+
std::panic::set_hook(Box::new(move |panic_info| {
158+
use std::io::Write;
159+
160+
let backtrace = std::backtrace::Backtrace::force_capture();
161+
tracing::error!("{panic_info}\n{backtrace}");
162+
163+
// we also need to print to stderr directly for when using `$logTrace` because
164+
// the message won't be sent to the client.
165+
// But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken.
166+
let mut stderr = std::io::stderr().lock();
167+
writeln!(stderr, "{panic_info}\n{backtrace}").ok();
168+
169+
try_show_message(
170+
"The Ruff language server exited with a panic. See the logs for more details."
171+
.to_string(),
172+
lsp_types::MessageType::ERROR,
173+
)
174+
.ok();
175+
}));
176+
136177
event_loop_thread(move || {
137178
Self::event_loop(
138179
&self.connection,

0 commit comments

Comments
 (0)