Skip to content

feat(core): 拆出 #1478 的共享 Core 改动(LSP 失败启动清理、Java 直接启动、归档解压加固、签名向量) - #1724

Merged
xiaoyumuxi merged 5 commits into
previewfrom
split/1478-shared-core
Nov 4, 2026
Merged

xiaoyumuxi merged 5 commits into
previewfrom
split/1478-shared-core

Conversation

@xiaoyumuxi

@xiaoyumuxi xiaoyumuxi commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

背景

#1478(codex/windows-plugin-parity-unified,作者 Mucheen)有 540 个文件,和 preview 冲突,无法直接合入。这个 PR 只拆出其中 Rust Core、共享契约和 macOS 测试 的部分,基于最新 preview 重新整理,review 通过后先合入。Windows 宿主的改动留在 #1478,等这个 PR 合入后再 rebase 适配。

Commit 保留了原作者的 Co-authored-by。

改动

1. LSP 启动失败后的进程清理(Rust Core)

  • lsp/interface/engine.rs
    • start_server 启动失败时,如果会话已经注册,就把它的身份记进内部的 failed_starts 积压,并立即尝试清理一次。返回给调用方的仍是原始错误。
    • 每次新的启动前先调用 cleanup_failed_start()。积压里的会话最多再尝试 dispose 一次,确认退出后才移出积压。仍未退出时,新的启动会以 ProcessFailed 被拒绝,不会让多个语言服务器进程同时残留。
    • start_server_attempt 先调用 validate_start_request。非法的初始配置在启动进程之前就被拒绝。启动进程挪到 initialize 请求分配之后。
    • 启动期间 indexing 被撤销(两个路径)时:fail("indexingChanged")、abandon、kill_process;第一个路径再 dispose(2s),确认退出才移除会话,否则保留注册。
    • destroy 先 process.dispose(2s) 确认进程已经退出,再移除会话。
    • 新增模块函数 start_server_owned,在 spawn_readers 之前通过回调把 session id 交给宿主。
  • lib.rs:新增 start_lsp_owned(payload, registered),供需要在注册时拿到 session id 的宿主使用(Windows 会用到)。macOS 仍调用原来的入口。
  • lsp/interface/process.rs
    • LanguageServerProcess trait 增加默认的 dispose:只有 exit_status() 已有结果时才返回 Ok。
    • SystemProcess::dispose:Windows 等进程树在时限内终止;非 Windows 每 10ms 重复一次幂等的 terminate_native()(child.kill())并读取退出状态,到截止时间仍在运行就返回 “Language-server root did not exit.”,最后一次 kill 失败的原因作为 details 附在这个错误上。
  • lsp/interface/remote_process.rs:RemoteProcess 覆盖 dispose,发送强制的 PROCESS_KILL(2 秒上限)并记录退出。远程进程树由后端持有,这个强制 kill 回执就是本地能拿到的最终结果。
  • lsp/interface/scripted.rs:测试用的脚本进程支持 dispose 和 block_cleanup,用来模拟清理失败。

