Skip to content

feat(wsl): 完善 WSL 实例创建后的 HOME 操作能力(issue #19 后续) - #48

Open
1264459640 wants to merge 2 commits into
dsh-plugins:mainfrom
1264459640:feat/wsl-home-ops
Open

feat(wsl): 完善 WSL 实例创建后的 HOME 操作能力(issue #19 后续)#48
1264459640 wants to merge 2 commits into
dsh-plugins:mainfrom
1264459640:feat/wsl-home-ops

Conversation

@1264459640

@1264459640 1264459640 commented Sep 13, 2026

Copy link
Copy Markdown

概述 (Summary)

解决在 WSL2 发行版中创建实例后无法操作其 DSH_HOME 的问题(issue #19 后续)。
建立集中式 WSL 路径桥与双通道执行机制(Windows 侧经 UNC 读写文件 + 发行版内经 wsl.exe 执行命令),将启动器的全部 HOME 管理优势对齐覆盖到 WSL2 实例。


核心改动 (Key Changes)

1. 集中路径桥与发行版按需拉起 (src-tauri/src/wsl.rs)

  • 新增 fs_path / home_fs_path / version_fs_path:Windows 访问路径集中解析,本地路径原样返回,WSL 路径映射为 \\wsl$\<distro>\ UNC 路径。
  • 新增 ensure_distro_running:操作前以 wsl.exe -d <distro> -- true 按需唤醒发行版,并带 5 秒 TTL 内存缓存,避免在高频列表中频繁产生子进程开销。
  • 补充路径映射单元测试(纯本地/WSL 路径双向覆盖)。

2. 纯文件类 HOME 操作对齐

  • Profile 管理 (commands.rs):list_profileslist_profile_infoscreate_profilecopy_profilerename_profiledelete_profile 统一走 home_fs_path,Profile 增删改查及类型判定完全正常。
  • MCP & Skills (mcp.rs, skills.rs):home_path_of 改走 home_fs_path,支持经 UNC 读写 cordis.patch.ymlskills/ 目录。
  • 实例图标 & 日志 (icons.rs, commands.rs):图标读写与快捷方式生成走 UNC 映射;确认日志原生存储于宿主机 data_dir/logs,无需特殊分支。
  • 目录直达 (commands.rs):open_instance_directory 改为 async,在打开目录前先调用 ensure_distro_running

3. 发行版内命令执行与插件管理 (plugins.rs)

  • resolve_instance 语义明确为返回 Windows 可视路径;新增 resolve_instance_linux 返回 Linux 真实路径与 distro 标识。
  • run_dsh_plugin 增加 WSL 感知:文件准备/补丁操作经 UNC 执行,dsh plugin add/remove 在 WSL 内执行(通过 wsl_bash 使用发行版管理的 Node、pnpm 及 Linux store 路径)。
  • dsh_plugin_command 改为 async,使用 wsl_test 探针异步验证发行版内版本文件,并按 6 占位符参数格式化 wsl_bash 脚本。
  • start_install_plugin_file_task:为 WSL 目标安装本地 .tgz 插件时,先复制至发行版临时目录后引用 Linux 路径。

4. 整合包导入导出与实例复制 (modpack.rs, tasks.rs, commands.rs)

  • do_import_modpack 支持导入到现有 WSL 实例:文件写入走 UNC,新增 pnpm_install_profile_wsl 在发行版内执行依赖安装。
  • copy_instance 与后台复制任务解除 WSL 限制,支持以 WSL 实例为源完整克隆到本地 Windows 新 HOME。

5. 内置终端与 TUI 支持 (terminal.rs, tui.rs)

  • terminal::spawn_session:对 WSL 实例启动 wsl.exe -d <distro> -- bash -i,跳过 Windows .cmd shim,工作目录以临时目录启动避免 UNC cwd 报错。
  • tui::start_tui_session:异步检测发行版并经 wsl_bashexec 语义拉起发行版内部的 Node + TUI bin。

6. 前端解禁与本地化 (src/)

  • InstanceEdit.vue:移除针对 WSL 的 profiles/plugins/skills/mcp/terminal 全页禁用占位,按各模块实际能力正常展示。
  • InstallWizard.vue / MigratePluginsDialog.vue:移除对 WSL 实例的过滤,支持向 WSL 安装/迁移插件。
  • locales:移除死键,文档(中/英 README)补充 WSL 运行机制说明。

验证情况 (Verification)

  • 格式化检查cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check 通过。
  • Clippy 门禁cargo clippy --workspace --all-targets -- -D warnings 零警告。
  • Rust 单测cargo test --workspace 115 项单元测试全部 pass(含 wsl::tests 新增测试)。
  • 前端构建 & 类型检查pnpm buildvue-tsc --noEmit && vite build)通过。
  • 真实环境冒烟测试:在真实 Ubuntu 发行版中验证 wsl.exe 命令交互、\\wsl$\Ubuntu\home\... 路径读写与 Linux $HOME 解析正常。
  • 打包验证pnpm tauri build --no-bundle 成功产出 15.4MB release 可执行文件。

- wsl.rs 新增路径桥 fs_path/home_fs_path/version_fs_path/ensure_distro_running(UNC 映射 + TTL 缓存 + 单测),集中处理 WSL 路径
- profiles/MCP/skills/图标/日志走 UNC;插件读操作 resolve_instance 返回 fs 路径
- 插件安装/卸载/迁移在发行版内执行 pnpm(resolve_instance_linux + wsl_bash + 发行版内 node/pnpm/store);本地 .tgz 复制进发行版 tmp
- 整合包导入现有 WSL 实例:UNC 写 profile 文件 + pnpm_install_profile_wsl;导出源经 UNC
- copy_instance 与复制任务支持 WSL 源(经 UNC),移除 WSL 拒绝守卫
- 终端/TUI 对 WSL 启动 wsl.exe bash(发行版内 node+bin,exec 语义)
- 前端解除 WSL 整页禁用(profiles/plugins/skills/mcp/terminal),插件安装/迁移向导纳入 WSL 实例;i18n 清理
- 文档(README 中英)补充 WSL 能力说明
@Gu-ZT

Gu-ZT commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

请求修改(request changes)

把「Windows 侧读文件 + 发行版内执行命令」的意图做成集中路径桥(fs_path/home_fs_path/version_fs_path)+ 双通道,方向是对的;unc_pathsh_quoteWslRootensure_distro_running 的缓存、launch_script、3 条纯函数单测(无 WSL 依赖、硬断言)质量都不错,废弃的 wslTabUnsupported 也从双语 locale 里清干净了。但有一个贯穿性的路径分隔符缺陷会让 WSL 侧的插件/Profile 链路全部失效,必须先修。

阻塞项 1:PathBuf::join 拼 Linux 路径会产出反斜杠混合路径,经 wsl.exe / bash 传下去必然失败

Windows 上 PathBuf::join 使用 \ 作分隔符,而 resolve_instance_linuxplugins.rs:1030)返回的是发行版内的 Linux 路径。本机 rustc 1.98.0 实测:

PathBuf::from("/home/u/.dsh-launcher/homes/x").join("profiles")
    == /home/u/.dsh-launcher/homes/x\profiles

命中的关键链路:

  • plugins.rs:2352-2357dsh_plugin_command):let bin = crate::process::version_bin(version_dir);,其中 version_dir 来自 resolve_instance_linux 的 Linux 路径,version_bin 内部 .join("node_modules").join("@deepseek-ai").join("dsh").join("lib")...process.rs)→ 得到 /home/u/.dsh-launcher/versions/<v>\node_modules\@deepseek-ai\dsh\lib\bin.js,随后 wsl_test(d, "-s", &bin.to_string_lossy()) 把这个字符串原样 argv 传给发行版内的 test -ssh_quotewsl.rs:40-42)不做反斜杠折叠,发行版里这不是合法路径 → test -s 恒假 → 函数返回「版本安装不完整(缺少 …),请重新安装该 DSH 版本」。
    该函数是 WSL 下三处唯一入口:插件安装(plugins.rs:2180)、relink_profile_storeplugins.rs:2292)、插件卸载(run_dsh_plugin)。即 WSL 实例的插件安装/卸载/store 重链 100% 失败,而本 PR 已在 InstallWizard.vue:24MigratePluginsDialog.vue 放开了这些入口。
  • modpack.rs:2020pnpm_install_profile_wsl(..., &home_linux.join("profiles").join(&profile_name), ...)home_linux 来自 home.path.clone()modpack.rs:1885 分支,Linux 路径)→ bash 脚本里 cd '/home/u/.../profiles\pack' 同样失败。

