Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ JDK 也一样:`ProjectRuntimeService` 不再问“app 内置的 JDK 在哪”

同一工作区的服务连续三次启动失败后,自动同步直接返回已有失败,直到显式停止/重启或切换工作区;进入 ready 后清零。这保留短暂故障后的重试,但阻止每次编辑器同步重新创建 JVM。这里选择有限尝试而非增加定时器和无限退避;代价是持续失败需要用户修复环境后重新启动服务。

#### 原生日志属于宿主日志会话

Java Support 使用 Equinox 的 `osgi.logfile` 把原生错误日志写入 `${logDir}/jdt.log`,
宿主在启动前提供当前会话的可写目录,插件安装包和工作区索引不承担日志所有权。
`eclipse.log.include.commandline=false` 避免运行参数进入原生文件,单段 1024 KiB、两个备份,
把该日志限制在约 3 MiB;`eclipse.log.level=WARNING` 控制 Equinox 原生日志,JDT 的 `logLevel` 默认同为 WARNING。
用户仍可明确调整既有日志级别,但原生文件不等同于 Core 脱敏后的结构化事件。

依据是 Equinox `EclipseStarter.PROP_LOGFILE` 与 `EquinoxLogWriter` 的官方属性;
不要把 Eclipse workspace 的 `.metadata` 缓存整个搬入日志目录,或复制源码和协议正文作为诊断。
清单依赖已有 `${logDir}` 契约:当前支持的是能提供本机 `logDirectory` 的宿主;远程或缺少目录的宿主按契约拒绝,
不偷偷回退到旧的工作区 `.metadata/.log`。直接 JVM 适配保留这些自定义 `-D` 参数且必须保持幂等。
本轮没有变更既有 `log.protocol` 属性,也没有证据据此宣称协议正文已被关闭;
详细 trace 仍遵循共享日志只接纳 message 摘要、不保存 verbose 载荷的既有边界。
组件参数测试不能替代真实 JDT 启动、日志轮转和完整工作流验收。

### 4. 运行、测试、调试由插件声明后交还宿主

插件一旦声明某个语言,宿主就认为这个语言的进程类能力归插件所有:能力目录(`PluginLanguageProviderCatalogSource`)去掉运行、测试、调试标记,语言包注册表(`LanguagePackRegistry`)不再创建内置的测试提供方,运行和测试服务要求必须有插件的执行模块。这条规则是为 PHP、Go 这类整套由插件提供的语言定的,目的是插件缺失或停用时宿主不偷偷退回内置工具。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,15 @@ macOS 的宿主调用点、类型化写入与诊断收集已落地。PR4 的日

引擎在日志源头选择封闭的事实类型,既有英文 message/detail/error 留作 v1 兼容;v2 使用目录 ID 和标量字段,经过同一个 Core 目录规范化、脱敏与通道推导边界。stderr 的读取状态最多保留 4 KiB 半行,按 LF 切分、去末尾 CR、忽略空行并冲刷 EOF 残余;每单调秒接纳 200 行,后续读取跨窗或结束时汇总丢弃计数。测试直接传毫秒值,不装计时器、不等真实一秒。text 落盘前仍执行 1 KiB 规范化。

`window/logMessage` 与 `window/showMessage` 复用结果词为过去时的 `*.message.logged/shown`;其他通知只记录方法,不把协议正文变成 text。trace 默认关闭,显式开关与发往上游的 `$/setTrace` 共用有序写锁,成功写入后才改变采集状态;只接纳限长并脱敏的 message 摘要,排除可能含 RPC 原始载荷和源码的 verbose。凭据脱敏和截断无法把源码转换为可落盘诊断;旧内存 detail 的兼容不代表允许把它写入 v2。macOS 已删除消息字符串匹配和重复 session 状态事实,按目录元数据取 v2 级别。带 Core 记录的 log 不再触发重复 onLogIssue,宿主自身启动/发现/请求失败仍走既有错误边界。
`window/logMessage` 与 `window/showMessage` 的明确严重度选择独立目录事件:上游 error 为 ERROR、warning 为 WARN、info 为 INFO、详细或未知类型为 DEBUG。未分类 stderr 仅作为 DEBUG 排障证据,不能因为输出在标准错误流就标成警告;最终进程或协议失败在实际失败边界记录 ERROR。其他通知只记录方法,不把协议正文变成 text。trace 默认关闭,显式开关与发往上游的 `$/setTrace` 共用有序写锁,成功写入后才改变采集状态;只接纳限长并脱敏的 message 摘要,排除可能含 RPC 原始载荷和源码的 verbose。凭据脱敏和截断无法把源码转换为可落盘诊断;旧内存 detail 的兼容不代表允许把它写入 v2。macOS 已删除消息字符串匹配和重复 session 状态事实,按目录元数据取 v2 级别。带 Core 记录的 log 不再触发重复 onLogIssue,宿主自身启动/发现/请求失败仍走既有错误边界。

