ebkalderon / tower-lsp

Language Server Protocol implementation written in Rust
Apache License 2.0
1.01k stars 57 forks source link

Server may not be exiting correctly after receiving `exit` notification #399

Open siegel opened 12 months ago

siegel commented 12 months ago

This issue describes a symptom in which a language server does not appear to be exiting when it receives the exit protocol notification.

From this comment:

In theory, Server::serve() should already gracefully shut down when it receives an exit message, regardless of the state of stdin, but if it doesn't, then there must be a bug in the logic somewhere.

Creating this issue as requested. :-)

nayeemrmn commented 11 months ago

It looks like the Deno LSP with VSCode has been leaving old processes alive whenever it gets restarted. This is higher priority for us now.

@ebkalderon Is there a workaround for this, like a way to react to the exit notification to cancel a token around Server::serve()? https://github.com/denoland/deno/blob/v1.37.2/cli/lsp/mod.rs#L84

ebkalderon commented 11 months ago

That certainly doesn't sound good! Unfortunately, I don't think there's currently a user-facing way to directly react to the exit notification yet, looking at the macro-generated code for the exit notification for reference (see the lib.rs module in the tower-lsp-macros crate).

With that said, I'll be sure to prioritize this for investigation tonight. Would you or someone on the Deno team happen to have a minimal reproducible example in mind that can be debugged or treated against? I will later pull a form of it into the tower-lsp repo and turn it into a unit or integration test for the sake of coverage.

Thanks for the patience, folks! Please let me know and I'll start debugging later tonight, in the hopes of pushing out an emergency release soon after.

nayeemrmn commented 11 months ago
#[tokio::main]
async fn main() {
  use tower_lsp::LanguageServer;
  use tower_lsp::LspService;
  use tower_lsp::Server;
  use tower_lsp::async_trait;
  use tower_lsp::jsonrpc::Result;
  use tower_lsp::lsp_types::InitializeParams;
  use tower_lsp::lsp_types::InitializeResult;

  struct Backend;
  #[async_trait]
  impl LanguageServer for Backend {
    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
        Ok(InitializeResult::default())
    }

    async fn shutdown(&self) -> Result<()> {
        Ok(())
    }
  }

  let stdin = tokio::io::stdin();
  let stdout = tokio::io::stdout();
  let (service, socket) = LspService::new(|_| Backend);
  Server::new(stdin, stdout, socket).serve(service).await;
}

And then run on the CLI:

(printf 'Content-Length: 33\r\n\r\n{"jsonrpc":"2.0","method":"exit"}' && cat) | cargo run

The program will hang until you press Ctrl+D to close stdin.

Any similar hanging stream would do for an automated test.

By the way, I think the main doc comment example (as rendered -- without the hidden lines) at https://github.com/ebkalderon/tower-lsp/blob/v0.20.0/src/lib.rs#L57-L73 will exhibit this.