最小修复:不要用 PathBuf 拼 Linux 路径。给 process::version_bin 加一个返回 String 的 Linux 变体(内部用 / 手工拼接),或在 wsl.rs 提供 linux_join(base: &str, rel: &str) -> String 并在 WSL 分支统一走它;建议给 wsl_test/wsl_bashdebug_assert!(!path.contains('\\')) 之类的护栏防回归。

阻塞项 2:路径桥对「本地 home + Linux 路径」的组合同样失真

fs_pathwsl.rs:83-101)在 distro == None 时返回 linux.to_path_buf() 原样 shell 字符串。但调用方在拿到结果后还会 .join("profiles")

  • commands.rs:501/545 之外的 create_profilehome_fs_path(home).join("profiles"))、copy/rename/delete profileplugins.rs:2128mcp.rsskills.rsicons.rs 等——若某个 home 的 path 是 Linux 形态而 wslNone(例如用户手工改过 config,或本 PR 复制实例链路产出的组合,见阻塞项 3),会得到 /home/u/...\profiles 这种既非 Linux 也非合法 UNC 的字符串。

另外注意这条在 main 上原本不可达(WSL tab 被整体禁用),是本 PR 删掉 InstanceEdit.vuewslTabUnsupported 分支后才暴露的,所以必须在本 PR 内解决。