拆分时在原 PR 基础上补的修正:

  1. 原 PR 的非 Windows dispose 忽略时限,只读一次退出状态。kill 发出后子进程要异步回收,紧接着读状态仍可能是“运行中”,destroy 会误报失败;没被 kill 过的进程则永远无法 dispose。改成在时限内重复 kill 并轮询。
  2. 原 PR 没有给 RemoteProcess 实现 dispose,落到 trait 默认实现后,Remote SSH 的 LSP 会话在 destroy 时会因为“尚未退出”而失败;补了覆盖实现。
  3. 接入 preview 新加的结构化日志门禁(Verify Structured Logging,规则 rust.let-underscore):
    • process.rs 的轮询不再用 let _ = 丢弃 kill 错误,而是保留最后一次失败并随最终错误返回。
    • engine.rs 启动失败后的收尾清理按日志规范第二节第 4 类(同一次失败已经记录过)加 log-exempt 标记:原始启动错误由本次请求的 Core 失败轨迹记录;清理残留仍由 failed_starts 持有,下一次启动先回收,回收不了就返回 ProcessFailed,由那次请求的轨迹记录。
  4. 原 PR 新增的真实 JDT 测试 real_jdtls_lists_entrypoints_in_a_plain_java_folder 没有配 Windows 用时上限。Windows CI 的 real-JDT 通道默认每项 15 秒,这个用例和 Java 25 用例一样要冷启动真实 JDT LS,run 41817286665 在 15.144 秒被看门狗终止,并非断言失败;本地 macOS arm64 同一提交 6.0 秒通过。ci-windows.yml 给它单独加 --test-budget …=240000,覆盖用例自身的 90 秒就绪 + 120 秒入口导入期限,其他用例和整套期限不变;依据写进 write-stable-tests/references/windows-and-rust.md。
  5. Review P2:build/detect.rs 的根目录检查改用 workspace_io::metadata,远程工作区根(包括经宿主符号链接打开的根)在宿主上判断,不再读本机未实体化的投影而误报空检测。新增 remote_build_detects_a_root_opened_through_a_host_symbolic_link,改回 std::fs::metadata 时该测试失败。
  6. Review P2:engine.rs 两条索引策略取消路径改为先 abandon 启动 attempt 再 session.fail,取消不再被记成插件 Initialize 握手失败。两个既有取消测试附加 startupOperation 并断言没有插件操作记录,旧顺序下两项都失败。

Review P1(java.main 直接启动仍读 extensions.maven.mainClass)属于 Java 功能,Java 支持后续整体迁出到独立插件,这个 PR 不处理。

对 macOS 的影响: macOS 在收到终止事件后调用一次 lsp.destroyServer 并忽略错误(LanguageServerSession.swift、RustCoreBridge.swift),调用方式不变;只是 destroy 现在最多等 2 秒确认进程退出。

2. Java 入口发现与直接启动

  • lsp/languages/java_entrypoints.rs:java_entrypoints_command() 不再传作用域 URI,由 JDT 返回全部入口,Core 只保留打开的工作区拥有的源文件。原来的 java_entrypoint_scope_uri 删除。
  • lsp/interface/engine.rs:preview 上 Maven profile 等待器的调用点也改成 java_entrypoints_command()(这个调用点是原 PR 之后 preview 新增的,拆分时补上)。
  • execution/configuration.rs:java.main 带 javaLaunch 时一律走 JDT 直接启动,不再重新编译单个文件;Maven 只在没有 buildPrepared 回执时加资源步骤;Gradle 和非托管项目直接用 JDT 给出的路径启动;没有 javaLaunch 时保留原来的先编译再运行。
    • macOS 只在 usesJavaProjectPreparation 为 true 时发送 javaLaunch,并且总是带 buildPrepared: true,所以 Maven 行为不变;Gradle 的 java.main 现在改用 JDT 路径,这是修正。

3. 构建系统检测

  • build/detect.rs:新增 scan_directory_root。工作区根目录不是目录或不存在时返回空结果;根目录是真实目录、但读取出现 I/O 错误时返回错误,不再当成“什么都没检测到”。

4. 项目生成器归档解压加固

  • project_generators/archive.rs
    • 解压前先用 validate_entries 预检全部条目,新增上限:目录深度 64 层(MAX_DEPTH)、路径总字节 16 MiB(MAX_PATH_BYTES)。
    • 目标路径上的目录必须是真实目录(real_directory):不跟随符号链接,Windows 上也拒绝 reparse point。
    • 文件内容按 32 KiB 分块复制,块之间检查取消。
    • 错误分为 ExtractionError::Invalid(违反解压约定)和 ExtractionError::Interrupted(取消或超时)。
    • Windows 专用的 safe_component 拒绝保留设备名(CON、NUL、COM1 等)、非法字符和以点或空格结尾的路径组件;非 Windows 不做这项检查。
  • project_generators/commands.rs:Interrupted 作为 Core 错误原样返回。
  • macOS 的 project.extractArchive 调用(RustProjectGeneratorOperations.swift)没有取消作用域,行为不变,只是多了预检。

5. 插件签名的跨平台测试向量

  • shared/contracts/plugins/v1/fixtures/signature-vectors.json(新增):固定的 Ed25519 种子、RFC 8032 锚点、规范载荷、文件摘要和内容摘要,包含非 ASCII 文件名。
  • shared/contracts/plugins/v1/README.md:说明这组向量的用途。
  • macos/Tests/LitheTests/PluginSignatureVectorTests.swift(新增):macOS 签名实现用同一组向量验证 RFC 8032 锚点、规范载荷、内容摘要、写读往返后的验签,以及篡改文件后拒绝。