### 官方插件通过新的类型化协议交付事实

官方 Node、Java、Rust SDK 使用独立协商的 `litheLogging` 1,通过 `lithe/log` 把操作结果交给 Core。插件只提供注册过的操作、稳定类别、计数和单调耗时;Core 从已安装插件身份选通道和目录事件。成功终态为 INFO、实际失败为 ERROR、回退和真实恢复为 WARN。高频成功可在注册或当前调用上抑制,失败继续可见;取消不是失败。

采用新协议是因为 stderr 无法可靠表达处理后的结果、原操作身份与字段边界。给自由消息加 `[ERROR]` 不能解决这个问题,还会把错误正文和用户内容带入日志。正确做法是 SDK 在结果边界发送一条类型化终态,处理函数只补它真正拥有的降级;不要先记录错误再返回同一次错误,或把 SQL、HTTP 正文、构建输出写进诊断。

Core 只接纳协商成功且字段符合封闭契约的事实,每个进程会话最多每分钟 600 条,使用现有 256 条队列与单一宿主写入器。非法输入每会话只告警一次,不回显被拒内容;越限只计丢弃数。SDK 初始化阶段最多缓存八条类型化事实,capability 响应后排空;正常终态在 RPC 响应前发出,使 Core 能将 wire 请求编号映射到真实宿主 op。协议与 fixture 以共享插件契约为准,不再维护自由文本日志 API。

详细诊断已开启时,新会话注册可能早于初始化响应。Core 此时只保存最新开关值,等握手完成后把 `$/setTrace` 排在 `initialized` 后;初始化前关闭会撤销待发送的启用。不要在注册时立即写通知,否则严格遵循 LSP 的服务器会丢弃它,宿主却以为已经开启。

