Skip to content

fix(desktop): 确认安装即启用插件,卸载先停进程再清理插件状态目录 - #1846

Merged
1lck merged 1 commit into
previewfrom
fix/plugin-install-enable-and-uninstall-state
Nov 13, 2026
Merged

1lck merged 1 commit into
previewfrom
fix/plugin-install-enable-and-uninstall-state

Conversation

@1lck

@1lck 1lck commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

关联 Issue / Linked issue

PR 类型 / Type

  • feat:新功能或用户可见的改进
  • fix:缺陷修复
  • docs:文档、Agent Note、Skill 等规则文件
  • release:稳定版发布准备

改动说明 / What changes

背景 / Why

真实走一遍“Lithe Store 安装 Java Navigation → F12 → 卸载”时发现两个问题:

  1. 在审查框确认安装后,插件还是禁用的,打开 Java 文件状态栏一直“LSP 未启动”,必须再去“已安装”里手动启用([Bug] Viva 从插件市场安装插件并确认后,插件仍是禁用状态,需要手动启用 #1844)。旧 macOS 客户端早就是“确认安装即启用”,Viva 重写时没有带上。
  2. 卸载后插件包和进程没了,但插件写入的 <插件根>/.state/<插件 ID>(例如解压出来的 JDK 源码)还留在磁盘上([Bug] Viva 卸载插件后残留插件状态目录 .state/<插件 ID>(如 JDK 源码副本) #1845)。排查时还发现:卸载是先删包、后停语言服务器;插件后端在禁用和卸载时都不会停止。

改动 / Changes

启用(pluginstate、pluginmanager、workbench)

  • pluginstate.Store.EnableUnlessChosen:在同一把锁里,没有记录才记为启用;用户已有选择(启用或禁用)都保留。包注释改为说明这条规则。
  • pluginmanager.Manager.EnableInstalled:确认插件已安装(否则返回 ErrUnknownPlugin),再调用上面的方法。
  • workbench.confirmPluginInstall:提交成功后(包括只有批准记录写入失败的 ApprovalError)在同一个后台任务里调用 EnableInstalled,失败记 plugin_enable 日志;修正原来“新安装默认启用”的误导注释。
  • 结果:首次安装、卸载后重装都是启用;先禁用再装新版本,保持禁用。

卸载清理状态(pluginpkg、pluginmanager、pluginservice)

  • pluginpkg.StateDirectory:.state 的唯一定义;pluginservice 的 pluginStateDirectory 改为引用它。
  • pluginpkg.RemoveState(root, id):校验根目录和插件 ID,把 .state/<id> 改名进 .trash 再删除;只处理目录、不跟随符号链接,不存在不算错误。.state/host-secret 和其他插件的目录不动。插件设置在偏好目录,不受影响。
  • Manager.Uninstall 在删除版本、批准记录、启用选择之后调用 RemoveState;更新相关注释。

停止顺序(pluginservice、workbench)

  • 新增 pluginservice/lifecycle.go:
    • Service.Uninstall(ctx, id, server):先摘除并停止插件后端、停止语言服务器(等进程退出,有界),再 Manager.Uninstall。
    • Service.Disable(ctx, id, server):先记录禁用,再停止语言服务器和后端,停止期间不会被重新拉起。
  • workbench.stopPluginServer 拆出 detachPluginServer(移出登记表、设墓碑、取消、撤诊断、释放文档,返回会话),stopPluginServer 行为不变。
  • workbench.uninstallPlugin:窗口线程先 detachPluginServer,后台调用 Service.Uninstall(原来是先删包,删完才停服务器,且不停后端)。
  • workbench.setPluginEnabled(false):窗口线程先 detachPluginServer,后台调用 Service.Disable(原来不停后端)。启用路径不变。

测试

  • pluginstate:TestEnableUnlessChosenRecordsOnlyAFirstChoice。
  • pluginmanager:TestUninstallRemovesThePluginState、TestEnableInstalledKeepsAnExplicitChoice。
  • pluginservice:TestUninstallStopsTheBackendBeforeRemovingThePlugin、TestDisableStopsTheBackend。
  • workbench:TestConfirmedInstallIsEnabledAndUninstallRemovesItsState(从磁盘装一个没有程序的插件,确认后已启用,卸载后包和状态目录都不在)。
  • 修正已有测试 TestContentProviderRegistrationRules 未加锁写 BackendHost.sessions 的数据竞争(-race 下失败;打开工作区会在后台 goroutine 里遍历 sessions)。

文档与矩阵

  • 新增 Agent Note .agents/notes/implemented/bug-fix/2026-11-13-viva-plugin-install-enables-and-uninstall-cleans-state.md。
  • 修正 2026-11-10-viva-plugin-host-services-and-backend-sessions.md(原文说停用、卸载会停后端,此前并不成立;补充卸载删除 .state/<id>),并在 2026-10-25-java-plugin-distribution-foundation.md 的“下载清单默认禁用”处加指向新 Note 的说明。
  • 功能矩阵 plugin-extension-management.json:补证据路径、Viva 验证动作和本次 macOS 验收说明,状态不变(partial / pending)。

不在本 PR 范围内 / Out of scope


类型专属:fix

复现与根因

  • 复现步骤、实际结果、期望结果:
    1. 全新用户目录,设置 → 插件 → 插件市场 → Java Navigation → 安装 → 审查框确认。实际:插件为禁用,打开 App.java 状态栏“LSP 未启动”,F12 无反应;期望:直接启用,可以跳转。
    2. 启用后 F12 跳进 JDK String.trim,再卸载。实际:<插件根>/.state/io.github.lithe-idea.java-navigation/0.0.1/jdk-sources/.../String.java 仍在;期望:.state/<插件 ID> 随卸载删除。
  • 根因(引入该行为的提交或 PR):都来自 Viva 迁移的 0faecba(Epic(0.7.0): 基于 Viva 框架的跨平台方案的迁移(#1794) #1799)。pluginstate 规定“没有记录即禁用”,confirmPluginInstall 不写记录;pluginpkg.Uninstall 明确不动状态目录,Manager.Uninstall 也不删;uninstallPlugin 先删包后停服务器;pluginservice.DetachBackend 没有任何调用方。
  • 影响平台:macOS 和 Windows(共用同一份 Go 代码);Windows 上运行中的可执行文件还会让删包失败。

检查项

  • 已先写回归测试,确认它在修复前失败、修复后通过;测试路径与修复前的失败输出:
    • pluginmanager_test.go TestUninstallRemovesThePluginState:the plugin's state survived
    • pluginmanager_test.go TestEnableInstalledKeepsAnExplicitChoice:fresh: enabled = false ... want true
    • pluginstate_test.go TestEnableUnlessChosenRecordsOnlyAFirstChoice:fresh: changed = false
    • workbench/plugins_test.go TestConfirmedInstallIsEnabledAndUninstallRemovesItsState(暂存工作台改动后运行):the confirmed installation is not enabled
    • pluginservice/lifecycle_test.go 两个测试(去掉后端停止后运行):the backend was not stopped before the package was removed、the disabled plugin's backend was not stopped
  • 无法自动化时,已写明手动复现与验证步骤:整条用户流程另做了录屏验收,见“验证”。
  • 已搜索被修改函数、类型、契约字段的全部调用方(含另一平台、插件、SDK、脚本和按字符串分发的调用):Manager.Uninstall 只有工作台调用;stopPluginServer 其余调用方(lsp_status.go)行为不变;DetachBackend 原来没有调用方;pluginStateDirectory 只在 pluginservice 内部使用。旧 Swift/Tauri 代码已移出本仓库。
  • 已检查另一平台是否有同类缺陷(有则本 PR 一并修复,或已提交 Issue):Viva 两个平台共用这份代码,本 PR 一并修复;旧 macOS 客户端本就是“确认安装即启用”。
  • 只改了修复所需的代码,没有夹带重构;修复过程中发现的其他问题已提交 Issue:[Feature] Viva 工作台三处写死插件状态目录名 ".state",应统一用 pluginpkg 的定义 #1841、[Feature] Viva 插件生命周期规则(启用选择、卸载清理与停止顺序)仍在 Go 平台层,应迁入 Rust Core #1842、[Bug] 卸载插件时构建、VCS Provider 和调试适配器进程未在删包前停止 #1843。另外修了阻塞 -race 的已有测试数据竞争(一行加锁)。
  • 修复改变了行为约束或容易复发,已在 .agents/notes/implemented/bug-fix/ 记录

范围检查 / Scope checks

功能矩阵 / Feature matrix

  • 已更新 shared/platform-feature-matrix/features/plugin-extension-management.json:两端的实现状态和验证状态、证据路径、owner、验证动作都如实填写;没有运行验证的保持 pending
  • 已运行 ./scripts/platform-feature-matrix/verify-platform-feature-matrix.sh;改了平台实现路径的,还运行了 ./scripts/platform-feature-matrix/verify-platform-feature-matrix-change.sh origin/preview HEAD
  • 没有提交 .artifacts/ 下生成的矩阵视图
  • [N/A] 没有更新矩阵的,理由:已更新

统一日志系统 / Structured logging

  • 已接入统一日志系统:沿用已有的 operationFailed 事件(plugin_enable、plugin_uninstall 两个已有 operation),没有新增事件、没有直接写 stderr
  • 一个操作只有一条终态,错误只在处理它的边界记录一次:卸载、禁用的停止错误与删除错误合并成一条
  • 没有记录源码、编辑内容、环境变量值、命令输出、凭据
  • [N/A] 已运行 generate-log-events.mjs --check 和 verify-log-usage.mjs:没有新增或修改日志事件声明,也没有新的日志调用方式

界面文案 / i18n

N/A:没有新增或修改任何用户可见文字。仍运行了 verify-app-language-localization.mjs 和 generate-i18n.mjs --check,均通过。

逻辑归属 / Logic placement

  • [N/A] 可共享的逻辑在 Rust Core:Core 目前不持有插件的安装、启用状态,这些规则一直在 Go(pluginpkg、pluginstate、pluginmanager)
  • 修改平台层已有的可共享逻辑时已迁入 Core;本次确有理由不迁的,理由与 refactor Issue:这是缺陷修复,迁移需要新设计跨仓库的 Core 插件生命周期接口(C ABI、fixture、PINNED_CORE_REVISION),已提 [Feature] Viva 插件生命周期规则(启用选择、卸载清理与停止顺序)仍在 Go 平台层,应迁入 Rust Core #1842,本 PR 只在原有 Go 模块内补规则,没有新增平台层模块
  • 工具或运行时专属的知识放在插件或成熟上游,宿主和 Core 没有按工具名加分支

性能与内存 / Performance and memory

  • 没有新增后台功能、轮询、缓存;卸载和禁用的进程停止有界(pluginInstallTimeout、pluginServerStopTimeout、backendStopTimeout),在后台任务里执行
  • 主线程没有文件读写或进程操作:启用、删除状态目录、停止进程都在 a.background 里;窗口线程只做登记表摘除
  • [N/A] 性能基线:不涉及启动、打开项目、输入、滚动、大文件、长输出或搜索

设置 / Settings

N/A:没有新增或修改设置项。

插件契约 / Plugin contract

N/A:没有改契约、能力版本、插件函数协议或宿主方法。

发行包与资源 / Bundle and resources

  • 运行时不向已安装的 app bundle 或 Windows 安装目录写入;只删除插件根(用户 Application Support 目录)下的 .state/<id>,并运行了 scripts/desktop/verify/verify-runtime-bundle-immutability.sh(通过)
  • [N/A] 没有新增或修改下载、解压、生成或缓存资源

无用代码与重构 / Dead code and refactoring


验证 / Verification

命令或操作 平台 结果
scripts/desktop/desktop-go.sh test ./... -count=1(加入后端停止前的版本) macOS arm64 通过,52 个包
scripts/desktop/desktop-go.sh test ./internal/workbench ./internal/pluginservice ./internal/pluginmanager ./internal/pluginstate ./internal/pluginpkg -count=1(变基到 42d6fb9 后) macOS arm64 通过
scripts/desktop/desktop-go.sh test -race ./internal/pluginservice -count=3、./internal/pluginmanager ./internal/pluginstate ./internal/pluginpkg、./internal/workbench -run 'Plugin|TestConfirmedInstall' macOS arm64 通过,无数据竞争
run-go-tests-with-timing.mjs(与 test-stability-macos.sh 相同阈值,7 个新增或改动的测试) macOS arm64 全部通过,单个最长 0.26 s
verify-test-stability.sh --base origin/preview --head HEAD macOS 通过
verify-platform-feature-matrix-change.sh origin/preview HEAD、verify-platform-feature-matrix.sh — 通过,219 项
./scripts/agent-notes/verify-agent-notes.sh — 通过,218 篇
verify-app-language-localization.mjs、generate-i18n.mjs --check — 通过
scripts/desktop/verify/verify-runtime-bundle-immutability.sh — 通过
整机流程验收:-tags vivahead 构建的客户端在无头模式下运行(画面为应用真实渲染),全新临时用户目录,线上 Lithe Store。依次断言:Store 列出插件 → 审查框 → 确认后“已启用” → 状态栏“LSP 就绪” → F12 跳到 Greeter.greet → F12 跳进 JDK String.trim → 卸载 → 插件包、进程、.state/<插件 ID> 都不在 → 再按 F12 不跳转、不重新拉起进程 → 退出后无残留进程 macOS arm64 14 项断言全部通过,有 73 秒录屏

未能执行的检查和原因:

  • Windows:在 lithe-dev(Windows Server 2022)上对本 PR 头 81175bb 运行了 scripts/desktop/desktop-go.sh vet 和 test -count=1:pluginstate、pluginmanager、pluginpkg、pluginservice、pluginhost 全部通过(preview 42d6fb9 基线同样通过)。workbench 包在这台机器上超过 Go 默认的 10 分钟上限(基线也超时,超时时正在运行的测试只跑了 7 秒,属于机器偏慢,不是卡死),没有得到结论,按维护者决定未再延长超时复测。Windows 特有的风险是删包时文件被运行中的进程占用,本 PR 已改成先停进程再删包,这一点没有实测。
  • 全量 ./... 测试是在加入后端停止那部分改动之前跑的;之后只改了 pluginservice 和 workbench,这两个包及其他受影响包在变基后已重新运行并通过。

提交前确认 / Final checks

  • 分支从最新的 preview 创建(已变基到 42d6fb9);目标分支 preview
  • 以 Draft 提交,本地验证通过后才标记 ready
  • 没有无关改动,没有提交 .build/、target/、dist/、.artifacts/ 等产物,没有凭据、个人路径和未脱敏的日志或截图
  • 本次构建、测试或预览启动的 Lithe 应用和子进程已全部关闭,没有进程残留

🤖 Generated with Claude Code

- 商店、社区源或磁盘安装在审查框确认后直接启用插件;用户此前明确禁用过的选择保留,卸载清除选择后重装又会启用(pluginstate.EnableUnlessChosen、pluginmanager.EnableInstalled)。
- 卸载删除插件的整个状态目录 .state/<插件 ID>(含插件解压的 JDK 源码副本等),先改名进 .trash 再删除;其他插件和 host-secret 不动(pluginpkg.RemoveState、StateDirectory)。
- 卸载先停止语言服务器和插件后端,再删包和状态;禁用先记录禁用再停止后端(pluginservice.Service.Uninstall / Disable)。此前禁用、卸载都不停后端,卸载也是删完包才停语言服务器。
- 修正 TestContentProviderRegistrationRules 未加锁写 BackendHost.sessions 的数据竞争。
- 新增 Agent Note,更新功能矩阵;既有债务建单 #1841、#1842、#1843。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ghfind-review ghfind-review Bot added the review: high ghfind author score; see https://ghfind.com label Nov 13, 2026
@1lck
1lck marked this pull request as ready for review November 13, 2026 07:07
@1lck
1lck merged commit 549696f into preview Nov 13, 2026
7 of 13 checks passed
@1lck
1lck deleted the fix/plugin-install-enable-and-uninstall-state branch November 13, 2026 07:07
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