Skip to content

feat: WSL/远程文件树与外部变更监听(第二层,基于 #236)#239

Merged
LarryZhu-dev merged 4 commits into
Auto-Plugin:mainfrom
zen010101:feat/wsl-file-watching-layer2
Jun 7, 2026
Merged

feat: WSL/远程文件树与外部变更监听(第二层,基于 #236)#239
LarryZhu-dev merged 4 commits into
Auto-Plugin:mainfrom
zen010101:feat/wsl-file-watching-layer2

Conversation

@zen010101

Copy link
Copy Markdown
Contributor

本 PR 堆叠在 #236(第一层修复)之上。#236 合入前,本 PR 的 diff 会包含 #236 的那个提交(fix: 修复打开 WSL 文件时主进程因 EISDIR 崩溃)。请先审阅 / 合并 #236#236 合入后本 PR 的 diff 会自动收敛为只剩第二层改动。

概述

第一层(#236)止血:打开 \\wsl.localhost\ 下的文件不再因 EISDIR 崩溃,代价是 WSL 路径被跳过自动加载——没有左侧文件树、也不提示外部变更。

第二层(本 PR)恢复功能:让 WSL/远程文件也能 (1) 显示左侧文件树;(2) 外部进程修改文件时重新加载。

为什么 WSL 走轮询

\\wsl.localhost\(9P)上实测:

方式 结果
fs.watch(chokidar 底层) 启动即 EISDIR,不可用
fs.watchFile(Node 轮询) 可靠检测追加 / 原子替换 / 同 mtime 改 size / 删除 / 重建
readdir({withFileTypes}) + stat 正常(不踩 EISDIR)

因此按路径分流:\\wsl.localhost\ / \\wsl$\ 走轮询;本地与 SMB 保持 chokidar 实时,不退化。

前置实测结论(spec §0,已 PASS)

  • 建树安全:生产 scanDirectory 在含 symlink / .so 链 / 大目录 / 深目录的真实 WSL 目录上建树,0 EISDIR / 0 uncaught(readdir 用 d_type 分类不 lstat;真实 dir/file 的 stat 正常)。
  • fs.watchFile 在 9P 可靠:含"同 mtime 仅改 size"也能检出(比较含 size)、删除回调 mtime=0、删后重建跟踪存活。
  • WSL symlink 从 Windows 侧读不透lstat/readlink→EISDIR,stat/readFile→ENOENT(与相对/绝对、指向文件/目录无关)。故 WSL 分支丢弃 symlink(本地/SMB 仍正常显示)。

实现要点

  • src/main/wslWatch.ts(新增,不依赖 electron,回调注入便于单测):
    • 路径分流 classifyWatchTarget + normalizeWatchPath(还原 \\?\UNC\ 前缀,防误判重开 EISDIR);
    • scanDirectory(WSL 丢弃 symlink;本地/SMB 加 isSymbolicLink() 分支);
    • WslDirectoryWatcher:per-dir setInterval + in-flight guard + epoch 丢弃迟到结果 + 快照(path+mtime+size) diff + 不可达不清树 + 300ms 去抖;
    • WslFileWatcher:per-window union 引用计数 + fs.watchFile + mtime=0 删除语义 + size 兜底 + 窗口 focus 主动比对兜底 + before-quit 清理句柄。
  • src/main/ipcBridge.tswatchDirectory / file:watch 按 classify 分流;getDirectoryFiles 改用模块 scanDirectorybrowser-window-focus / before-quit / 窗口关闭清理。
  • src/renderer/utils/workspacePath.tsshouldAutoLoadWorkspace 撤销对远程的跳过(§0 已证建树安全)。
  • src/renderer/hooks/useWorkSpace.ts:远程空树也置加载锁(避免 tabs 变化反复整树重扫拖慢 9P);refreshWorkSpace 加 in-flight 互斥。

测试

  • node:test 22 个(纯函数 + 两个 watcher 管理器,注入假依赖)全过;新增 pnpm test 脚本。
  • 真机集成:用真实 fs.watchFile + snapshotDirectory 在真 \\wsl.localhost\ 上验证目录增删 / 文件追加替换 / focus / 引用计数,9/9 通过。

已知局限(文档化,不在本层解决)

  • WSL symlink 读不透,文件树不显示 symlink;其重指向不可感知。
  • 本地树内嵌指向 WSL 的 junction/symlink、Z:\ 等映射盘:靠第一层 error handler 兜底。
  • 大仓首次建树有秒级延迟(9P + 节点数),属预期(spec §5:刷新延迟 ≈ 间隔 + scan 耗时)。
  • 未编辑文件的外部变更沿用 milkup 既有"静默重载"行为(有未保存编辑时才弹确认框)。

完整设计与评审记录见 docs/spec-wsl-file-watching-layer2.md

zen010101 added 4 commits June 4, 2026 01:00
打开 \\wsl.localhost\ 下的文件时,应用会自动把文件所在目录作为工作区并用
chokidar 递归监听。chokidar 遍历到目录中的 Linux 软链接(如 *.so.0)时会对其
lstat,而 Windows/Node 对经 9P 重定向器暴露的 WSL 软链接做 lstat 会返回 EISDIR
(底层是 9P 不支持读取该软链接的 reparse 数据,返回 Incorrect function,被 libuv
映射成了 EISDIR)。该错误没有任何地方处理,冒泡成主进程 uncaughtException,于是弹出
"A JavaScript error occurred in the main process" 并使文件区卡死。

修复采用双保险:

- workspacePath.ts:isRemoteWorkspacePath 不再依赖 isWindows 做前置短路。该模块运行在
  渲染进程(nodeIntegration:false),那里 process.platform 不可用,isWindows 恒为
  false,会让原有的 WSL/远程路径防护永远失效。去掉该短路后,防护真正生效,从源头避免对
  WSL 目录自动加载工作区与递归监听。UNC 路径(\\wsl.localhost\、\\wsl$\、\\server\share)
  本身是 Windows 专有写法,不会出现在其他平台的合法路径里,无需平台判断即可安全识别。

- ipcBridge.ts:给 file watcher 与 directoryWatcher 各补一个 "error" 监听。chokidar
  出错时会 emit "error",没有监听器时会冒泡成主进程 uncaughtException。补上后任何 watcher
  错误都只记录、不再使应用崩溃,对所有文件系统(不止 WSL)都更健壮。

注:本次修复保证不再崩溃,但 WSL 文件暂时不会自动加载左侧文件树(防护跳过了)。后续会做
第二层改进,让 WSL 文件也能正常显示文件树。
§0 实测 2026-06-06 (Debian11 / Node v22 / Windows host):

A 完整建树无 EISDIR -- PASS:
  - 生产 scanDirectory 在含 symlink/.so链/大目录/深目录 的 WSL 树上建 115 节点,
    0 吞错 / 0 EISDIR / 0 uncaught
  - 新发现: WSL symlink 从 Windows 侧完全读不透
    lstat/readlink=EISDIR, stat(跟随)/readFile=ENOENT (含指向真实存在的目标)
    -> symlink 仅能由 readdir d_type 识别, 拿不到 mtime, 无法解引用/打开

B fs.watchFile 在 9P 可靠性 -- PASS (全绿):
  追加 / 原子替换(mv,新inode) / 同mtime仅改size(size触发) /
  删除(mtime=0) / 删后重建(跟踪存活) / 同秒多写 全部 DETECTED

据 A 缩范围 (spec §4.3/§6.2/§8):
  - WSL 分支默认丢弃 symlink (避免死节点/点开 ENOENT), 重指向不可知文档化为局限
  - isSymbolicLink() 分支只对 local/SMB 有效
  - WSL symlink inert 显示移入 §8 defer
第二层:撤销第一层对 WSL/远程路径的"跳过自动加载工作区",让 WSL 文件也能
显示文件树并在外部变更时提示重载。事件式 fs.watch 在 9P 上启动即 EISDIR,
故 WSL 改走轮询;本地/SMB 保持 chokidar 实时。依据 §0 前置实测(已 PASS)。

新增 src/main/wslWatch.ts(不依赖 electron,回调注入,便于单测):
- 路径分流 classifyWatchTarget / normalizeWatchPath(还原 \\?\UNC\ 防误判重开 EISDIR)
- scanDirectory(WSL 丢弃读不透的 symlink;本地/SMB 加 isSymbolicLink 分支)
- WslDirectoryWatcher:per-dir interval + in-flight guard + epoch 丢弃迟到结果
  + 快照(path+mtime+size)diff + 不可达不清树 + 300ms debounce
- WslFileWatcher:per-window union 引用计数 + fs.watchFile + mtime=0 删除语义
  + size 兜底 + focus checkWindow 兜底 + before-quit dispose

接入 ipcBridge.ts:watchDirectory/file:watch 按 classify 分流;getDirectoryFiles
改用模块 scanDirectory;app browser-window-focus / before-quit / 窗口关闭清理。

renderer:workspacePath.shouldAutoLoadWorkspace 撤销远程跳过;useWorkSpace 远程
空树也置锁(避免 tabs 变化反复重扫慢 9P) + refreshWorkSpace 加 in-flight 互斥。

测试:node:test 22 个(纯函数 + 两个管理器,假依赖) 全过;另用真实 fs.watchFile +
snapshotDirectory 在真 \\wsl.localhost\ 集成验证 9/9(目录增删/文件追加替换/focus/
引用计数)。新增 pnpm test 脚本。

注:GUI 视觉确认(文件树渲染 / 重载弹框)需本机 pnpm run dev 验证。
@LarryZhu-dev LarryZhu-dev merged commit b889dc9 into Auto-Plugin:main Jun 7, 2026
@LarryZhu-dev

Copy link
Copy Markdown
Contributor

@zen010101 感谢你的贡献!

@zen010101 zen010101 deleted the feat/wsl-file-watching-layer2 branch June 7, 2026 15:21
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.

2 participants