最小修复:让路径桥只负责产出Windows 可用路径,Linux 侧另设 home_linux_path/version_linux_path;UNC 与子路径拼接改成一次性 format!PathBuf::join 在 UNC 前缀上表现正常,但在 Linux 路径上不行),调用方不再对 fs_path 的产物做 join

阻塞项 3:复制 WSL 实例产出的本地实例,其 version_id 仍指向 Linux 版本目录

tasks.rs:1950 新建实例时 version_id: source.version_id.clone(),而新建的 HOME 是本地(wsl: Nonetasks.rs:1937-1941)。WSL 源实例的版本记录是 DshVersion { wsl: Some(distro), dir: /home/... },于是复制出的实例变成 home.wsl == None + version.wsl == Some(_)process.rs 的「实例 DSH 版本与运行环境不匹配」校验看的是 home 侧(不报错),但 version_bin_ready(&version.dir) 对 Linux 路径必然为假 → 启动报「版本安装不完整」。

最小修复:复制路径在源 HOME 是 WSL 时,要么拒绝(提示需先安装本地版本),要么自动改绑一个 wsl.is_none() 的同版本记录。

阻塞项 4:list_profiles / list_profile_infos 是同步命令,发行版休眠时静默返回空列表

commands.rs:490commands.rs:530 都是同步 #[tauri::command]fn,非 async fn),因此无法 await ensure_distro_running;实现是 if let Ok(entries) = std::fs::read_dir(&profiles_dir)commands.rs:501/545),UNC 读失败(发行版未启动/自动休眠)时返回空列表且不报错

后果:Profile 页显示「没有 profile」;MigratePluginsDialog.vue 的源 Profile 下拉恒为空(api.listProfiles 走同一命令);InstanceEdit.vue 的 profile 列表同样为空。

最小修复:改成 async fn,在 home.wsl.is_some() 时先 ensure_distro_running,并把 read_dir 的错误显式上抛,让「发行版不可用」与「确实没有 profile」可区分。

