fix: time out process cwd lsof lookup (#3809)

This commit is contained in:
Neil
2026-05-30 11:04:48 -07:00
committed by GitHub
parent 1645b56e11
commit 05b78b8f2f
2 changed files with 80 additions and 8 deletions
+33 -1
View File
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, readlinkMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
@@ -26,6 +26,10 @@ describe('resolveProcessCwd', () => {
})
})
afterEach(() => {
vi.useRealTimers()
})
it('bounds cached cwd results across unique process ids', async () => {
const { resolveProcessCwd } = await import('./process-cwd')
@@ -41,4 +45,32 @@ describe('resolveProcessCwd', () => {
await expect(resolveProcessCwd(1)).resolves.toBe('/cwd/1')
expect(readlinkMock).toHaveBeenCalledTimes(258)
})
it('falls back when lsof never reports completion', async () => {
vi.useFakeTimers()
readlinkMock.mockRejectedValue(new Error('proc unavailable'))
const killMock = vi.fn()
execFileMock.mockImplementation(() => ({ kill: killMock }))
const { resolveProcessCwd } = await import('./process-cwd')
let settled = false
const cwdPromise = resolveProcessCwd(42).then((cwd) => {
settled = true
return cwd
})
await vi.waitFor(() =>
expect(execFileMock).toHaveBeenCalledWith(
'lsof',
['-a', '-p', '42', '-d', 'cwd', '-Fn'],
{ encoding: 'utf-8', timeout: 1500 },
expect.any(Function)
)
)
await vi.advanceTimersByTimeAsync(1500)
expect(settled).toBe(true)
await expect(cwdPromise).resolves.toBe('')
expect(killMock).toHaveBeenCalled()
})
})
+47 -7
View File
@@ -1,8 +1,5 @@
import { execFile as execFileCb } from 'child_process'
import { readlink } from 'fs/promises'
import { promisify } from 'util'
const execFile = promisify(execFileCb)
/**
* Resolve the current working directory of a local process by pid.
@@ -92,10 +89,7 @@ async function doResolve(pid: number): Promise<string> {
// and emits cwd records for every process on the system, so the n-line
// scan below picks up the first unrelated process (often pid ~391 with
// cwd `/`) and returns `/` regardless of the target pid's real cwd.
const { stdout } = await execFile('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], {
encoding: 'utf-8',
timeout: LSOF_TIMEOUT_MS
})
const stdout = await readCwdWithLsof(pid)
for (const line of stdout.split('\n')) {
if (line.startsWith('n') && line.includes('/')) {
// Why: lsof -d cwd is authoritative — don't second-guess it with
@@ -111,3 +105,49 @@ async function doResolve(pid: number): Promise<string> {
return ''
}
function readCwdWithLsof(pid: number): Promise<string> {
return new Promise((resolve, reject) => {
let settled = false
let child: ReturnType<typeof execFileCb> | undefined
const timer = setTimeout(() => {
if (settled) {
return
}
settled = true
child?.kill()
reject(new Error(`lsof timed out after ${LSOF_TIMEOUT_MS}ms`))
}, LSOF_TIMEOUT_MS)
const settle = (callback: () => void): void => {
if (settled) {
return
}
settled = true
clearTimeout(timer)
callback()
}
// Why: execFile's timeout only signals lsof; a missing callback would
// otherwise leave the shared per-pid cwd lookup promise cached forever.
try {
child = execFileCb(
'lsof',
['-a', '-p', String(pid), '-d', 'cwd', '-Fn'],
{
encoding: 'utf-8',
timeout: LSOF_TIMEOUT_MS
},
(error, stdout) => {
if (error) {
settle(() => reject(error))
return
}
settle(() => resolve(String(stdout)))
}
)
} catch (error) {
settle(() => reject(error))
}
})
}