Skip to content

Commit 4b2822f

Browse files
committed
Add support for automatic server restart on crash
1 parent 353e9de commit 4b2822f

File tree

4 files changed

+111
-0
lines changed

4 files changed

+111
-0
lines changed

doc/LanguageClient.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,20 @@ margin, set this setting to 0. Example:
657657
<
658658
Default: 1
659659

660+
2.43 g:LanguageClient_restartOnCrash *g:LanguageClient_restartOnCrash*
661+
662+
If enabled, the client will attempt to restart the server on the event that it
663+
unexpectedly crashes.
664+
665+
Default: 1
666+
667+
2.44 g:LanguageClient_maxRestartRetries *g:LanguageClient_maxRestartRetries*
668+
669+
Max number of times to attempt to recover from a server crash. Each language
670+
handles its own count independently.
671+
672+
Default: 5
673+
660674
==============================================================================
661675
3. Commands *LanguageClientCommands*
662676

@@ -1094,6 +1108,11 @@ This event is triggered when diagnostics changed.
10941108

10951109
Triggered after textDocument/didOpen notification is sent to language server.
10961110

1111+
6.5 LanguageServerCrashed
1112+
*LanguageServerCrashed*
1113+
1114+
This event is triggered when a language server unexpectedly quits.
1115+
10971116
==============================================================================
10981117
7. License *LanguageClientLicense*
10991118

src/language_server_protocol.rs

Lines changed: 69 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

@@ -3832,12 +3844,20 @@ impl LanguageClient {
38323844
(child_id, reader, writer)
38333845
};
38343846

3847+
let lcn = self.clone();
3848+
let on_server_crash = move |language_id: &LanguageId| -> () {
3849+
if let Err(err) = lcn.on_server_crash(language_id) {
3850+
error!("Restart attempt failed: {}", err);
3851+
}
3852+
};
3853+
38353854
let client = RpcClient::new(
38363855
Some(language_id.clone()),
38373856
reader,
38383857
writer,
38393858
child_id,
38403859
self.get(|state| state.tx.clone())?,
3860+
on_server_crash,
38413861
)?;
38423862
self.update(|state| {
38433863
state
@@ -3876,6 +3896,55 @@ impl LanguageClient {
38763896
Ok(Value::Null)
38773897
}
38783898

3899+
fn on_server_crash(&self, language_id: &LanguageId) -> Result<()> {
3900+
if language_id.is_none() {
3901+
return Ok(());
3902+
}
3903+
3904+
self.vim()?
3905+
.rpcclient
3906+
.notify("s:ExecuteAutocmd", "LanguageServerCrashed")?;
3907+
let filename = self.vim()?.get_filename(&Value::Null)?;
3908+
self.vim()?
3909+
.rpcclient
3910+
.notify("setbufvar", json!([filename, VIM_IS_SERVER_RUNNING, 0]))?;
3911+
3912+
let restart_on_crash = self.get(|state| state.restart_on_crash)?;
3913+
if !restart_on_crash {
3914+
return Ok(());
3915+
}
3916+
3917+
let max_restart_retries = self.get(|state| state.max_restart_retries)?;
3918+
let mut restarts =
3919+
self.get(|state| state.restarts.get(language_id).cloned().unwrap_or_default())?;
3920+
restarts += 1;
3921+
3922+
self.update(|state| {
3923+
let mut restarts = restarts;
3924+
if restarts > max_restart_retries {
3925+
restarts = 0;
3926+
};
3927+
3928+
state.clients.remove(language_id);
3929+
state.restarts.insert(language_id.clone(), restarts);
3930+
Ok(())
3931+
})?;
3932+
3933+
if restarts > max_restart_retries {
3934+
self.vim()?.echoerr(format!(
3935+
"Server for {} restarted too many times, not retrying any more.",
3936+
language_id.clone().unwrap()
3937+
))?;
3938+
return Ok(());
3939+
}
3940+
3941+
self.vim()?.echoerr("Server crashed, restarting client")?;
3942+
std::thread::sleep(Duration::from_millis(300 * (restarts as u64).pow(2)));
3943+
self.start_server(&json!({"languageId": language_id.clone().unwrap()}))?;
3944+
3945+
Ok(())
3946+
}
3947+
38793948
pub fn handle_server_exited(&self, params: &Value) -> Result<()> {
38803949
let filename = self.vim()?.get_filename(params)?;
38813950
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
@@ -43,15 +43,24 @@ impl RpcClient {
4343
writer: impl Write + Send + 'static,
4444
process_id: Option<u32>,
4545
sink: Sender<Call>,
46+
on_crash: impl Fn(&LanguageId) -> () + Clone + Send + 'static,
4647
) -> Result<Self> {
4748
let (reader_tx, reader_rx): (Sender<(Id, Sender<jsonrpc_core::Output>)>, _) = unbounded();
4849

4950
let language_id_clone = language_id.clone();
5051
let reader_thread_name = format!("reader-{:?}", language_id);
52+
let on_crash_clone = on_crash.clone();
5153
thread::Builder::new()
5254
.name(reader_thread_name.clone())
5355
.spawn(move || {
5456
if let Err(err) = loop_read(reader, reader_rx, &sink, &language_id_clone) {
57+
match err.downcast_ref::<std::io::Error>() {
58+
Some(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
59+
on_crash_clone(&language_id_clone)
60+
}
61+
_ => {}
62+
}
63+
5564
error!("Thread {} exited with error: {:?}", reader_thread_name, err);
5665
}
5766
})?;
@@ -63,6 +72,13 @@ impl RpcClient {
6372
.name(writer_thread_name.clone())
6473
.spawn(move || {
6574
if let Err(err) = loop_write(writer, &writer_rx, &language_id_clone) {
75+
match err.downcast_ref::<std::io::Error>() {
76+
Some(err) if err.kind() == std::io::ErrorKind::BrokenPipe => {
77+
on_crash(&language_id_clone)
78+
}
79+
_ => {}
80+
}
81+
6682
error!("Thread {} exited with error: {:?}", writer_thread_name, err);
6783
}
6884
})?;

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)