拆分时在原 PR 基础上补的修正: 原测试要求 CryptoKit 重新签出和 fixture 逐字节相同的签名,这在 macOS 上不可能通过:CryptoKit 的 Ed25519 签名带随机数,同一私钥对同一消息签两次结果也不同,只有 RFC 8032 的确定性实现(Windows 用的那类)才能复现。原 PR 是 Draft,没跑 macOS CI,所以没暴露出来(本地 4 项里 2 项失败)。现在改成:

  • primitiveMatchesTheRFC8032Anchor:公钥逐字节一致;RFC 8032 记录的签名能用派生公钥验过;CryptoKit 新签的签名也能验过。
  • recordedSignatureCoversTheSwiftCanonicalPayload(原 deterministicSignatureReproducesTheSharedVector):fixture 的 payloadBase64 和记录签名自洽;Swift 计算的 files 与 fixture 一致;用 Swift 自己构造的规范载荷去验 fixture 记录的签名,Ed25519 只接受原消息,因此能证明两端规范载荷逐字节一致。
  • 契约 README 同步写明:两端必须构造相同的规范载荷和摘要、接受记录的签名;带随机数的签名器用验签而不是比较字节。

6. 文档与 Agent Note

  • shared/contracts/rust-core-api.md:失败启动清理语义;javaEntrypoints 发现范围。
  • .agents/notes/.../2026-09-21-java-entrypoints-owned-by-jdt.md:原 PR 的更新。
  • 拆分时补的 Note 段落:
    • 2026-09-13-language-tooling-and-lsp-runtime-ownership.md:失败启动的积压、最多重试一次、start_lsp_owned 的注册时机、非 Windows 轮询与 Windows 进程树等待的区别,并引用对应测试。
    • 2026-09-18-java-project-build-and-launch-boundary.md:java.main + javaLaunch 的直接启动规则。
    • 2026-10-17-build-system-plugin-contract.md:build.detect 的空结果边界。

7. 平台功能矩阵

verify-platform-feature-matrix-change.sh 要求平台实现改动同时更新能力记录。#1478 原来的 16 个矩阵 JSON 主要是 Windows 证据,没有带过来;这里只补本 PR 涉及的 macOS 证据和说明,verificationStatus 都保持 pending(只有 Core/单测证据,没有复验 macOS 真实 UI):

  • lsp-lifecycle:失败启动积压与 dispose 语义,证据加 process.rs、remote_process.rs、scripted.rs,验证动作加 Core 测试命令。
  • run-discovery:javaEntrypoints 发现范围与 java.main 直接启动,证据加 java_entrypoints.rs。
  • build-system-plugins:build.detect 空结果边界,证据加 tests/build_systems.rs。
  • plugin-project-generators:归档解压的预检与上限,证据加 archive.rs。
  • declarative-plugin-contract:签名向量,证据加 fixture 和 Swift 测试。

新增测试

  • native_owner_keeps_failed_initialize_identity_until_verified_disposal
  • wire_owner_retries_failed_initialize_disposal_before_another_start
  • wire_owner_cleans_at_most_one_failed_start_before_rejecting_a_backlog
  • invalid_initial_configuration_is_rejected_before_launch
  • native_owner_retains_indexing_cancellation_after_registration
  • the_command_includes_jdt_invisible_projects_before_workspace_filtering
  • unscoped_results_keep_only_sources_owned_by_the_open_workspace
  • real_jdtls_lists_entrypoints_in_a_plain_java_folder(真实 JDT)
  • existing_directory_scan_errors_are_not_empty_detections
  • detection_returns_empty_for_non_directory_and_missing_workspace_roots
  • java_launch_without_maven_consumes_jdt_paths_without_recompiling_one_file
  • archive 解压:cancellation_between_chunks_stops_before_the_next_write_or_entry、timeout_between_chunks_preserves_its_first_error_and_closes_the_output、rejects_excessive_archive_path_depth_before_creating_any_parent、expanded_byte_budget_counts_actual_written_bytes、implicit_parents_and_path_bytes_share_the_preflight_budget、archive_paths_cannot_hide_traversal_in_normalized_components、command_reports_stable_cancellation_and_timeout_before_any_write、existing_hard_link_target_is_never_truncated_or_replaced、refuses_existing_symlink_directories_without_touching_their_target、windows_archive_paths_reject_ads_device_names_and_verbatim_normalization(仅 Windows)
  • PluginSignatureVectorTests(Swift,4 项)

