🐛 fix(vscode): 完成列/删 worktree 时自动杀掉占用进程再删目录

This commit is contained in:
cruldra committed 2026-08-05 13:31:00 +08:00
1 parent 23e6f67373
commit 29aaabdabf
4 files changed
+133 -41

No files matched your search

+58 -1
View File
@@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { describe, expect, it } from 'vitest'
import { findProcessesUsingWorktree, resolveWorktreePath } from './worktree'
import { findProcessesUsingWorktree, killProcessesUsingWorktree, resolveWorktreePath } from './worktree'
describe('resolveWorktreePath', () => {
const workspaceRoot = '/workspace/project'
@@ -55,3 +55,60 @@ describe.runIf(existsSync('/proc'))('findProcessesUsingWorktree', () => {
}
})
})
// 杀占用进程:与 find 同依赖 /proc,非 Linux 跳过。
describe.runIf(existsSync('/proc'))('killProcessesUsingWorktree', () => {
it('杀掉 cwd 落在 worktree 下的进程,find 结果变空', async () => {
const dir = mkdtempSync(path.join(os.tmpdir(), 'wt-kill-'))
const child = spawn('sleep', ['30'], { cwd: dir, stdio: 'ignore' })
try {
await new Promise<void>((resolve, reject) => {
child.on('spawn', () => resolve())
child.on('error', reject)
})
expect(findProcessesUsingWorktree(dir).some(h => h.pid === String(child.pid))).toBe(true)
const killed = killProcessesUsingWorktree(dir)
expect(killed.some(h => h.pid === String(child.pid))).toBe(true)
// 等子进程真正退出(kill 函数已等 300–500ms,这里再兜底)
await new Promise<void>((resolve) => {
if (child.killed || child.exitCode !== null)
resolve()
else
child.on('exit', () => resolve())
setTimeout(resolve, 1000)
})
expect(findProcessesUsingWorktree(dir).some(h => h.pid === String(child.pid))).toBe(false)
}
finally {
try {
child.kill('SIGKILL')
}
catch {}
rmSync(dir, { recursive: true, force: true })
}
})
it('不杀 process.pid / process.ppid(命中时跳过)', () => {
// chdir 到独占临时目录,让 find 能命中本进程 cwd,又不会扫到 vitest/项目其它进程。
const dir = mkdtempSync(path.join(os.tmpdir(), 'wt-self-'))
const prev = process.cwd()
const selfPid = String(process.pid)
const parentPid = String(process.ppid)
try {
process.chdir(dir)
// find 应能看到自身(cwd=dir);kill 必须跳过 self/ppid
expect(findProcessesUsingWorktree(dir).some(h => h.pid === selfPid)).toBe(true)
const killed = killProcessesUsingWorktree(dir)
expect(killed.some(h => h.pid === selfPid)).toBe(false)
expect(killed.some(h => h.pid === parentPid)).toBe(false)
expect(process.kill(process.pid, 0)).toBe(true)
}
finally {
process.chdir(prev)
rmSync(dir, { recursive: true, force: true })
}
})
})
+49 -4
View File
@@ -110,10 +110,8 @@ export interface WorktreeProcHit {
/**
* 扫 /proc 找出 cwd 或打开的 fd 落在 worktree 目录(含子目录)下的进程。
*
* 删 worktree 前的守门:dev server(pnpm dev / vite / next dev)常驻在 worktree
* 里持续往构建产物目录写文件,此时递归删除会与写入竞态——rm 删完子项后 rmdir
* 父目录,进程刚好又塞进新文件 → ENOTEMPTY。force:true 只免疫「文件不存在」,
* 救不了「删除期间目录被重新填充」。所以占用就别删,交给用户先停进程。
* 扫占用 worktree 的进程。dev server 等常驻进程会让递归删除撞 ENOTEMPTY
* (删期间目录被重新填充);调用方应先 killProcessesUsingWorktree 再删。
*
* 非 Linux(无 /proc)返回空数组 → 调用方降级为直接删除。
*/
@@ -166,3 +164,50 @@ export function findProcessesUsingWorktree(absWorktreePath: string): WorktreePro
}
return hits
}
/**
* 删 worktree 前杀掉占用目录的进程(SIGTERM → 短暂等待 → SIGKILL)。
*
* 跳过 process.pid / process.ppid,避免误杀扩展宿主或父 VS Code。
* 非 Linux(find 返回空)为 no-op。返回被尝试杀掉的 hits,供调用方记日志。
*/
export function killProcessesUsingWorktree(absWorktreePath: string): WorktreeProcHit[] {
const selfPid = String(process.pid)
const parentPid = String(process.ppid)
// ① 扫占用 → 过滤掉自身/父进程
const hits = findProcessesUsingWorktree(absWorktreePath)
.filter(h => h.pid !== selfPid && h.pid !== parentPid)
if (hits.length === 0)
return hits
// ② SIGTERM 温和退出(ESRCH=已死 → 忽略)
for (const h of hits) {
try {
process.kill(Number(h.pid), 'SIGTERM')
}
catch (err) {
const code = (err as NodeJS.ErrnoException).code
if (code !== 'ESRCH')
console.warn(`[superpowers] SIGTERM pid ${h.pid} 失败:`, err)
}
}
// ③ 同步等 ~400ms,给 dev server 关句柄的时间
const sab = new SharedArrayBuffer(4)
const ia = new Int32Array(sab)
Atomics.wait(ia, 0, 0, 400)
// ④ 仍存活的 SIGKILL 兜底
for (const h of hits) {
try {
process.kill(Number(h.pid), 0) // 探测是否仍在
process.kill(Number(h.pid), 'SIGKILL')
}
catch (err) {
const code = (err as NodeJS.ErrnoException).code
if (code !== 'ESRCH')
console.warn(`[superpowers] SIGKILL pid ${h.pid} 失败:`, err)
}
}
return hits
}