fix: 修复若干问题,添加java lsp
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
|
||||
docs
|
||||
|
||||
cmd/server.exe
|
||||
*.log
|
||||
*.out
|
||||
logs/
|
||||
cmd/logs/
|
||||
log/
|
||||
|
||||
config.json
|
||||
config.debug-java.json
|
||||
|
||||
**/*_test.go
|
||||
|
||||
# jdtls: only need plugins + config_linux at runtime, but keep all for simplicity
|
||||
jdt-language-server-1.57.0-202602111032/config_win/org.eclipse.osgi/
|
||||
jdt-language-server-1.57.0-202602111032/config_win/org.eclipse.core.runtime/
|
||||
jdt-language-server-1.57.0-202602111032/config_win/org.eclipse.equinox.app/
|
||||
jdt-language-server-1.57.0-202602111032/config_win/org.eclipse.equinox.launcher/
|
||||
jdt-language-server-1.57.0-202602111032/.claude/
|
||||
|
||||
@@ -1,22 +1,88 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
# syntax=docker/dockerfile:1
|
||||
###############################################################################
|
||||
# All-in-one LSP Gateway Image
|
||||
# - Go → gopls
|
||||
# - JS / TS → typescript-language-server
|
||||
# - Java → Eclipse JDT Language Server (jdtls) + JDK 22
|
||||
###############################################################################
|
||||
|
||||
# ── Stage 1: Build Go gateway binary ────────────────────────────────────────
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
|
||||
|
||||
FROM alpine:3.20
|
||||
# ── Stage 2: Build gopls ────────────────────────────────────────────────────
|
||||
FROM golang:1.25-bookworm AS gopls-builder
|
||||
RUN go install golang.org/x/tools/gopls@latest
|
||||
|
||||
RUN addgroup -S app && adduser -S -G app app
|
||||
# ── Stage 3: Runtime ────────────────────────────────────────────────────────
|
||||
FROM eclipse-temurin:22-jdk-noble
|
||||
|
||||
ARG NODE_MAJOR=22
|
||||
|
||||
# ---- System deps: Node.js, python3 (jdtls launcher) ----
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl python3 && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ---- typescript-language-server (covers JavaScript + TypeScript) ----
|
||||
RUN npm install -g typescript-language-server typescript && \
|
||||
npm cache clean --force
|
||||
|
||||
# ---- Go toolchain (gopls needs it at runtime for analysis) ----
|
||||
COPY --from=golang:1.25-bookworm /usr/local/go /usr/local/go
|
||||
ENV PATH="/usr/local/go/bin:/go/bin:${PATH}" \
|
||||
GOPATH="/go"
|
||||
|
||||
# ---- gopls ----
|
||||
COPY --from=gopls-builder /go/bin/gopls /go/bin/gopls
|
||||
|
||||
# ---- Eclipse JDT Language Server ----
|
||||
COPY jdt-language-server-1.57.0-202602111032 /opt/jdtls
|
||||
RUN chmod +x /opt/jdtls/bin/jdtls /opt/jdtls/bin/jdtls.py
|
||||
|
||||
# ---- Gateway binary ----
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/server /app/server
|
||||
COPY config.example.json /app/config.example.json
|
||||
|
||||
ENV PORT=8080
|
||||
# ---- Default config: all 4 LSP servers, Redis/Nacos off by default ----
|
||||
COPY <<'EOF' /app/config.json
|
||||
{
|
||||
"port": "8080",
|
||||
"workspaceDir": "/workspace",
|
||||
"allowOrigin": "*",
|
||||
"requestTimeout": "30s",
|
||||
"sessionTTL": "20m",
|
||||
"cleanupInterval": "2m",
|
||||
"maxSessions": 256,
|
||||
"enableRedis": false,
|
||||
"enableNacosRegister": false,
|
||||
"appEnv": "prod",
|
||||
"logLevel": "info",
|
||||
"logConsoleEnabled": true,
|
||||
"servers": [
|
||||
{ "language": "go", "languageId": "go", "command": "gopls", "args": [] },
|
||||
{ "language": "javascript", "languageId": "javascript", "command": "typescript-language-server", "args": ["--stdio"] },
|
||||
{ "language": "typescript", "languageId": "typescript", "command": "typescript-language-server", "args": ["--stdio"] },
|
||||
{ "language": "java", "languageId": "java", "command": "/opt/jdtls/bin/jdtls", "args": [] }
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# ---- Directories & non-root user ----
|
||||
RUN mkdir -p /workspace /go && \
|
||||
groupadd --system app && \
|
||||
useradd --system --gid app --home-dir /home/app --create-home app && \
|
||||
chown -R app:app /app /workspace /go /home/app
|
||||
|
||||
ENV PORT=8080 \
|
||||
WORKSPACE_DIR=/workspace
|
||||
EXPOSE 8080
|
||||
|
||||
USER app
|
||||
|
||||
53
backend/Makefile
Normal file
53
backend/Makefile
Normal file
@@ -0,0 +1,53 @@
|
||||
GO ?= go
|
||||
CMD_DIR := ./cmd/server
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
EXE := .exe
|
||||
else
|
||||
EXE :=
|
||||
endif
|
||||
|
||||
BINARY := lsp-gateway$(EXE)
|
||||
CONFIG_FILE ?= ./config.json
|
||||
|
||||
.PHONY: help tidy fmt vet test build run run-config clean
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " make build Build binary ($(BINARY))"
|
||||
@echo " make run Run service with current env"
|
||||
@echo " make run-config Run service with CONFIG_FILE=$(CONFIG_FILE)"
|
||||
@echo " make test Run all tests"
|
||||
@echo " make fmt Format Go code"
|
||||
@echo " make vet Run go vet"
|
||||
@echo " make tidy Sync go.mod/go.sum"
|
||||
@echo " make clean Remove built binary"
|
||||
|
||||
tidy:
|
||||
$(GO) mod tidy
|
||||
|
||||
fmt:
|
||||
$(GO) fmt ./...
|
||||
|
||||
vet:
|
||||
$(GO) vet ./...
|
||||
|
||||
test:
|
||||
$(GO) test ./...
|
||||
|
||||
build:
|
||||
$(GO) build -o $(BINARY) $(CMD_DIR)
|
||||
|
||||
run:
|
||||
$(GO) run $(CMD_DIR)
|
||||
|
||||
run-config:
|
||||
CONFIG_FILE=$(CONFIG_FILE) $(GO) run $(CMD_DIR)
|
||||
|
||||
clean:
|
||||
-$(GO) clean
|
||||
ifeq ($(OS),Windows_NT)
|
||||
-powershell -NoProfile -Command "if (Test-Path '$(BINARY)') { Remove-Item -Force '$(BINARY)' }"
|
||||
else
|
||||
-rm -f $(BINARY)
|
||||
endif
|
||||
@@ -10,6 +10,13 @@
|
||||
- 按 `language + sessionId` 维护长生命周期会话
|
||||
- 会话空闲自动回收(TTL)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- Spring Cloud Gateway 对接:`docs/spring-cloud-gateway-integration.md`
|
||||
- Java + Nacos 对接:`docs/java-nacos-integration-guide.md`
|
||||
- 前端对接:`docs/frontend-integration.md`
|
||||
- 前端经 Gateway 对接(AIOJ):`../docs/lsp-golang-gateway-frontend-integration.md`
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
@@ -119,6 +126,7 @@ NACOS_PORT=8080
|
||||
- `GET /health`
|
||||
- `GET /health/live`
|
||||
- `GET /health/ready`(含当前会话统计)
|
||||
- `GET /health/lsp-status`(按语言的 LSP 在线探测状态)
|
||||
|
||||
## HTTP 补全接口
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"monica-go-completion-backend/internal/completion"
|
||||
"monica-go-completion-backend/internal/logging"
|
||||
"monica-go-completion-backend/internal/lsp"
|
||||
"monica-go-completion-backend/internal/monitor"
|
||||
)
|
||||
|
||||
var requestIDSeed atomic.Int64
|
||||
@@ -235,14 +236,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
manager := completion.NewManager(completion.ManagerConfig{
|
||||
WorkspaceDir: cfg.WorkspaceDir,
|
||||
MaxSessions: cfg.MaxSessions,
|
||||
SessionTTL: cfg.SessionTTL,
|
||||
CleanupInterval: cfg.CleanupInterval,
|
||||
InstanceID: cfg.InstanceID,
|
||||
Registry: registry,
|
||||
}, cfg.Servers, func(ctx context.Context, spec completion.LanguageServerSpec, workspaceDir string) (completion.RuntimeClient, error) {
|
||||
clientFactory := func(ctx context.Context, spec completion.LanguageServerSpec, workspaceDir string) (completion.RuntimeClient, error) {
|
||||
return lsp.NewClient(ctx, lsp.Config{
|
||||
Command: spec.Command,
|
||||
Args: spec.Args,
|
||||
@@ -250,11 +244,31 @@ func main() {
|
||||
LanguageID: spec.LanguageID,
|
||||
ClientName: "monica-lsp-gateway",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
manager := completion.NewManager(completion.ManagerConfig{
|
||||
WorkspaceDir: cfg.WorkspaceDir,
|
||||
MaxSessions: cfg.MaxSessions,
|
||||
SessionTTL: cfg.SessionTTL,
|
||||
CleanupInterval: cfg.CleanupInterval,
|
||||
InstanceID: cfg.InstanceID,
|
||||
Registry: registry,
|
||||
}, cfg.Servers, clientFactory)
|
||||
defer func() {
|
||||
_ = manager.Close()
|
||||
}()
|
||||
|
||||
// 后台预热各语言 LSP 会话,首次请求无需等待冷启动。
|
||||
go manager.WarmUp(context.Background())
|
||||
|
||||
// 后台探测各语言 LSP 可用性(demo 实现:周期性握手探测)。
|
||||
lspStatusMonitor := monitor.NewLSPStatusMonitor(cfg.Servers, cfg.WorkspaceDir, clientFactory, monitor.Config{
|
||||
ProbeInterval: 60 * time.Second,
|
||||
ProbeTimeout: 30 * time.Second,
|
||||
FailureThreshold: 2,
|
||||
})
|
||||
defer lspStatusMonitor.Close()
|
||||
|
||||
// 注册通用中间件与业务路由。
|
||||
router := gin.New()
|
||||
if err := configureProxySettings(router, cfg); err != nil {
|
||||
@@ -267,8 +281,9 @@ func main() {
|
||||
router.Use(apiTokenMiddleware(cfg.APIToken))
|
||||
|
||||
api.RegisterRoutes(router, manager, api.RouteOptions{
|
||||
RequestTimeout: cfg.RequestTimeout,
|
||||
MaxBodyBytes: cfg.MaxBodyBytes,
|
||||
RequestTimeout: cfg.RequestTimeout,
|
||||
MaxBodyBytes: cfg.MaxBodyBytes,
|
||||
LSPStatusProvider: lspStatusMonitor,
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
"args": [
|
||||
"--stdio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "java",
|
||||
"languageId": "java",
|
||||
"command": "/opt/jdtls/bin/jdtls",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
"args": [
|
||||
"--stdio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"language": "java",
|
||||
"languageId": "java",
|
||||
"command": "jdt-language-server-1.57.0-202602111032/jdtls.bat",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# Go LSP Gateway 接入 Java 微服务(Nacos)指南
|
||||
|
||||
## 1. 结论先说
|
||||
|
||||
可以。
|
||||
你完全可以把当前 Go LSP Gateway 注册到 Nacos,然后由 Java 微服务通过服务名发现并转发请求。
|
||||
|
||||
但要注意:
|
||||
- Nacos 解决的是「服务发现」问题。
|
||||
- LSP 场景还需要「会话粘性」问题(同 `sessionId` 要持续命中同一实例)。
|
||||
- 粘性在你现在的实现里由 Redis 会话目录 + `routeTo` 重试机制承担,Nacos 本身不替代这部分。
|
||||
|
||||
## 2. 当前 Go 服务能力(已具备)
|
||||
|
||||
对外接口:
|
||||
- HTTP: `POST /api/v1/completions/{language}`
|
||||
- WS: `GET /ws/completions`、`GET /ws/completions/{language}`
|
||||
- 健康检查:`/health`、`/health/live`、`/health/ready`
|
||||
|
||||
多副本会话能力:
|
||||
- Redis 会话外置(默认 `10.0.0.10:6379`, DB `1`)
|
||||
- 会话归属冲突返回 `409`,并带 `routeTo` + `X-LSP-Route-To`
|
||||
|
||||
## 3. 推荐架构
|
||||
|
||||
1. 前端 -> Java Gateway/BFF -> Go LSP Gateway
|
||||
2. Go LSP Gateway 多副本部署(Nacos 注册)
|
||||
3. Redis 作为会话目录(`language + sessionId -> owner instance`)
|
||||
|
||||
## 4. Nacos 接入方式
|
||||
|
||||
你有两种落地方式:
|
||||
|
||||
1. 由部署系统注册(推荐)
|
||||
指 K8s/运维平台在发布时自动调用 Nacos OpenAPI 注册实例,Go 程序本身不依赖 Nacos SDK。
|
||||
2. Go 进程内注册
|
||||
在 Go 服务启动时使用 `nacos-sdk-go` 注册/心跳/下线。
|
||||
|
||||
你当前明确要求走 SDK,本项目已支持方式 2。
|
||||
|
||||
## 5. Java 侧必须做的事
|
||||
|
||||
## 5.1 统一转发入口
|
||||
|
||||
Java 提供统一接口给前端,例如:
|
||||
- `POST /editor/completions/{language}`
|
||||
|
||||
内部转发到:
|
||||
- `http://{lsp-service}/api/v1/completions/{language}`
|
||||
|
||||
## 5.2 强制透传稳定 sessionId
|
||||
|
||||
请求体必须带稳定 `sessionId`,建议格式:
|
||||
- `tenant:user:project:tab`
|
||||
|
||||
如果 `sessionId` 不稳定,会导致会话频繁重建、补全抖动。
|
||||
|
||||
## 5.3 处理 409 routeTo(关键)
|
||||
|
||||
当 Go 返回 `409` 时:
|
||||
- 读取响应体 `routeTo`(或 header `X-LSP-Route-To`)
|
||||
- Java 侧自动重试一次到 `routeTo`
|
||||
|
||||
这样可以跨实例正确命中会话拥有者。
|
||||
|
||||
## 5.4 透传请求头
|
||||
|
||||
- `X-Request-Id`(链路追踪)
|
||||
- `X-API-Key`(若 Go 配置了 `LSP_API_TOKEN`)
|
||||
|
||||
## 6. Spring Cloud Alibaba(Nacos)示例
|
||||
|
||||
## 6.1 application.yml(服务发现)
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
application:
|
||||
name: editor-bff
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 10.0.0.20:8848
|
||||
```
|
||||
|
||||
Go 服务在 Nacos 中注册名假设为:`lsp-gateway`
|
||||
|
||||
## 6.2 Go 侧与 Spring 配置映射(SDK 注册)
|
||||
|
||||
你的 Java 配置:
|
||||
|
||||
```yaml
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: true
|
||||
register-enabled: true
|
||||
server-addr: 10.0.0.10:8848
|
||||
username: nacos
|
||||
password: nacos
|
||||
```
|
||||
|
||||
对应 Go 环境变量:
|
||||
|
||||
- `ENABLE_NACOS_REGISTER=true`
|
||||
- `NACOS_SERVER_ADDR=10.0.0.10:8848`
|
||||
- `NACOS_USERNAME=nacos`
|
||||
- `NACOS_PASSWORD=nacos`
|
||||
- `NACOS_SERVICE_NAME=lsp-gateway`
|
||||
- `NACOS_GROUP=DEFAULT_GROUP`
|
||||
- `NACOS_IP=<当前实例可达IP>`
|
||||
- `NACOS_PORT=8080`(或你的服务端口)
|
||||
|
||||
## 6.3 Gateway 路由示例(HTTP + WS)
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: lsp-http
|
||||
uri: lb://lsp-gateway
|
||||
predicates:
|
||||
- Path=/lsp/api/**
|
||||
filters:
|
||||
- RewritePath=/lsp/api/(?<segment>.*), /api/$\{segment}
|
||||
|
||||
- id: lsp-ws
|
||||
uri: lb:ws://lsp-gateway
|
||||
predicates:
|
||||
- Path=/lsp/ws/**
|
||||
filters:
|
||||
- RewritePath=/lsp/ws/(?<segment>.*), /ws/$\{segment}
|
||||
```
|
||||
|
||||
## 6.4 BFF 转发逻辑(WebClient 伪代码)
|
||||
|
||||
```java
|
||||
Mono<ResponseEntity<String>> proxyCompletion(String language, String body, HttpHeaders headers) {
|
||||
return call("lb://lsp-gateway/api/v1/completions/" + language, body, headers)
|
||||
.flatMap(resp -> {
|
||||
if (resp.getStatusCodeValue() != 409) return Mono.just(resp);
|
||||
|
||||
String routeTo = extractRouteTo(resp); // from body.routeTo or X-LSP-Route-To
|
||||
if (routeTo == null || routeTo.isBlank()) return Mono.just(resp);
|
||||
|
||||
String direct = routeTo + "/api/v1/completions/" + language;
|
||||
return call(direct, body, headers); // retry once
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Go 服务部署参数建议(生产)
|
||||
|
||||
- `ENABLE_REDIS_STICKY=true`
|
||||
- `REDIS_ADDR=10.0.0.10:6379`
|
||||
- `REDIS_DB=1`
|
||||
- `REDIS_PASSWORD=`(空)
|
||||
- `INSTANCE_ID`:每个实例唯一(建议 Pod 名)
|
||||
- `INSTANCE_URL`:实例可回源地址(供 routeTo 使用)
|
||||
- `ENABLE_NACOS_REGISTER=true`
|
||||
- `NACOS_SERVER_ADDR=10.0.0.10:8848`
|
||||
- `NACOS_USERNAME=nacos`
|
||||
- `NACOS_PASSWORD=nacos`
|
||||
- `NACOS_IP`:实例可达内网 IP(不要填 127.0.0.1)
|
||||
- `NACOS_PORT`:实例监听端口
|
||||
- `MAX_SESSIONS`:按机器资源评估(如 200~500)
|
||||
- `SESSION_TTL`:建议 10~30 分钟
|
||||
|
||||
## 8. 常见误区
|
||||
|
||||
1. “用了 Nacos 就不需要 Redis 会话目录”
|
||||
错。Nacos 只告诉你“有哪些实例”,不维护“某会话属于哪台实例”。
|
||||
|
||||
2. “随机 LB 也能跑”
|
||||
能跑但体验会抖,LSP 上下文会丢。
|
||||
|
||||
3. “不传 sessionId 也没关系”
|
||||
错。会话粘性依赖稳定 sessionId。
|
||||
|
||||
## 9. 联调 checklist
|
||||
|
||||
1. Nacos 中能看到 `lsp-gateway` 实例。
|
||||
2. Java 转发 HTTP 可通。
|
||||
3. 同一 `sessionId` 连续请求响应稳定。
|
||||
4. 人为让请求打到非 owner 实例时,Java 能按 `routeTo` 自动重试成功。
|
||||
5. WS 通道可建立,断线重连后会话仍可恢复。
|
||||
@@ -1,94 +0,0 @@
|
||||
# 为什么要做 Redis 会话外置 + 粘性路由
|
||||
|
||||
## 背景
|
||||
|
||||
当前 LSP 网关的核心能力是:
|
||||
- 把编辑器请求转成 LSP(JSON-RPC over stdio)
|
||||
- 为每个 `language + sessionId` 维护一个长生命周期会话(对应语言服务器进程与文档状态)
|
||||
|
||||
单实例时,这套方案天然稳定。
|
||||
一旦进入微服务部署(多副本 + 负载均衡),如果没有额外机制,会出现严重一致性问题。
|
||||
|
||||
## 不做会发生什么
|
||||
|
||||
### 1. 会话状态丢失
|
||||
|
||||
同一个用户会话的请求可能先到实例 A、再到实例 B。
|
||||
但 B 没有 A 的内存态(didOpen/didChange 版本、LSP 上下文),会导致:
|
||||
- 补全质量波动
|
||||
- 诊断/跳转不一致
|
||||
- 重复初始化语言服务器
|
||||
|
||||
### 2. 成本放大
|
||||
|
||||
会话不稳定会触发频繁建进程,带来:
|
||||
- 更高 CPU/内存
|
||||
- 更高请求尾延迟(P95/P99)
|
||||
- 更多瞬时失败
|
||||
|
||||
### 3. 故障不可控
|
||||
|
||||
无统一会话归属时,问题难定位(到底是哪台实例持有上下文、何时丢失)。
|
||||
|
||||
## 为什么要 Redis 会话外置
|
||||
|
||||
Redis 不是替代 LSP 进程,而是做“会话目录(Session Directory)”:
|
||||
- 记录某个 `language + sessionId` 当前归属哪个实例
|
||||
- 记录实例心跳与可回源地址
|
||||
- 给会话绑定 TTL,支持自动过期与回收
|
||||
|
||||
它带来的价值:
|
||||
- 多副本下会话归属可见、可控
|
||||
- 实例重启/扩容/缩容时路由行为可预测
|
||||
- 可以做统一治理(观测、告警、自动修复)
|
||||
|
||||
## 为什么要粘性路由
|
||||
|
||||
LSP 天然是“有状态协议”:同一会话必须持续命中同一实例才能复用上下文。
|
||||
粘性路由的目标就是保证这一点。
|
||||
|
||||
网关当前策略:
|
||||
1. 请求到达后先在 Redis `claim` 会话归属
|
||||
2. 如果归属自己:本地处理
|
||||
3. 如果归属其他实例:返回 `409 + routeTo`(HTTP 头 `X-LSP-Route-To`)
|
||||
4. 上游(前端或 Java 网关)据此重试到目标实例
|
||||
|
||||
这比纯随机 LB 更符合 LSP 工作方式。
|
||||
|
||||
## 设计取舍
|
||||
|
||||
### 收益
|
||||
- 会话一致性显著提升
|
||||
- 进程复用率提高,资源更稳
|
||||
- 故障域清晰,便于排障
|
||||
|
||||
### 代价
|
||||
- 引入 Redis 依赖(网络与可用性要保障)
|
||||
- 路由逻辑更复杂(冲突重试、TTL 管理)
|
||||
- 需要对上游调用方约束:必须稳定传 `sessionId`
|
||||
|
||||
## 什么时候可以不做
|
||||
|
||||
可以暂不启用 Redis/粘性路由的场景:
|
||||
- 仅单实例部署
|
||||
- 开发/演示环境
|
||||
- 对补全一致性不敏感
|
||||
|
||||
但只要进入生产多副本,建议启用。
|
||||
|
||||
## 适配 Java 微服务体系的意义
|
||||
|
||||
将 LSP 网关作为独立微服务后:
|
||||
- Java 业务服务无需关心各语言 LSP 细节
|
||||
- 可以统一接入鉴权、限流、链路追踪
|
||||
- LSP 能力扩展(Go/JS/TS/Java/Python)不会反复侵入业务服务
|
||||
|
||||
这就是把它做成独立 LSP 服务的核心原因:**把状态复杂性收敛在一个可治理的边界里**。
|
||||
|
||||
## 当前实现对应点
|
||||
|
||||
- Redis 默认配置:`10.0.0.10:6379`, `DB=1`, 无密码
|
||||
- 会话注册与认领:`internal/cluster/redis_registry.go`
|
||||
- 会话管理与归属检查:`internal/completion/manager.go`
|
||||
- 路由冲突返回:`internal/api/handler.go`, `internal/api/ws_handler.go`
|
||||
- 启动配置入口:`cmd/server/main.go`
|
||||
@@ -22,10 +22,17 @@ type SessionStatsProvider interface {
|
||||
ActiveSessions() map[string]int
|
||||
}
|
||||
|
||||
// LSPStatusProvider 暴露按语言的 LSP 探测状态。
|
||||
type LSPStatusProvider interface {
|
||||
LspServiceStatus() map[string]any
|
||||
}
|
||||
|
||||
// RouteOptions 控制 HTTP/WS 接口的超时与请求体上限。
|
||||
type RouteOptions struct {
|
||||
RequestTimeout time.Duration // 单次补全调用超时时间。
|
||||
MaxBodyBytes int64 // 请求体最大字节数(HTTP/WS 共用)。
|
||||
// LSPStatusProvider 可选,用于输出语言服务在线状态。
|
||||
LSPStatusProvider LSPStatusProvider
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册健康检查、HTTP 补全接口和 WebSocket 补全接口。
|
||||
@@ -41,6 +48,7 @@ func RegisterRoutes(router *gin.Engine, service CompletionService, options ...Ro
|
||||
if options[0].MaxBodyBytes > 0 {
|
||||
opts.MaxBodyBytes = options[0].MaxBodyBytes
|
||||
}
|
||||
opts.LSPStatusProvider = options[0].LSPStatusProvider
|
||||
}
|
||||
|
||||
router.GET("/health", func(c *gin.Context) {
|
||||
@@ -62,6 +70,17 @@ func RegisterRoutes(router *gin.Engine, service CompletionService, options ...Ro
|
||||
})
|
||||
})
|
||||
|
||||
router.GET("/health/lsp-status", func(c *gin.Context) {
|
||||
languages := map[string]any{}
|
||||
if opts.LSPStatusProvider != nil {
|
||||
languages = opts.LSPStatusProvider.LspServiceStatus()
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"languages": languages,
|
||||
})
|
||||
})
|
||||
|
||||
registerWSRoutes(router, service, opts)
|
||||
|
||||
handleCompletion := func(c *gin.Context) {
|
||||
|
||||
@@ -28,6 +28,14 @@ func (f *fakeCompletionService) Complete(_ context.Context, _ completion.Request
|
||||
return f.resp, nil
|
||||
}
|
||||
|
||||
type fakeLSPStatusProvider struct {
|
||||
status map[string]any
|
||||
}
|
||||
|
||||
func (f *fakeLSPStatusProvider) LspServiceStatus() map[string]any {
|
||||
return f.status
|
||||
}
|
||||
|
||||
// 验证 HTTP 补全接口的成功路径。
|
||||
func TestRegisterRoutesCompletionSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -174,3 +182,41 @@ func TestRegisterRoutesCompletionWebSocketSuccess(t *testing.T) {
|
||||
t.Fatalf("unexpected items: %+v", resp.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 /health/lsp-status 会返回语言探测状态快照。
|
||||
func TestRegisterRoutesLspStatus(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
RegisterRoutes(r, &fakeCompletionService{}, RouteOptions{
|
||||
LSPStatusProvider: &fakeLSPStatusProvider{
|
||||
status: map[string]any{
|
||||
"go": map[string]any{
|
||||
"online": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health/lsp-status", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if got["status"] != "ok" {
|
||||
t.Fatalf("expected status ok, got %#v", got["status"])
|
||||
}
|
||||
languages, ok := got["languages"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("languages field type mismatch: %#v", got["languages"])
|
||||
}
|
||||
if _, ok := languages["go"]; !ok {
|
||||
t.Fatalf("expected go language status, got %#v", languages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var ErrUnsupportedLanguage = errors.New("unsupported language")
|
||||
@@ -38,12 +40,13 @@ type ClientFactory func(ctx context.Context, spec LanguageServerSpec, workspaceD
|
||||
|
||||
// ManagerConfig 控制会话池容量、TTL 与实例信息。
|
||||
type ManagerConfig struct {
|
||||
WorkspaceDir string // LSP 进程工作区目录。
|
||||
MaxSessions int // 本实例会话上限。
|
||||
SessionTTL time.Duration // 会话空闲超时。
|
||||
CleanupInterval time.Duration // 会话清理周期。
|
||||
InstanceID string // 当前实例 ID。
|
||||
Registry SessionRegistry // 可选分布式会话注册中心。
|
||||
WorkspaceDir string // LSP 进程工作区目录。
|
||||
MaxSessions int // 本实例会话上限。
|
||||
SessionTTL time.Duration // 会话空闲超时。
|
||||
CleanupInterval time.Duration // 会话清理周期。
|
||||
InstanceID string // 当前实例 ID。
|
||||
Registry SessionRegistry // 可选分布式会话注册中心。
|
||||
SessionInitTimeout time.Duration // LSP 客户端初始化超时(独立于请求超时)。
|
||||
}
|
||||
|
||||
// Manager 按 language/session 复用 LSP 会话,并负责清理与淘汰。
|
||||
@@ -92,6 +95,9 @@ func NewManager(config ManagerConfig, specs []LanguageServerSpec, factory Client
|
||||
if config.CleanupInterval <= 0 {
|
||||
config.CleanupInterval = 2 * time.Minute
|
||||
}
|
||||
if config.SessionInitTimeout <= 0 {
|
||||
config.SessionInitTimeout = 60 * time.Second
|
||||
}
|
||||
if strings.TrimSpace(config.InstanceID) == "" {
|
||||
config.InstanceID = "instance-local"
|
||||
}
|
||||
@@ -116,6 +122,34 @@ func NewManager(config ManagerConfig, specs []LanguageServerSpec, factory Client
|
||||
return m
|
||||
}
|
||||
|
||||
// WarmUp 预热指定语言的 LSP 会话,使首次请求无需等待冷启动。
|
||||
func (m *Manager) WarmUp(ctx context.Context, languages ...string) {
|
||||
targets := languages
|
||||
if len(targets) == 0 {
|
||||
targets = make([]string, 0, len(m.specByLang))
|
||||
for lang := range m.specByLang {
|
||||
targets = append(targets, lang)
|
||||
}
|
||||
}
|
||||
for _, lang := range targets {
|
||||
lang = normalizeLanguage(lang)
|
||||
spec, ok := m.specByLang[lang]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sessionKey := buildSessionKey(lang, "default")
|
||||
_, err := m.getOrCreateSession(ctx, sessionKey, "default", spec)
|
||||
if err != nil {
|
||||
zap.L().Warn("warm-up failed",
|
||||
zap.String("language", lang),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
zap.L().Info("warm-up succeeded", zap.String("language", lang))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete 处理补全请求,包含语言匹配、会话归属、会话复用/创建。
|
||||
func (m *Manager) Complete(ctx context.Context, req Request) (Response, error) {
|
||||
language := normalizeLanguage(req.Language)
|
||||
@@ -253,7 +287,11 @@ func (m *Manager) getOrCreateSession(
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
client, err := m.newClient(ctx, spec, m.config.WorkspaceDir)
|
||||
// 使用独立超时创建 LSP 客户端,避免被较短的请求超时截断。
|
||||
initCtx, initCancel := context.WithTimeout(context.Background(), m.config.SessionInitTimeout)
|
||||
defer initCancel()
|
||||
|
||||
client, err := m.newClient(initCtx, spec, m.config.WorkspaceDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ func NewClient(parent context.Context, cfg Config) (*Client, error) {
|
||||
cfg.ClientName = "monica-lsp-gateway"
|
||||
}
|
||||
|
||||
cmd := exec.Command(cfg.Command, cfg.Args...)
|
||||
cmd := exec.Command(filepath.FromSlash(cfg.Command), cfg.Args...)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create stdin pipe: %w", err)
|
||||
@@ -138,7 +138,7 @@ func NewClient(parent context.Context, cfg Config) (*Client, error) {
|
||||
// 独立协程持续读取 stdout 并分发响应。
|
||||
go client.readLoop(stdout)
|
||||
|
||||
initCtx, cancel := context.WithTimeout(parent, 10*time.Second)
|
||||
initCtx, cancel := context.WithTimeout(parent, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.initialize(initCtx, cfg.RootPath); err != nil {
|
||||
@@ -277,7 +277,20 @@ func (c *Client) Completion(ctx context.Context, uri string, line, character int
|
||||
|
||||
var windowsDrivePattern = regexp.MustCompile(`^[A-Za-z]:`)
|
||||
|
||||
// normalizeURI 将相对 file URI 重写为工作区绝对路径 URI。
|
||||
// languageExtensions 定义 languageId 对应的规范文件扩展名。
|
||||
var languageExtensions = map[string]string{
|
||||
"java": ".java",
|
||||
"go": ".go",
|
||||
"javascript": ".js",
|
||||
"typescript": ".ts",
|
||||
"python": ".py",
|
||||
"c": ".c",
|
||||
"cpp": ".cpp",
|
||||
"rust": ".rs",
|
||||
}
|
||||
|
||||
// normalizeURI 将相对 file URI 重写为工作区绝对路径 URI,
|
||||
// 并将不匹配 languageId 的扩展名(如 .txt)替换为语言对应扩展名。
|
||||
func (c *Client) normalizeURI(rawURI string) (string, error) {
|
||||
if rawURI == "" {
|
||||
return "", errors.New("empty uri")
|
||||
@@ -305,6 +318,14 @@ func (c *Client) normalizeURI(rawURI string) (string, error) {
|
||||
localPath = filepath.Join(c.workspaceDir, filepath.FromSlash(rel))
|
||||
}
|
||||
|
||||
// 若文件扩展名与 languageId 不匹配(如 .txt),替换为语言对应扩展名。
|
||||
if expectedExt, ok := languageExtensions[strings.ToLower(c.languageID)]; ok {
|
||||
currentExt := strings.ToLower(filepath.Ext(localPath))
|
||||
if currentExt != expectedExt {
|
||||
localPath = strings.TrimSuffix(localPath, filepath.Ext(localPath)) + expectedExt
|
||||
}
|
||||
}
|
||||
|
||||
return pathToURI(localPath)
|
||||
}
|
||||
|
||||
|
||||
180
backend/internal/monitor/lsp_status_monitor.go
Normal file
180
backend/internal/monitor/lsp_status_monitor.go
Normal 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))
|
||||
}
|
||||
109
backend/internal/monitor/lsp_status_monitor_test.go
Normal file
109
backend/internal/monitor/lsp_status_monitor_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebSearch"
|
||||
]
|
||||
}
|
||||
}
|
||||
26
backend/jdt-language-server-1.57.0-202602111032/bin/jdtls
Normal file
26
backend/jdt-language-server-1.57.0-202602111032/bin/jdtls
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
###############################################################################
|
||||
# Copyright (c) 2022 Marc Schreiber and others.
|
||||
#
|
||||
# This program and the accompanying materials are made available under the
|
||||
# terms of the Eclipse Public License 2.0 which is available at
|
||||
# http://www.eclipse.org/legal/epl-2.0.
|
||||
#
|
||||
# SPDX-License-Identifier: EPL-2.0
|
||||
#
|
||||
# Contributors:
|
||||
# Marc Schreiber - initial API and implementation
|
||||
###############################################################################
|
||||
import importlib.util
|
||||
import sys
|
||||
import os
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
file_path = os.path.join(script_dir, "jdtls.py")
|
||||
|
||||
spec = importlib.util.spec_from_file_location("jdtls", file_path)
|
||||
jdtls = importlib.util.module_from_spec(spec)
|
||||
sys.modules["jdtls"] = jdtls
|
||||
spec.loader.exec_module(jdtls)
|
||||
|
||||
jdtls.main(sys.argv[1:])
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
python %~dp0/jdtls %*
|
||||
pause
|
||||
136
backend/jdt-language-server-1.57.0-202602111032/bin/jdtls.py
Normal file
136
backend/jdt-language-server-1.57.0-202602111032/bin/jdtls.py
Normal file
@@ -0,0 +1,136 @@
|
||||
###############################################################################
|
||||
# Copyright (c) 2022 Marc Schreiber and others.
|
||||
#
|
||||
# This program and the accompanying materials are made available under the
|
||||
# terms of the Eclipse Public License 2.0 which is available at
|
||||
# http://www.eclipse.org/legal/epl-2.0.
|
||||
#
|
||||
# SPDX-License-Identifier: EPL-2.0
|
||||
#
|
||||
# Contributors:
|
||||
# Marc Schreiber - initial API and implementation
|
||||
###############################################################################
|
||||
import argparse
|
||||
from hashlib import sha1
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
def get_java_executable(known_args):
|
||||
if known_args.java_executable is not None:
|
||||
java_executable = known_args.java_executable
|
||||
else:
|
||||
java_executable = 'java'
|
||||
|
||||
if 'JAVA_HOME' in os.environ:
|
||||
ext = '.exe' if platform.system() == 'Windows' else ''
|
||||
java_exec_to_test = Path(os.environ['JAVA_HOME']) / 'bin' / f'java{ext}'
|
||||
if java_exec_to_test.is_file():
|
||||
java_executable = str(java_exec_to_test.resolve())
|
||||
|
||||
if not known_args.validate_java_version:
|
||||
return java_executable
|
||||
|
||||
java_major_version = get_java_major_version(java_executable)
|
||||
|
||||
if java_major_version < 21:
|
||||
raise Exception("jdtls requires at least Java 21")
|
||||
|
||||
return java_executable
|
||||
|
||||
def get_java_major_version (java_executable):
|
||||
out = subprocess.check_output([java_executable, '-version'], stderr = subprocess.STDOUT, universal_newlines=True)
|
||||
|
||||
matches = re.finditer(r"(?<=version\s\")(?P<major>\d+)(\.\d+\.\d+(_\d+)?)?", out)
|
||||
for match in matches:
|
||||
return int(match.group("major"))
|
||||
|
||||
raise Exception("Could not determine Java version")
|
||||
|
||||
def find_equinox_launcher(jdtls_base_directory):
|
||||
plugins_dir = jdtls_base_directory / "plugins"
|
||||
if (plugins_dir / 'org.eclipse.equinox.launcher.jar').is_file():
|
||||
# mason-registry packaging
|
||||
return str(plugins_dir / 'org.eclipse.equinox.launcher.jar')
|
||||
|
||||
launchers = plugins_dir.glob('org.eclipse.equinox.launcher_*.jar')
|
||||
for launcher in launchers:
|
||||
return str(plugins_dir / launcher)
|
||||
|
||||
raise Exception("Cannot find equinox launcher")
|
||||
|
||||
def get_shared_config_path(jdtls_base_path):
|
||||
system = platform.system()
|
||||
|
||||
if system in ['Linux', 'FreeBSD']:
|
||||
config_dir = 'config_linux'
|
||||
elif system == 'Darwin':
|
||||
config_dir = 'config_mac'
|
||||
elif system == 'Windows':
|
||||
config_dir = 'config_win'
|
||||
else:
|
||||
raise Exception("Unknown platform {} detected".format(system))
|
||||
|
||||
return str(jdtls_base_path / config_dir)
|
||||
|
||||
def main(args):
|
||||
cwd_name = os.path.basename(os.getcwd())
|
||||
|
||||
system = platform.system()
|
||||
|
||||
if system == 'Windows' and 'APPDATA' in os.environ:
|
||||
cachedir = Path(os.environ['APPDATA'])
|
||||
elif system == 'Darwin' and 'HOME' in os.environ:
|
||||
cachedir = Path(os.environ['HOME']) / 'Library' / 'Caches'
|
||||
elif system == 'Linux' and 'HOME' in os.environ:
|
||||
cachedir = Path(os.environ['HOME']) / '.cache'
|
||||
else:
|
||||
cachedir = Path(tempfile.gettempdir())
|
||||
|
||||
cachedir = cachedir / 'jdtls'
|
||||
jdtls_data_path = os.path.join(cachedir, "jdtls-" + sha1(cwd_name.encode()).hexdigest())
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--validate-java-version', action='store_true', default=True)
|
||||
parser.add_argument('--no-validate-java-version', dest='validate_java_version', action='store_false')
|
||||
parser.add_argument("--java-executable", help="Path to java executable used to start runtime.")
|
||||
parser.add_argument("--jvm-arg",
|
||||
default=[],
|
||||
action="append",
|
||||
help="An additional JVM option (can be used multiple times. Note, use with equal sign. For example: --jvm-arg=-Dlog.level=ALL")
|
||||
parser.add_argument("-data", default=jdtls_data_path)
|
||||
|
||||
known_args, args = parser.parse_known_args(args)
|
||||
java_executable = get_java_executable(known_args)
|
||||
java_major_version = get_java_major_version(java_executable)
|
||||
|
||||
jdtls_base_path = Path(__file__).parent.parent
|
||||
shared_config_path = get_shared_config_path(jdtls_base_path)
|
||||
jar_path = find_equinox_launcher(jdtls_base_path)
|
||||
|
||||
exec_args = ["-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-Dosgi.checkConfiguration=true",
|
||||
"-Dosgi.sharedConfiguration.area=" + shared_config_path,
|
||||
"-Dosgi.sharedConfiguration.area.readOnly=true",
|
||||
"-Dosgi.configuration.cascaded=true",
|
||||
"-Xms1G",
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens", "java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED"] \
|
||||
+ known_args.jvm_arg \
|
||||
+ ["-jar", jar_path,
|
||||
"-data", known_args.data] \
|
||||
+ args
|
||||
|
||||
if (java_major_version >= 24):
|
||||
exec_args = [ '-Djdk.xml.maxGeneralEntitySizeLimit=0', '-Djdk.xml.totalEntitySizeLimit=0' ] + exec_args
|
||||
|
||||
if os.name == 'posix':
|
||||
os.execvp(java_executable, exec_args)
|
||||
else:
|
||||
subprocess.run([java_executable] + exec_args)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
#safe table
|
||||
#Sun Feb 15 22:37:38 CST 2026
|
||||
.contributions=5
|
||||
.contributors=5
|
||||
.extraData=5
|
||||
.mainData=5
|
||||
.namespaces=5
|
||||
.orphans=5
|
||||
.table=5
|
||||
.crca8161cbd.v1
|
||||
@@ -0,0 +1,10 @@
|
||||
#safe table
|
||||
#Sun Feb 15 22:38:43 CST 2026
|
||||
.contributions=6
|
||||
.contributors=6
|
||||
.extraData=6
|
||||
.mainData=6
|
||||
.namespaces=6
|
||||
.orphans=6
|
||||
.table=6
|
||||
.crc552025a9.v1
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
#safe table
|
||||
#Sun Feb 15 22:38:50 CST 2026
|
||||
framework.info=192
|
||||
.crcaa8d7f53.v1
|
||||
@@ -0,0 +1,4 @@
|
||||
#safe table
|
||||
#Sun Feb 15 22:39:05 CST 2026
|
||||
framework.info=193
|
||||
.crc09b9ce13.v1
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user