阻塞项 5:list_installed_plugins / check_plugin_updates / set_plugins_enabled 缺同样的前置检查

plugins.rs:82110981169 三个命令都是 async fn,但没有任何一处调用 ensure_distro_running(本 PR 只在 plugins.rs:1216/1713/1798/2123modpack.rs:1554/1972tasks.rs:1871tui.rs:129commands.rs:1291 加了)。read_profile_manifestplugins.rs:787-798)在路径不存在时静默返回空 manifest

后果:发行版休眠时插件列表恒为空 → UI 判定「未安装」;更严重的是 set_plugins_enabledplugins.rs:1169-1192)从空字符串起算 raw写回 cordis.patch.yml,等于静默覆盖掉文件里已有的 insertdisabled 行(上一轮 #37 刚修好的那类损坏)。

最小修复:这三个命令在读写前统一 ensure_distro_running,并把「WSL home 根目录不可读」与「真的没有插件」区分开——前者必须返回错误,绝不能返回空结果后继续写文件。

阻塞项 6:store_paths_match 用 Windows 语义比较 Linux store 路径

plugins.rs:2239-2251 的归一化做 replace('/', "\\")cfg!(windows) 为真时 to_lowercase()。WSL 分支传入的 linux_storeWslRoot::pnpm_store() 拼出的 /home/...,而 .modules.yaml 里 pnpm 记录的是发行版内真实路径。大小写(/home/User vs /home/user)或分隔符形态不同即误判「不一致」→ 触发无谓的 relink_profile_store(重操作)或反向漏判导致 pnpm 抛 ERR_PNPM_UNEXPECTED_STORE

最小修复:让该函数接受「是否 Linux 语义」参数(WSL 分支不做 lowercase、只把 \ 折叠成 /),并补一条 Linux store 的单测——现有 store_paths_match_handles_versioned_subdir_and_slashes 只覆盖 Windows 语义。

非阻塞建议

  1. ensure_distro_running 的 5s TTL 与 WSL2 自动休眠叠加,会让每次列表型调用都可能拉起一个 wsl.exe 子进程。建议改成「先做 fs 操作,失败再 ensure 并重试一次」,而不是每次先探测。
  2. ensure_distro_runningwsl.exe -- true 探测依赖发行版默认 PATH 里的 true,建议用 -- /bin/truebash -lc true 更稳。
  3. wsl.rs:82-83 的注释声称「business code never scatters unc_path calls」,但实际有 12 处直调(process.rsscan.rscommands.rs:1292plugins.rs:1217/1799/2124/1725modpack.rs:1975tasks.rs:1872 等),且 process.rs 全程未被本 PR 修改。建议要么把 process.rs/scan.rs 一并切到 version_fs_path,要么弱化注释,避免后来者误信。
  4. commands.rs:1355create_launch_shortcut)经 home_fs_path 读 WSL home 内的图标但没 ensure_distro_running,发行版休眠时静默回落到默认图标,用户无感。
  5. InstanceEdit.vuemcpScopePathhome.path.includes('\\') 猜分隔符,WSL home(/home/...)会显示 Linux 形式的路径,但实际写入的是 \\wsl$\...,属展示不一致。
  6. plugins.rs:1724-1727 把插件 tgz 复制进发行版 ~/.dsh-launcher/tmp没有任何清理路径,会在发行版内持续增长;且该函数用 if let Ok((_, _, Some(distro))) = resolve_instance_linux(...),实例不存在时会静默走本地分支、把 Windows 路径当 Linux 路径交给 pnpm,建议改成显式错误。
  7. plugins.rs:2072-2097 的任务取消对 WSL 分支只 kill()wsl.exe wrapper,发行版内的 pnpm install 会成为孤儿继续改写 profile。对照本 PR 在实例启动路径已实现的「inner PID + wsl kill」(process.rs:559-567),这里应同样处理。
  8. UNC 写入的 .npmrc/YAML 未做行尾归一化:ensure_build_scripts_allowed/ensure_profile_npmrcplugins.rs:2130-2132)与 cordis.patch.yml 写入若带 \r\n,Linux 侧 pnpm 会把 \r 当作值的一部分,且 - id: xxx\r 会与 strip_cordis_rows/line_id 的匹配失配。我无法在无 WSL 的机器上确认真实行尾,建议显式写 \n 并加断言。
  9. 通过 \\wsl$ 创建/写入 profile 与快照内容(commands.rs 的 profile CRUD、plugins.rs:2128-2132modpack.rs:1978-2010)而 pnpm 在发行版内消费这棵树,两者的权限/可执行位/扩展属性语义不一致(本 PR 自己也强调了「必须用发行版内 pnpm,否则树 Linux 跑不了」,但树是 Windows 侧建的)。建议把 profile/快照的写入也移入发行版,或至少在 README/PR 描述里把这块标注为「实验、需真机验证」,不要写成已对齐。
  10. README(README.md:41README.zh_CN.md:41)写「Once created, HOME works like a local instance (profiles CRUD, plugin install/enable/disable/uninstall, skills, MCP, icons, logs, modpack export/import, embedded terminal)」。按上述阻塞项,其中插件安装/卸载与 profiles CRUD 在代码层面目前走不通,建议在修复前调整措辞。