不包含(留在 #1478,后续适配 Windows)

windows/、Plugins/win、scripts/windows、rust/lithe-git-host、Windows Core 测试、shared/i18n 词条、Windows fixtures、scripts/{plugins,resources,ci,i18n,architecture} 的改动、write-stable-tests Skill、Windows 相关 Note、workflows、official-plugins.json、bun/package 文件,以及 16 个功能矩阵 JSON。这些都依赖 Windows 宿主,单独合入没有意义。

本地验证(macOS arm64)

  • cargo test -p lithe-core:lib 1812 项全部通过,其他测试二进制 1 + 2 + 25 + 5 项通过。
  • 新增和改动相关的定向测试 587 项通过。
  • 真实 JDT LS 1.61.0(macOS,JDK 21):real_jdtls_lists_entrypoints_in_a_plain_java_folder 和原有的入口点真实测试通过。
  • cargo fmt --check 无改动;verify-rust-core-comments、verify-rust-core-static 通过。
  • scripts/rust-core/verify-rust-core-swift-bridge.sh:C ABI、链接符号和 Git 执行集成通过。
  • ./scripts/macos/test/test-macos.sh --filter PluginSignatureVectorTests:4 项通过(修正前 2 项失败,见第 5 节)。
  • node --test scripts/logging/test-log-events.mjs(15 项)和 verify-log-usage.mjs(0 处违规)通过。
  • 完整 macOS Swift 测试:本地和 cargo 编译并发跑,机器负载很高,套件在一个等待 stdin 的子进程上卡住 10 分钟后被我停掉,以 CI 的 Swift tests 为准。停掉前看到的失败都不涉及本 PR 改动的文件:
  • Windows 编译检查(windows/tauri/src-tauri 下 cargo check --bin lithe-windows):只有 preview 上原有的 2 个错误(project_window_registry.rs:23、:29),本 PR 没有引入新错误。
  • verify-shared-contracts.sh、verify-agent-notes.sh(203 篇)、verify-runtime-bundle-immutability.sh 通过。
  • node scripts/i18n/verify-app-language-localization.mjs、node scripts/i18n/generate-i18n.mjs --check 通过(本 PR 没有界面文案)。
  • verify-platform-feature-matrix-change.sh origin/preview HEAD、verify-platform-feature-matrix.sh 通过。
  • 新增预算后:write-stable-tests 的 test-verify-test-stability.mjs、plan-ci-lane-reuse.test.mjs(20 项)、test-verify-download-cache.mjs 通过;CI 报告确认测试名前缀和既有 Java 25 预算一致,能命中。
  • 测试结束后没有残留的 Lithe 或 JDT LS 进程。

另外:preview 上 windows/tauri/src-tauri/Cargo.lock 缺少 lithe-core 已经依赖的 unicode-normalization(Windows 编译检查时 cargo 会自动补一行)。这和本 PR 无关,没有带进来。

合入后

这个 PR 修改了 shared/contracts/plugins/v1/(README 和新增的 signature-vectors.json),合入后需要在 lithe-plugin-docs 运行 node scripts/contract.mjs sync --revision <合并后的 preview commit> 并提交。

从 #1478 拆出 Rust Core、共享契约和 macOS 测试部分,基于最新 preview
重新整理;Windows 宿主改动留在 #1478 后续适配。

- LSP:启动失败的会话进入积压,下次启动前最多再清理一次,清理不了就拒绝新启动;
  非法初始配置在启动进程前拒绝;destroy 先确认进程退出再移除会话;
  新增 start_lsp_owned 在注册时交出 session id
- 进程 dispose:非 Windows 在时限内重复 kill 并轮询退出状态;
  RemoteProcess 以强制 PROCESS_KILL 回执完成 dispose
- Java:javaEntrypoints 不再传作用域,结果只保留工作区拥有的源文件;
  java.main 带 javaLaunch 时直接用 JDT 路径启动,Maven 只在没有
  buildPrepared 回执时补资源步骤
- build.detect:根目录不存在或不是目录时返回空,真实目录的 I/O 错误不再吞掉
- 项目归档解压:预检深度与路径字节上限、只走真实目录、分块复制并检查取消、
  区分 Invalid/Interrupted,Windows 拒绝保留设备名
- 插件签名跨平台向量与 macOS 测试;CryptoKit 签名带随机数,
  macOS 改为用验签证明规范载荷一致
- 同步 rust-core-api 契约与相关 Agent Note
- 更新 lsp-lifecycle、run-discovery、build-system-plugins、
  plugin-project-generators、declarative-plugin-contract 五项功能矩阵记录

Co-authored-by: Mucheen <1528136628@qq.com>
@ghfind-review ghfind-review Bot added the review: high ghfind author score; see https://ghfind.com label Nov 4, 2026
@xiaoyumuxi
xiaoyumuxi marked this pull request as ready for review November 4, 2026 10:42
@lithe-code-review

lithe-code-review Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Lithe Review

结论: ⛔ 有阻塞
范围: preview ← split/1478-shared-core · head: 34f2ba5

Findings

  1. [P1] 直接启动分支仍要求已迁出的 Maven 主类字段
    rust/lithe-core/src/execution/configuration.rs:2520

    • 实际影响:有效的 Provider Java 配置无法生成启动计划,返回 Java project launch is missing its main class。
    • 触发条件:java.main 携带有效 javaLaunch,配置按当前生成器格式将主类保存在 extensions.java.mainClass,没有 extensions.maven.mainClass。
    • 复现步骤:将新增的非 Maven 启动测试中 mainClass 移到 extensions.java,保留源码和 JDT 路径,再调用 runConfig.createLaunchPlan。新条件使其进入直接启动分支,但第 2606 行仍强制读取 Maven 字段。现有 projectOwnership 生成器和归属测试明确生成这种无 Maven 扩展的配置;macOS 对其发送 javaLaunch。
    • 来源:本次引入。base 的非 Maven 配置走后面的源码编译分支,不受此字段检查影响。
  2. [P2] 通过远程文件适配器检查构建根目录
    rust/lithe-core/src/build/detect.rs:149

    • 实际影响:远程存在的工程被成功返回为空检测结果,构建系统消失。
    • 触发条件:远程工作区根是指向绑定路径之外的符号链接。
    • 复现步骤:让远端 link 指向含构建文件的 real,以 link 建立远程绑定并调用 build.detect。Workspace::new 经远程适配器解析真实路径,将其投影到本机未实体化的 .lithe-remote-host 命名空间;新增的 std::fs::metadata 在本机得到 NotFound,提前返回空列表。原扫描器通过 workspace_io 能读取该远程目录。
    • 来源:本次引入。新增本机检查绕过了原有远程 I/O 路径。
  3. [P2] 在标记会话失败前撤销已取消的插件启动记录
    rust/lithe-core/src/lsp/interface/engine.rs:2335

    • 实际影响:正常的索引策略取消被记录成插件握手失败,产生错误的 plugins.operation.failed / host.initializeFailed 诊断。
    • 触发条件:声明式 LSP 携带 startupOperation,在 initialize 完成前因索引策略变化取消。
    • 复现步骤:给现有 RetireOnLaunch 取消场景附加有效启动 handoff,检查 plugin_operations::test_capture。新增的 session.fail() 会先通过第 6442 行消费 attempt 并记录 Initialize 失败,后面的 abandon() 已无法撤回。第 2377 行的另一取消路径相同。
    • 来源:本次引入。base 在终止进程前直接 abandon,不记录这次取消为失败。
  4. [P2] 处理新增的清理错误丢弃,恢复日志门禁
    rust/lithe-core/src/lsp/interface/engine.rs:1893

    • 实际影响:新增代码触发现有日志用法门禁;清理失败的信息也会被丢弃。
    • 触发条件:静态检查扫描新增的 let _ = self.cleanup_failed_start(),以及 rust/lithe-core/src/lsp/interface/process.rs:317 的 let _ = self.terminate_native()。
    • 复现步骤:现有 scripts/logging/verify-log-usage.mjs:110 的 rust.let-underscore 规则直接匹配这两处生产代码,均无相邻豁免。.github/workflows/verify-logging.yml 会执行该检查。本结论来自静态规则核对,未运行脚本,也未取得已失败 Check Run 的日志。
    • 来源:本次引入。

Scope Check

  • 关联 Issue:未找到可验证的关联 Issue,按 PR 描述核对实现范围。
  • 已审查完整的 26 文件清单,以及直接相关的生成器、macOS/Windows 启动调用方、远程文件适配器和日志门禁。
  • Java 语义继续复用 JDT,归档处理继续复用现有 ZIP 库,未发现新增并行领域模型。
  • Finding 1 为合并前必须修复的问题;Finding 2–4 为非阻塞建议,均由本次改动引入且属于本次职责范围。

Suggestions

  1. 移除直接启动对旧 Maven 主类字段的强制依赖,使用已验证的 JDT 目标或统一主类访问规则;增加仅含 extensions.java.mainClass 的生成配置回归。
  2. 改用 crate::remote::workspace_io::metadata,增加远程符号链接根的检测回归。
  3. 两条索引取消路径都先 abandon,再调用失败状态处理;测试同时断言没有插件操作失败记录。
  4. 在处理边界记录清理失败的目录事件并保留原启动错误;仅对符合规则的已记录失败或预期缺失添加具体豁免,不放宽门禁。

Summary

共享 Core 的实现方向和上游复用边界一致,但新直接启动条件没有覆盖当前生产配置格式,造成运行回归。新增测试使用旧 Maven 字段,未覆盖这一关键兼容面。

Verification

  • 本次仅进行了指定 base…head 的静态只读审查;未修改文件、运行仓库代码或测试、访问网络。
  • 作者报告 Core、定向测试、真实 JDT、4 项 Swift 签名测试,以及契约、i18n、bundle 不变性等检查通过;这些结果未独立复验。
  • 提供的 head Check Run 中 Structured Logging 为 failure,多项 Rust、Swift、Windows 和打包检查仍在运行,macOS gate 已取消,不能认定完整 CI 通过。
  • 尚缺完整平台工作流结果、测试稳定性逐项计时报告,以及此次 LSP 启动/清理改动的前后性能数据。插件契约副本还需按 PR 说明在合入后同步至 lithe-plugin-docs。

- process.rs:非 Windows 的 dispose 轮询不再丢弃 terminate_native 的错误,
  进程超过期限仍未退出时,把最后一次 kill 失败作为 details 附在
  "Language-server root did not exit." 上返回。
- engine.rs:启动失败后的收尾清理属于“同一次失败已经记录过”的例外,
  补上 log-exempt 标记写明原因;残留仍由 failed_starts 持有,下一次
  启动会先回收并在失败时返回 ProcessFailed。
- ci-windows.yml:real-JDT 通道给 real_jdtls_lists_entrypoints_in_a_plain_java_folder
  加 240 秒的 --test-budget。它和 Java 25 用例一样要冷启动真实 JDT LS,Windows run
  41817286665 在默认 15 秒看门狗处被终止(无断言失败),同一提交在 macOS arm64 上 6.0 秒通过。
  240 秒覆盖用例自身的 90 秒就绪 + 120 秒入口导入期限,其他用例和整套期限不变。
- write-stable-tests 的 windows-and-rust 参考页记录这项放宽的依据。
- build/detect.rs:scan_directory_root 改用 workspace_io::metadata,远程工作区根
  (包括经宿主符号链接打开的根)在宿主上判断,不再读本机未实体化的投影而返回空检测。
  新增 remote_build_detects_a_root_opened_through_a_host_symbolic_link 回归测试。
- lsp/interface/engine.rs:两条索引策略取消路径先 abandon 启动 attempt 再 session.fail,
  取消不再被 fail 记成 Initialize 握手失败。两个既有取消测试附加 startupOperation
  并断言没有插件操作记录。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review: high ghfind author score; see https://ghfind.com

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant