Skip to content

Commit 17cdba6

Browse files
committed
Add support for automatic server restart on crash
1 parent 0e0f99d commit 17cdba6

File tree

4 files changed

+112
-0
lines changed

4 files changed

+112
-0
lines changed

doc/LanguageClient.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,20 @@ Highlight group to be used for code lens.
635635

636636
Default: 'Comment'
637637

638+
2.42 g:LanguageClient_restartOnCrash *g:LanguageClient_restartOnCrash*
639+
640+
If enabled, the client will attempt to restart the server on the event that it
641+
unexpectedly crashes.
642+
643+
Default: 1
644+
645+
2.43 g:LanguageClient_maxRestartRetries *g:LanguageClient_maxRestartRetries*
646+
647+
Max number of times to attempt to recover from a server crash. Each language
648+
handles it's own count independently.
649+
650+
Default: 5
651+
638652
==============================================================================
639653
3. Commands *LanguageClientCommands*
640654

@@ -1072,6 +1086,11 @@ This event is triggered when diagnostics changed.
10721086

10731087
Triggered after textDocument/didOpen notification is sent to language server.
10741088

1089+
6.5 LanguageServerCrashed
1090+
*LanguageServerCrashed*
1091+
1092+
This event is triggered when a language server unexpectedly quits.
1093+
10751094
==============================================================================
10761095
7. License *LanguageClientLicense*
10771096

src/language_server_protocol.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ impl LanguageClient {
214214
.as_ref(),
215215
)?;
216216

217+
#[allow(clippy::type_complexity)]
218+
let (restart_on_crash, max_restart_retries): (u8, u8) = self.vim()?.eval(
219+
[
220+
"get(g:, 'LanguageClient_restartOnCrash', 1)",
221+
"get(g:, 'LanguageClient_maxRestartRetries', 5)",
222+
]
223+
.as_ref(),
224+
)?;
225+
217226
// vimscript use 1 for true, 0 for false.
218227
let auto_start = auto_start == 1;
219228
let selection_ui_auto_open = selection_ui_auto_open == 1;
@@ -328,6 +337,9 @@ impl LanguageClient {
328337
state.preferred_markup_kind = preferred_markup_kind;
329338
state.enable_extensions = enable_extensions;
330339
state.code_lens_hl_group = code_lens_hl_group;
340+
state.max_restart_retries = max_restart_retries;
341+
state.restart_on_crash = restart_on_crash == 1;
342+
331343
Ok(())
332344
})?;
333345

@@ -1090,6 +1102,7 @@ impl LanguageClient {
10901102
}
10911103
self.handle_cursor_moved(&Value::Null)?;
10921104

1105+
error!("UPDATING STATE");
10931106
self.update(|state| {
10941107
state.clients.remove(&Some(language_id.into()));
10951108
state.last_cursor_line = 0;
@@ -3970,12 +3983,20 @@ impl LanguageClient {
39703983
(child_id, reader, writer)
39713984
};
39723985

3986+
let lcn = self.clone();
3987+
let on_server_crash = move |language_id: &LanguageId| -> () {
3988+
if let Err(err) = lcn.on_server_crash(language_id) {
3989+
error!("Restart attempt failed: {}", err);
3990+
}
3991+
};
3992+
39733993
let client = RpcClient::new(
39743994
Some(language_id.clone()),
39753995
reader,
39763996
writer,
39773997
child_id,
39783998
self.get(|state| state.tx.clone())?,
3999+
on_server_crash,
39794000
)?;
39804001
self.update(|state| {
39814002
state
@@ -4016,6 +4037,55 @@ impl LanguageClient {
40164037
Ok(Value::Null)
40174038
}
40184039

4040+
fn on_server_crash(&self, language_id: &LanguageId) -> Result<()> {
4041+
if language_id.is_none() {
4042+
return Ok(());
4043+
}
4044+
4045+
self.vim()?
4046+
.rpcclient
4047+
.notify("s:ExecuteAutocmd", "LanguageServerCrashed")?;
4048+
let filename = self.vim()?.get_filename(&Value::Null)?;
4049+
self.vim()?
4050+
.rpcclient
4051+
.notify("setbufvar", json!([filename, VIM_IS_SERVER_RUNNING, 0]))?;
4052+
4053+
let restart_on_crash = self.get(|state| state.restart_on_crash)?;
4054+
if !restart_on_crash {
4055+
return Ok(());
4056+
}
4057+
4058+
let max_restart_retries = self.get(|state| state.max_restart_retries)?;
4059+
let mut restarts =
4060+
self.get(|state| state.restarts.get(language_id).cloned().unwrap_or_default())?;
4061+
restarts += 1;
4062+
4063+
self.update(|state| {
4064+
let mut restarts = restarts;
4065+
if restarts > max_restart_retries {
4066+
restarts = 0;
4067+
};
4068+
4069+
state.clients.remove(language_id);
4070+
state.restarts.insert(language_id.clone(), restarts);
4071+
Ok(())
4072+
})?;
4073+
4074+
if restarts > max_restart_retries {
4075+
self.vim()?.echoerr(format!(
4076+
"Server for {} restarted too many times, not retrying any more.",
4077+
language_id.clone().unwrap()
4078+
))?;
4079+
return Ok(());
4080+
}
4081+
4082+
self.vim()?.echoerr("Server crashed, restarting client")?;
4083+
std::thread::sleep(Duration::from_millis(300 * (restarts as u64).pow(2)));
4084+
self.start_server(&json!({"languageId": language_id.clone().unwrap()}))?;
4085+
4086+
Ok(())
4087+
}
4088+
40194089
pub fn handle_server_exited(&self, params: &Value) -> Result<()> {
40204090
let filename = self.vim()?.get_filename(params)?;
40214091
let language_id = self.vim()?.get_language_id(&filename, params)?;

src/rpcclient.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,24 @@ impl RpcClient {
3535
writer: impl Write + Send + 'static,
3636
process_id: Option<u32>,
3737
sink: Sender<Call>,
38+
on_crash: impl Fn(&LanguageId) -> () + Clone + Send + 'static,
3839
) -> Result<Self> {
3940
let (reader_tx, reader_rx): (Sender<(Id, Sender<jsonrpc_core::Output>)>, _) = unbounded();
4041

4142
let language_id_clone = language_id.clone();
4243
let reader_thread_name = format!("reader-{:?}", language_id);
44+
let on_crash_clone = on_crash.clone();
4345
thread::Builder::new()
4446
.name(reader_thread_name.clone())
4547
.spawn(move || {
4648
if let Err(err) = loop_read(reader, reader_rx, &sink, &language_id_clone) {
49+
match err.downcast_ref::<std::io::Error>() {
50+
Some(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
51+
on_crash_clone(&language_id_clone)
52+
}
53+
_ => {}
54+
}
55+
4756
error!("Thread {} exited with error: {:?}", reader_thread_name, err);
4857
}
4958
})?;
@@ -55,6 +64,13 @@ impl RpcClient {
5564
.name(writer_thread_name.clone())
5665
.spawn(move || {
5766
if let Err(err) = loop_write(writer, &writer_rx, &language_id_clone) {
67+
match err.downcast_ref::<std::io::Error>() {
68+
Some(err) if err.kind() == std::io::ErrorKind::BrokenPipe => {
69+
on_crash(&language_id_clone)
70+
}
71+
_ => {}
72+
}
73+
5874
error!("Thread {} exited with error: {:?}", writer_thread_name, err);
5975
}
6076
})?;

src/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ pub struct State {
142142

143143
#[serde(skip_serializing)]
144144
pub clients: HashMap<LanguageId, Arc<RpcClient>>,
145+
pub restarts: HashMap<LanguageId, u8>,
145146

146147
#[serde(skip_serializing)]
147148
pub vim: Vim,
@@ -215,6 +216,8 @@ pub struct State {
215216
pub server_stderr: Option<String>,
216217
pub logger: Logger,
217218
pub preferred_markup_kind: Option<Vec<MarkupKind>>,
219+
pub restart_on_crash: bool,
220+
pub max_restart_retries: u8,
218221
}
219222

220223
impl State {
@@ -228,6 +231,7 @@ impl State {
228231
BufWriter::new(std::io::stdout()),
229232
None,
230233
tx.clone(),
234+
|_: &LanguageId| {},
231235
)?);
232236

233237
Ok(Self {
@@ -236,6 +240,7 @@ impl State {
236240
clients: hashmap! {
237241
None => client.clone(),
238242
},
243+
restarts: HashMap::new(),
239244

240245
vim: Vim::new(client),
241246

@@ -296,6 +301,8 @@ impl State {
296301
preferred_markup_kind: None,
297302
enable_extensions: None,
298303
code_lens_hl_group: "Comment".into(),
304+
restart_on_crash: true,
305+
max_restart_retries: 5,
299306

300307
logger,
301308
})

0 commit comments

Comments
 (0)