mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
feat(lsp): capture server stderr to diagnose init failures
drainStderr discarded all server stderr, so an LSP init failure (e.g. 'command not found', missing module) surfaced as an opaque 'initialization failed'. Retain a bounded tail (StderrCaptureMax=4096B, oldest dropped) under a mutex and attach it to the init-failure error context. Adds TestClientStderrCapture.
This commit is contained in:
+29
-2
@@ -22,6 +22,10 @@ const ProcessKillTimeout = 5 * time.Second
|
||||
// StderrBufferSize is the buffer size for draining stderr.
|
||||
const StderrBufferSize = 1024
|
||||
|
||||
// StderrCaptureMax is the maximum number of recent stderr bytes retained for
|
||||
// diagnostics (e.g. to explain an init failure). Older bytes are dropped.
|
||||
const StderrCaptureMax = 4096
|
||||
|
||||
// Client represents an LSP client connection.
|
||||
type Client struct {
|
||||
stdin io.WriteCloser
|
||||
@@ -35,6 +39,8 @@ type Client struct {
|
||||
runningMu sync.RWMutex
|
||||
stopOnce sync.Once
|
||||
mu sync.Mutex
|
||||
stderrMu sync.Mutex
|
||||
stderrTail []byte // most recent stderr, capped at StderrCaptureMax (diagnostics)
|
||||
running bool
|
||||
}
|
||||
|
||||
@@ -135,14 +141,35 @@ func (c *Client) drainStderr() {
|
||||
return
|
||||
default:
|
||||
}
|
||||
// Read and discard stderr output
|
||||
_, err := c.stderr.Read(buf)
|
||||
n, err := c.stderr.Read(buf)
|
||||
if n > 0 {
|
||||
c.captureStderr(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// captureStderr appends p to the retained stderr tail, keeping at most
|
||||
// StderrCaptureMax bytes (dropping the oldest).
|
||||
func (c *Client) captureStderr(p []byte) {
|
||||
c.stderrMu.Lock()
|
||||
defer c.stderrMu.Unlock()
|
||||
c.stderrTail = append(c.stderrTail, p...)
|
||||
if len(c.stderrTail) > StderrCaptureMax {
|
||||
c.stderrTail = c.stderrTail[len(c.stderrTail)-StderrCaptureMax:]
|
||||
}
|
||||
}
|
||||
|
||||
// RecentStderr returns the retained tail of the server's stderr, trimmed. Useful
|
||||
// for surfacing why an LSP server failed to start.
|
||||
func (c *Client) RecentStderr() string {
|
||||
c.stderrMu.Lock()
|
||||
defer c.stderrMu.Unlock()
|
||||
return strings.TrimSpace(string(c.stderrTail))
|
||||
}
|
||||
|
||||
// Call sends a request and waits for a response.
|
||||
func (c *Client) Call(ctx context.Context, method string, params interface{}) (*Response, error) {
|
||||
c.runningMu.RLock()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// drainStderr retains a bounded tail of server stderr for diagnostics; capture
|
||||
// must keep recent bytes and never grow past StderrCaptureMax.
|
||||
func TestClientStderrCapture(t *testing.T) {
|
||||
c := &Client{}
|
||||
|
||||
if c.RecentStderr() != "" {
|
||||
t.Errorf("expected empty stderr initially, got %q", c.RecentStderr())
|
||||
}
|
||||
|
||||
c.captureStderr([]byte("gopls: command not found\n"))
|
||||
if !strings.Contains(c.RecentStderr(), "command not found") {
|
||||
t.Errorf("expected captured stderr, got %q", c.RecentStderr())
|
||||
}
|
||||
|
||||
// Exceed the cap: only the most recent StderrCaptureMax bytes are kept.
|
||||
big := strings.Repeat("x", StderrCaptureMax*2)
|
||||
c.captureStderr([]byte(big))
|
||||
if got := len(c.RecentStderr()); got > StderrCaptureMax {
|
||||
t.Errorf("stderr tail not bounded: len=%d, want <= %d", got, StderrCaptureMax)
|
||||
}
|
||||
}
|
||||
@@ -212,10 +212,16 @@ func (m *Manager) GetServer(ctx context.Context, lang protocol.Language) (*Manag
|
||||
// Initialize server
|
||||
if err := m.initializeServer(ctx, newSrv); err != nil {
|
||||
newSrv.initErr = err
|
||||
return nil, errors.Wrap(errors.ErrLSPInitFailed, "LSP server initialization failed", err).
|
||||
wrapped := errors.Wrap(errors.ErrLSPInitFailed, "LSP server initialization failed", err).
|
||||
WithContext("language", string(lang)).
|
||||
WithContext("command", config.Command[0]).
|
||||
WithRemediation("Check LSP server logs for initialization errors")
|
||||
// Surface the server's own stderr (e.g. "command not found", missing
|
||||
// module) so the failure is diagnosable instead of opaque.
|
||||
if stderr := client.RecentStderr(); stderr != "" {
|
||||
wrapped = wrapped.WithContext("stderr", stderr)
|
||||
}
|
||||
return nil, wrapped
|
||||
}
|
||||
|
||||
// Mark as successfully initialized to prevent cleanup
|
||||
|
||||
Reference in New Issue
Block a user