on main help sign in
fix(lsp): fallback to Kill() on timeout (#2349)
commit56d79d108ee5036224ef13155871457425c7729e authorTai Groot <tai@taigrr.com> dateWed 4 Mar 2026, 03:07 (8 months ago) parentaa0997b
2 files changed+27−8
internal/lsp/client.go+25−6 internal/lsp/manager.go+2−2
internal/lsp/client.go+25−6
@@ -125,19 +125,38 @@ func (c *Client) Initialize(ctx context.Context, workspaceDir string) (*protocol
125125 return result, nil
126126}
127127
128+// closeTimeout is the maximum time to wait for a graceful LSP shutdown.
129+const closeTimeout = 5 * time.Second
130+
128131// Kill kills the client without doing anything else.
129132func (c *Client) Kill() { c.client.Kill() }
130133
131−// Close closes all open files in the client, then the client.
134+// Close closes all open files in the client, then shuts down gracefully.
135+// If shutdown takes longer than closeTimeout, it falls back to Kill().
132136func (c *Client) Close(ctx context.Context) error {
133137 c.CloseAllFiles(ctx)
134138
135− // Shutdown and exit the client
136− if err := c.client.Shutdown(ctx); err != nil {
137− slog.Warn("Failed to shutdown LSP client", "error", err)
138− }
139+ // Use a timeout to prevent hanging on unresponsive LSP servers.
140+ // jsonrpc2's send lock doesn't respect context cancellation, so we
141+ // need to fall back to Kill() which closes the underlying connection.
142+ closeCtx, cancel := context.WithTimeout(ctx, closeTimeout)
143+ defer cancel()
139144
140− return c.client.Exit()
145+ done := make(chan error, 1)
146+ go func() {
147+ if err := c.client.Shutdown(closeCtx); err != nil {
148+ slog.Warn("Failed to shutdown LSP client", "error", err)
149+ }
150+ done <- c.client.Exit()
151+ }()
152+
153+ select {
154+ case err := <-done:
155+ return err
156+ case <-closeCtx.Done():
157+ c.client.Kill()
158+ return closeCtx.Err()
159+ }
141160}
142161
143162// createPowernapClient creates a new powernap client with the current configuration.
internal/lsp/manager.go+2−2
@@ -205,7 +205,7 @@ func (s *Manager) startServer(ctx context.Context, name, filepath string, server
205205 if existing, ok := s.clients.Get(name); ok {
206206 switch existing.GetServerState() {
207207 case StateReady, StateStarting, StateDisabled:
208− client.Close(ctx)
208+ _ = client.Close(ctx)
209209 s.callback(name, existing)
210210 return
211211 }
@@ -228,7 +228,7 @@ func (s *Manager) startServer(ctx context.Context, name, filepath string, server
228228
229229 if _, err := client.Initialize(initCtx, s.cfg.WorkingDir()); err != nil {
230230 slog.Error("LSP client initialization failed", "name", name, "error", err)
231− client.Close(ctx)
231+ _ = client.Close(ctx)
232232 s.clients.Del(name)
233233 return
234234 }