Expand All @@ -103,6 +111,8 @@ Core 拥有会话生命周期状态。`*.session.updated` 直接附到已有 `ty

六处非 LSP 直接打印改为进程级 Core 队列:只接纳 owner 为 lithe-core 的目录事实,入队前规范化,最多 256 条,重复待取事实与超额计数,由宿主一次 drain 取走并重置。队列没有文件 writer、后台任务或独立持久化;遍历只记相对路径和稳定错误类别,远程清理只记类别,不记录地址、scratch 名称或输出。现有请求事件流只覆盖单个请求,无法覆盖析构和后台清理,因此选择独立有界 handoff(交给宿主写入的内存队列)。

只靠宿主 drain 取走时,没有其他宿主活动就没有人来取:插件 `lithe/log` 事实、析构和后台清理事实会一直留在内存,既不落盘,也不推给日志窗口。因此 Core 在队列旁保留一个唤醒回调槽位。drain 之后第一条事实或第一次丢弃计数触发一次回调,同一 drain 周期内的后续变化合并,不再回调;drain 在队列锁内清除请求标记,之后入队的事实一定会再次触发。安装时已有待取事实也立即回调。回调在持有槽位锁时同步执行,移除也取这把锁,所以移除返回后 Core 不会再使用宿主的 context,宿主可以释放它。回调不得重入 Core,只负责安排宿主已有的 drain。这个槽位属于已有的进程队列,不是新的日志单例:它不写文件、不持有 sink,服务的日志依赖仍从构造器注入。

`${logDir}` 从启动上下文的绝对 `logDirectory` 展开,共用语言服务与后端启动路径;宿主创建当前应用会话的插件 files 目录。Core 只拼接文本,不创建目录,不从插件安装位置推导可写位置。这些文件属于运行状态,不跨 worktree 复用,不改变签名或 Sparkle delta 源基线。macOS 当前日志会话目录由统一存储器提供,Core plugin_files_path 决定相对部分。平台以 mkdirat/openat/O_NOFOLLOW 逐段创建 0700 目录并拒绝符号链接,避免检查与创建之间被换成越界链接。存储不可用时省略路径;使用变量的模板由 Core 按既有 invalidRequest 拒绝。远程进程无法写本机日志目录,同样省略并拒绝该变量。安装阶段只用未创建的占位目录校验展开。静态只读边界验证与组件测试不等于真实应用 bundle hash smoke。

目录适配器抛出的真实 I/O 错误也按存储不可用处理,由 Core 检查模板是否依赖 `${logDir}`。不使用该变量的插件继续启动;不能把可选日志目录变成全部语言服务、构建、VCS、后端和调试器的前置条件。
Expand All @@ -121,7 +131,7 @@ v1 的 detail/message/level 是兼容边界,必须保持原字节:例如 pro

Core 的目录遍历和 scratch 清理复用 PR1 已声明的 app.search.directory_skipped、app.workspace.directory_skipped 和 app.workspace.scratch_cleanup_failed;error 只含固定消息、稳定类别与固定 domain,不含绝对路径、scratch 名或远程地址。删除重复 app.project.directory_skipped、remote.scratch.cleanup_failed。remote process/upload 按 ClientError 变体映射 timeout、closed、transport、spawn、protocol 或 remote_<协议码>,不保留正文。

CoreLogRecord 只接受标量,保留 event、目录 level、channel、op 和源 ts。LogSink 的 MacLogStore、LogScope 和测试 TypedLogRecorder 三个实现均支持它。MacLogStore 在原 drain/flush/finishSession 节奏批量调用一次 drainCoreEvents,由同一个 writer 分配 seq 并 validateCatalog:true;丢弃汇总转为 app 的 log.dropped,不增加空闲轮询或独立 writer。
CoreLogRecord 只接受标量,保留 event、目录 level、channel、op 和源 ts。LogSink 的 MacLogStore、LogScope 和测试 TypedLogRecorder 三个实现均支持它。MacLogStore 每次 drain、flush 和 finishSession 都批量调用一次 drainCoreEvents,由同一个 writer 分配 seq 并 validateCatalog:true;丢弃汇总转为 app 的 log.dropped。每个进程只有组合根创建的应用 writer 安装 Core 唤醒回调;回调只复用宿主事件已有的 100 ms 单次 drain 调度,已有调度时不重复安排,不增加空闲轮询或独立 writer。测试和其他临时 MacLogStore 不安装,避免两个 writer 争抢同一个进程队列。finishSession 先停止接纳并移除回调,再做最后一次 drain。

详细诊断不在宿主另写分类:会话通道根按 Core `diagnostics.resolveLogChannel` 返回的 category 归入当前开关(lsp→language、vcs→git、plugins→plugins),与持久化 DEBUG 的准入共用 `diagnosticRoots`。当前类别开关、新会话注册和 15 分钟单次到期向活动会话发送 setDetailedDiagnostics,退出时关闭。相同失败状态只记录一次 app.logging.diagnostics_failed,避免 drain→失败日志→drain 循环;改变目标状态允许重试。

Expand All @@ -137,6 +147,19 @@ agent-host、git-host、ide-host 和 lithe-remote 客户端被链接进 Core,

日志门禁随之扩展到这四个 crate。运行在远端主机上的 `lithe-remote-server`(`rust/lithe-remote/src/server/`、`src/bin/`)和 ide-host 的独立可执行文件不链接进本机 Core,没有可注入的 sink,暂时排除在门禁外;远端服务端日志回传和 Monaco 控制台转发在 Issue #1680 中跟踪。

### DAP:记录状态与失败,保留程序内容边界

调试适配器协议(DAP,调试器与宿主交换控制消息的协议)的共享状态由 Core 持有,
因此会话开始、初始化完成、结束和操作失败都由 `debug/engine.rs` 在真实转换点记录。
通道使用校验后的适配器身份;不适合路径的身份使用稳定 SHA-256 摘要,不能让项目路径或显示名称进入通道。
生命周期关键里程碑为 INFO,暂停、运行等内部状态为 DEBUG,超时与适配器拒绝为 ERROR。
取消和过期引用仅为 DEBUG,不把正常取消写成产品故障;已有失败操作记录时,随后会话 Failed 不重复记录。

原生宿主只补自己拥有的启动失败、初始化超时和异常进程退出,以固定阶段与退出码传给生成事件。
正确做法是 Core 根据 `OperationFailed.code` 记录稳定分类;不要把 adapter stderr、DAP 响应 message、
调试变量、内存、源码或被调试程序输出塞入 text/error。原始 stderr 继续属于调试控制台,不代表持久化。
共享 Core 队列复用宿主既有 drain 和队列唤醒回调,不创建第二个 writer、轮询或应用退出后仍持有的缓存。

## 考虑过的备选方案

- JSON Lines:通用解析方便,但每行重复键名和身份信息、手工阅读噪声较多。采用有严格语法与 fixture 的单行文本,代价是维护专门编解码器。
Expand All @@ -147,6 +170,8 @@ agent-host、git-host、ide-host 和 lithe-remote 客户端被链接进 Core,
- 现在删除 v1:会破坏仍直接使用旧 Rust API 的 Windows。保留兼容入口,迁移完成后再单独评估删除。
- 辅助 crate 依赖 Core 直接记录:形成循环依赖。把事件队列拆成第三个公共 crate 可以解开循环,但目录校验和脱敏都在 Core,拆出去要么复制规则,要么只搬一个空壳队列,不如注入一个函数指针简单。
- 辅助 crate 未注入时回退到 stderr:askpass 等入口的输出是协议的一部分,回退会污染协议,也让门禁不得不为每个 crate 开 allowlist。
- 宿主定时轮询 drainCoreEvents:空闲时也持续唤醒,落盘延迟取决于周期,违背不增加空闲轮询的约束。改为 Core 有事实时合并唤醒。
- 每条事实回调一次,或在回调里直接 drain:突发事实会产生大量跨语言调用;在记录线程里 drain 会把磁盘 I/O 带到 Core 调用方线程,还要重入 Core。回调只安排宿主已有的单次 drain。

## 后果

Expand All @@ -162,6 +187,7 @@ agent-host、git-host、ide-host 和 lithe-remote 客户端被链接进 Core,
- `node scripts/logging/verify-log-usage.mjs`
- `node --test --test-timeout=60000 scripts/logging/test-log-events.mjs`
- `cargo test --manifest-path rust/Cargo.toml -p lithe-core diagnostics`
- `cargo test --manifest-path rust/Cargo.toml -p lithe-core --test core_log_waker`
- `./scripts/rust-core/verify-rust-core-comments.sh`
- `./scripts/architecture/verify-shared-contracts.sh`
- `./scripts/windows/verify/verify-windows-boundaries.sh`
Expand All @@ -173,6 +199,7 @@ fixture 覆盖往返、转义、非法输入、路径段、轮转与保留、共
## 适用范围

- `rust/lithe-core/src/diagnostics/`
- `rust/lithe-core/src/debug/engine.rs`
- `rust/lithe-agent-host/src/failure_log.rs`
- `rust/lithe-git-host/src/failure_log.rs`
- `rust/lithe-ide-host/src/failure_log.rs`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ JDT LS 这类语言服务器有很多启动参数,最常见的是堆内存上

### 4. JDT 适配层让出两个参数

直接启动 JDT 时,宿主以前会丢弃插件传来的 `-Xmx` 和 `-Dlog.level=`,再塞自己的默认值。现在这两个参数改为“插件的最后一个合法值优先,没有就用宿主默认值(`-Xmx2048m`、`-Dlog.level=ALL`,即以前的行为)”。合法性很窄:堆大小必须是数字加可选的 `k`/`m`/`g`,并且必须大于 1536 MiB(JDT LS 在不超过 1.5 GB 时会改走“分批导入”,模块坐标重复会让整个 Java 服务失败,见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md`);日志级别必须是字母数字;不合法的值被丢弃而不是传给 JVM。Java Support 的清单因此把 `heapMaxMB` 的下限和默认值都设为 2048。`-Xms`、`--add-*`、Eclipse 产品属性、元数据位置和 Lombok 仍归宿主所有。宿主另外追加一组 GC 与日志默认值,插件参数里同键的值优先(见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md` 决策第 5 条)。
直接启动 JDT 时,宿主以前会丢弃插件传来的 `-Xmx` 和 `-Dlog.level=`,再塞自己的默认值。现在这两个参数改为“插件的最后一个合法值优先,没有就用宿主默认值(`-Xmx2048m`、`-Dlog.level=WARNING`;日志默认收紧到警告和错误)”。合法性很窄:堆大小必须是数字加可选的 `k`/`m`/`g`,并且必须大于 1536 MiB(JDT LS 在不超过 1.5 GB 时会改走“分批导入”,模块坐标重复会让整个 Java 服务失败,见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md`);日志级别必须是字母数字;不合法的值被丢弃而不是传给 JVM。Java Support 的清单因此把 `heapMaxMB` 的下限和默认值都设为 2048。`-Xms`、`--add-*`、Eclipse 产品属性、元数据位置和 Lombok 仍归宿主所有。宿主另外追加一组 GC 与日志默认值,插件参数里同键的值优先(见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md` 决策第 5 条)。

这个读取是幂等的:宿主把自己产出的参数再喂回适配层,得到相同的结果,已有的稳定性测试继续成立。

Expand Down
9 changes: 7 additions & 2 deletions Plugins/mac/Official/JavaSupport/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"type": "enum",
"title": "Language server log level",
"description": "How much the Java language server writes to its own log. Quieter levels reduce disk writes.",
"default": "ALL",
"default": "WARNING",
"options": [
{ "value": "ALL", "title": "All messages" },
{ "value": "INFO", "title": "Information" },
Expand All @@ -60,7 +60,12 @@
"packagedRuntime": "runtime/${arch}/bin/java",
"runtimeArguments": [
"-Xmx${setting:heapMaxMB}m",
"-Dlog.level=${setting:logLevel}"
"-Dlog.level=${setting:logLevel}",
"-Dosgi.logfile=${logDir}/jdt.log",
"-Declipse.log.include.commandline=false",
"-Declipse.log.size.max=1024",
"-Declipse.log.backup.max=2",
"-Declipse.log.level=WARNING"
],
"entry": "language-server/plugins/org.eclipse.equinox.launcher_1.8.0.v20260804-1928.jar"
},
Expand Down
16 changes: 16 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustCoreBridge+Logging.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import LitheModuleAPI
import LitheRustCore

extension RustCoreBridge {
typealias LogScalar = CoreLogScalar
Expand Down Expand Up @@ -165,6 +166,21 @@ extension RustCoreBridge {
try executeResult(command: "diagnostics.drainCoreEvents", payload: [String: String]()).get()
}

/// Core calls `callback` at most once per drain cycle, from any thread, while
/// it owns the installation. The callback must only schedule host work.
typealias CoreLogWaker = @convention(c) (UnsafeMutableRawPointer?) -> Void
/// Replaces the process writer's pending-fact callback. Facts already
/// waiting signal synchronously, so callers must not hold a lock the
/// callback takes. `context` must stay valid until removal returns.
static func installCoreLogWaker(_ callback: CoreLogWaker, context: UnsafeMutableRawPointer) {
lithe_bridge_install_log_waker(callback, context)
}
/// After return Core never invokes the callback with `context` again.
@discardableResult
static func removeCoreLogWaker(context: UnsafeMutableRawPointer) -> Bool {
lithe_bridge_remove_log_waker(context) == 1
}

func encodeLogs(_ request: LogBatch) throws -> LogBatchResult {
try executeResult(command: "diagnostics.encodeLogBatch", payload: request).get()
}
Expand Down
Loading
Loading