fix: 修复若干问题,添加java lsp

This commit is contained in:
2026-02-15 22:41:24 +08:00
parent eab464060b
commit 00b0d825d8
264 changed files with 1036 additions and 309 deletions

View File

@@ -0,0 +1,180 @@
package monitor
import (
"context"
"strings"
"sync"
"time"
"monica-go-completion-backend/internal/completion"
)
// Config 控制探测间隔与下线阈值。
type Config struct {
ProbeInterval time.Duration
ProbeTimeout time.Duration
FailureThreshold int
}
// LanguageStatus 表示单语言探测状态。
type LanguageStatus struct {
Language string `json:"language"`
Online bool `json:"online"`
LastCheckedAt time.Time `json:"lastCheckedAt"`
LastSuccessAt time.Time `json:"lastSuccessAt,omitempty"`
ConsecutiveFailures int `json:"consecutiveFailures"`
LastError string `json:"lastError,omitempty"`
}
// LSPStatusMonitor 周期性探测各语言 LSP 是否可用。
type LSPStatusMonitor struct {
specs []completion.LanguageServerSpec
workspaceDir string
factory completion.ClientFactory
cfg Config
mu sync.RWMutex
statuses map[string]LanguageStatus
stopCh chan struct{}
stopOnce sync.Once
}
// NewLSPStatusMonitor 创建并启动探测协程。
func NewLSPStatusMonitor(
specs []completion.LanguageServerSpec,
workspaceDir string,
factory completion.ClientFactory,
cfg Config,
) *LSPStatusMonitor {
if cfg.ProbeInterval <= 0 {
cfg.ProbeInterval = 8 * time.Second
}
if cfg.ProbeTimeout <= 0 {
cfg.ProbeTimeout = 5 * time.Second
}
if cfg.FailureThreshold <= 0 {
cfg.FailureThreshold = 2
}
normalized := make([]completion.LanguageServerSpec, 0, len(specs))
statuses := make(map[string]LanguageStatus)
for _, spec := range specs {
lang := normalizeLanguage(spec.Language)
if lang == "" {
continue
}
spec.Language = lang
normalized = append(normalized, spec)
statuses[lang] = LanguageStatus{
Language: lang,
Online: false,
}
}
m := &LSPStatusMonitor{
specs: normalized,
workspaceDir: workspaceDir,
factory: factory,
cfg: cfg,
statuses: statuses,
stopCh: make(chan struct{}),
}
go m.loop()
return m
}
// LspServiceStatus 返回按语言聚合的状态快照。
func (m *LSPStatusMonitor) LspServiceStatus() map[string]any {
m.mu.RLock()
defer m.mu.RUnlock()
out := make(map[string]any, len(m.statuses))
for language, status := range m.statuses {
out[language] = status
}
return out
}
// Close 停止后台探测循环。
func (m *LSPStatusMonitor) Close() {
m.stopOnce.Do(func() {
close(m.stopCh)
})
}
func (m *LSPStatusMonitor) loop() {
// 启动后先做一轮探测,避免首次读取全是未知状态。
m.probeAll()
ticker := time.NewTicker(m.cfg.ProbeInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.probeAll()
case <-m.stopCh:
return
}
}
}
func (m *LSPStatusMonitor) probeAll() {
for _, spec := range m.specs {
m.probeOne(spec)
}
}
func (m *LSPStatusMonitor) probeOne(spec completion.LanguageServerSpec) {
language := normalizeLanguage(spec.Language)
if language == "" {
return
}
ctx, cancel := context.WithTimeout(context.Background(), m.cfg.ProbeTimeout)
defer cancel()
client, err := m.factory(ctx, spec, m.workspaceDir)
if err != nil {
m.markFailure(language, err)
return
}
_ = client.Close()
m.markSuccess(language)
}
func (m *LSPStatusMonitor) markSuccess(language string) {
now := time.Now()
m.mu.Lock()
defer m.mu.Unlock()
st := m.statuses[language]
st.LastCheckedAt = now
st.LastSuccessAt = now
st.ConsecutiveFailures = 0
st.LastError = ""
st.Online = true
m.statuses[language] = st
}
func (m *LSPStatusMonitor) markFailure(language string, err error) {
now := time.Now()
m.mu.Lock()
defer m.mu.Unlock()
st := m.statuses[language]
st.LastCheckedAt = now
st.ConsecutiveFailures++
if err != nil {
st.LastError = err.Error()
}
if st.ConsecutiveFailures >= m.cfg.FailureThreshold {
st.Online = false
}
m.statuses[language] = st
}
func normalizeLanguage(language string) string {
return strings.ToLower(strings.TrimSpace(language))
}

View File

@@ -0,0 +1,109 @@
package monitor
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"monica-go-completion-backend/internal/completion"
)
type fakeRuntimeClient struct{}
func (f *fakeRuntimeClient) DidOpen(_ context.Context, _ string, _ string, _ int) error {
return nil
}
func (f *fakeRuntimeClient) DidChange(_ context.Context, _ string, _ string, _ int) error {
return nil
}
func (f *fakeRuntimeClient) Completion(
_ context.Context,
_ string,
_ int,
_ int,
) (completion.Response, error) {
return completion.Response{}, nil
}
func (f *fakeRuntimeClient) Close() error {
return nil
}
func waitUntil(t *testing.T, timeout time.Duration, cond func() bool) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("condition not met within %s", timeout)
}
func TestMonitorMarksOnlineAfterSuccessfulProbe(t *testing.T) {
specs := []completion.LanguageServerSpec{
{Language: "go", LanguageID: "go", Command: "gopls"},
}
factory := func(
_ context.Context,
_ completion.LanguageServerSpec,
_ string,
) (completion.RuntimeClient, error) {
return &fakeRuntimeClient{}, nil
}
m := NewLSPStatusMonitor(specs, ".", factory, Config{
ProbeInterval: 20 * time.Millisecond,
ProbeTimeout: 100 * time.Millisecond,
FailureThreshold: 2,
})
defer m.Close()
waitUntil(t, 500*time.Millisecond, func() bool {
raw := m.LspServiceStatus()["go"]
status, ok := raw.(LanguageStatus)
return ok && status.Online
})
}
func TestMonitorMarksOfflineAfterConsecutiveFailures(t *testing.T) {
specs := []completion.LanguageServerSpec{
{Language: "go", LanguageID: "go", Command: "gopls"},
}
var calls atomic.Int32
factory := func(
_ context.Context,
_ completion.LanguageServerSpec,
_ string,
) (completion.RuntimeClient, error) {
n := calls.Add(1)
if n == 1 {
return &fakeRuntimeClient{}, nil
}
return nil, errors.New("probe failed")
}
m := NewLSPStatusMonitor(specs, ".", factory, Config{
ProbeInterval: 20 * time.Millisecond,
ProbeTimeout: 100 * time.Millisecond,
FailureThreshold: 2,
})
defer m.Close()
waitUntil(t, 500*time.Millisecond, func() bool {
raw := m.LspServiceStatus()["go"]
status, ok := raw.(LanguageStatus)
return ok && status.LastSuccessAt.UnixNano() > 0
})
waitUntil(t, 800*time.Millisecond, func() bool {
raw := m.LspServiceStatus()["go"]
status, ok := raw.(LanguageStatus)
return ok && !status.Online && status.ConsecutiveFailures >= 2
})
}