mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-19 05:57:38 +00:00
fix(lsp): detect, evict, and respawn crashed language servers
A crashed language server stayed cached with ready==true: readLoop exited and drained in-flight calls but never cleared the running flag, and GetServer keyed reuse on ready alone. Every subsequent hover/definition/references then passed the running guard, wrote to a dead stdin, and blocked the full request timeout — permanently, since active use kept the server off the idle reaper. A single crash wedged that language at ~10s/call with no self-heal. - readLoop now flips running=false on exit, so Call fails fast instead of hanging. - GetServer treats ready && client.IsRunning() as the liveness condition; a stale entry is evicted (closed async — its process has already exited) and a fresh server is spawned in its place. - drainPending's send is now non-blocking, matching the dispatch path, so a duplicate-id race can't deadlock it under c.mu. Test (proven to fail without the fix): a client whose process exits is marked not-running and its next Call fails fast rather than blocking.
This commit is contained in:
+19
-1
@@ -304,7 +304,12 @@ func (c *Client) send(msg interface{}) error {
|
||||
// On exit (for any reason), it drains all pending Call waiters with a
|
||||
// synthetic error so that goroutines blocked in Call are unblocked.
|
||||
func (c *Client) readLoop() {
|
||||
// On exit (EOF/error from a crashed or closed server) mark the client not
|
||||
// running so subsequent Calls fail fast — otherwise they pass the running
|
||||
// guard, send to a dead stdin, and block until their own timeout. Then
|
||||
// unblock any already-in-flight waiters. LIFO: setRunning(false) runs first.
|
||||
defer c.drainPending()
|
||||
defer c.setRunning(false)
|
||||
|
||||
reader := bufio.NewReader(c.stdout)
|
||||
|
||||
@@ -380,13 +385,19 @@ func (c *Client) drainPending() {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for id, ch := range c.pending {
|
||||
ch <- &Response{
|
||||
// Non-blocking, matching the dispatch path: the channel is buffered(1)
|
||||
// and read once, but a duplicate-id race could have already filled it —
|
||||
// never block here while holding c.mu.
|
||||
select {
|
||||
case ch <- &Response{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &ResponseError{
|
||||
Code: -32603, // InternalError
|
||||
Message: "LSP client readLoop terminated",
|
||||
},
|
||||
}:
|
||||
default:
|
||||
}
|
||||
delete(c.pending, id)
|
||||
}
|
||||
@@ -398,3 +409,10 @@ func (c *Client) IsRunning() bool {
|
||||
defer c.runningMu.RUnlock()
|
||||
return c.running
|
||||
}
|
||||
|
||||
// setRunning sets the running flag under its lock.
|
||||
func (c *Client) setRunning(v bool) {
|
||||
c.runningMu.Lock()
|
||||
c.running = v
|
||||
c.runningMu.Unlock()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user