🐛 fix(vscode): 删 worktree 前扫 /proc 检测进程占用,命中则中止清理

dev server 常驻在 worktree 里持续写构建产物,递归删除与写入竞态导致
rmdir 撞 ENOTEMPTY。新增 findProcessesUsingWorktree 扫 /proc 找 cwd 或
fd 落在 worktree 下的进程,三处删除调用点(拖到完成列 / 手动删 / 删整工单)
命中即弹错误 toast 列出 pid+命令并中止(不删、不杀进程),用户停掉后重试。
非 Linux 无 /proc 返回空,降级为原直接删除。
This commit is contained in:
2026-07-13 09:26:23 +08:00
parent b585016cd7
commit aff4ee2758
4 changed files with 155 additions and 3 deletions
+66 -1
View File
@@ -7,7 +7,7 @@
*/
import { execFile } from 'node:child_process'
import { promises as fsp } from 'node:fs'
import { promises as fsp, readdirSync, readFileSync, readlinkSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
@@ -101,3 +101,68 @@ export async function removeWorktreeDir(workspaceRoot: string, absWorktreePath:
})
})
}
export interface WorktreeProcHit {
pid: string
cmd: string
}
/**
* 扫 /proc 找出 cwd 或打开的 fd 落在 worktree 目录(含子目录)下的进程。
*
* 删 worktree 前的守门:dev serverpnpm dev / vite / next dev)常驻在 worktree
* 里持续往构建产物目录写文件,此时递归删除会与写入竞态——rm 删完子项后 rmdir
* 父目录,进程刚好又塞进新文件 → ENOTEMPTY。force:true 只免疫「文件不存在」,
* 救不了「删除期间目录被重新填充」。所以占用就别删,交给用户先停进程。
*
* 非 Linux(无 /proc)返回空数组 → 调用方降级为直接删除。
*/
export function findProcessesUsingWorktree(absWorktreePath: string): WorktreeProcHit[] {
const hits: WorktreeProcHit[] = []
const target = path.resolve(absWorktreePath)
const prefix = target.endsWith(path.sep) ? target : target + path.sep
let pids: string[]
try {
pids = readdirSync('/proc').filter(n => /^\d+$/.test(n))
}
catch {
return hits // 无 /proc → 无从扫描
}
for (const pid of pids) {
let used = false
// ① cwd 落在 worktree 下(dev server 常把工作目录设在里面)
try {
const cwd = readlinkSync(`/proc/${pid}/cwd`)
if (cwd === target || cwd.startsWith(prefix)) // (deleted) 后缀也照样命中前缀
used = true
}
catch {} // 进程已退出 / 无权限 → 跳过
// ② 否则查打开的 fd 是否指向 worktree 下的文件(构建产物写句柄)
if (!used) {
try {
for (const fd of readdirSync(`/proc/${pid}/fd`)) {
try {
const p = readlinkSync(`/proc/${pid}/fd/${fd}`)
if (p === target || p.startsWith(prefix)) {
used = true
break
}
}
catch {} // 单个 fd 竞态关闭 → 跳过
}
}
catch {} // /proc/<pid>/fd 读不到 → 跳过
}
if (used) {
let cmd = ''
try {
cmd = readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, ' ').trim()
}
catch {} // cmdline 读不到就留空,pid 仍报给用户
hits.push({ pid, cmd })
}
}
return hits
}