验证情况

  • 完整读过 18 个文件的 diff(1278 行)与关键区域逐行阅读:wsl.rs 全文、plugins.rs(解析实例/插件命令/CLI 目标/dsh_plugin_command/store 比较/任务取消)、commands.rs(home/profile 命令、create_launch_shortcut)、tasks.rs(复制实例、WSL 版本安装)、modpack.rs(导入的 WSL 分支)、terminal.rs/tui.rs(WSL shell 与 cwd)、skills.rs/mcp.rs/icons.rs/process.rs/scan.rs,以及前端 InstanceEdit.vue/InstallWizard.vue/MigratePluginsDialog.vue
  • 独立探针验证反斜杠混合(本机 rustc 1.98.0,仓库外运行):PathBuf::from("/home/user/.dsh-launcher/homes/x").join("profiles")/home/user/.dsh-launcher/homes/x\profiles,且该串 exists() 为 false。
  • 本机 wsl.exe 存在但无任何发行版wsl --status 退出码 50),因此只能验证「无发行版」分支;\\wsl$ 的真实读写行为、wsl.exe -- bash -lc 的参数保真度、发行版休眠时 UNC 的失败形态(快速失败还是阻塞)、UNC 写入的实际行尾,都无法在本机确认。
  • 已确认 clippy 在无 WSL 平台不产生新的 cfg 死代码风险(wsl.rs:169 已有既存约定,wsl_test 调用点在 Linux 上仍被引用,ead6a23 修的是 Some(d)Some(_))。
  • CI 状态:本 PR 的 pull_request 运行此前处于 action_required(首次贡献者需批准),我已批准并跑完,三平台 Quality + Integration checks 全绿。但请注意 CI 三个 runner 都没有安装 WSL,对本 PR 的 WSL 代码路径覆盖率为 0,因此"CI 绿"不能作为本 PR 的正确性证据。

修完阻塞项 1~6(其中 1、2 是同一个根因的两面)后再评审。这个功能对 WSL 用户价值很大,主要卡在路径分隔符与「发行版未就绪」的处理上。

@Gu-ZT

Gu-ZT commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

补充说明(维护者):经评估,本 PR 修复上面 6 项阻断问题需要跨文件的 WSL 路径分隔符重构(PathBuf::join 反斜杠混入 Linux 路径、fs_path 透传、store_paths_match Windows 语义等),且本仓库 CI 无 WSL 环境、维护方本机也无 WSL 可验证,修改无法被可靠验证。按无验证条件的改动不合入原则,本次不继续推进合入,PR 保持开启;issue #49 保持 open。若后续有 WSL CI 环境或作者补齐上述问题,欢迎重新评审。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 完善 WSL2 实例的 DSH_HOME 可操作性(支持 Profile / 插件 / Skills / MCP / 终端管理)

2 participants