From dadeeca765f2e9c6a55294941e25c1773149a357 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:25:36 -0700 Subject: [PATCH] fix(relay): use safe process runner for inspection --- .../pty-child-process-inspection.test.ts | 60 +++++++++++++++++++ src/relay/pty-child-process-inspection.ts | 21 +++---- 2 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 src/relay/pty-child-process-inspection.test.ts diff --git a/src/relay/pty-child-process-inspection.test.ts b/src/relay/pty-child-process-inspection.test.ts new file mode 100644 index 00000000000..e164fdff086 --- /dev/null +++ b/src/relay/pty-child-process-inspection.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { inspectProcessChildren } from './pty-child-process-inspection' +import { runProcess } from '../shared/child-process/run-process' + +vi.mock('../shared/child-process/run-process', () => ({ runProcess: vi.fn() })) + +const result = (overrides: Partial>> = {}) => ({ + code: 0, + signal: null, + stdout: '42\n', + stderr: '', + timedOut: false, + ...overrides +}) + +describe('inspectProcessChildren', () => { + beforeEach(() => vi.mocked(runProcess).mockReset()) + + it('reports children only from successful pgrep output', async () => { + vi.mocked(runProcess).mockResolvedValue(result()) + + await expect(inspectProcessChildren(41)).resolves.toEqual({ hasChildProcesses: true }) + expect(runProcess).toHaveBeenCalledWith({ + program: 'pgrep', + args: ['-P', '41'], + timeoutMs: 3000 + }) + }) + + it('reports no children only from the pgrep no-match exit', async () => { + vi.mocked(runProcess).mockResolvedValue(result({ code: 1, stdout: '' })) + + await expect(inspectProcessChildren(41)).resolves.toEqual({ hasChildProcesses: false }) + }) + + it.each([ + result({ code: 0, stdout: '' }), + result({ code: 2, stdout: '' }), + result({ code: null, signal: 'SIGTERM', stdout: '' }), + result({ code: 1, stdout: '', timedOut: true }) + ])('marks degraded process reads unavailable', async (processResult) => { + vi.mocked(runProcess).mockResolvedValue(processResult) + + await expect(inspectProcessChildren(41)).resolves.toEqual({ + hasChildProcesses: false, + unavailable: true + }) + }) + + it('marks spawn failures unavailable', async () => { + vi.mocked(runProcess).mockImplementationOnce(() => { + throw new Error('pgrep missing') + }) + + await expect(inspectProcessChildren(41)).resolves.toEqual({ + hasChildProcesses: false, + unavailable: true + }) + }) +}) diff --git a/src/relay/pty-child-process-inspection.ts b/src/relay/pty-child-process-inspection.ts index 6184911b5e0..4994849be7b 100644 --- a/src/relay/pty-child-process-inspection.ts +++ b/src/relay/pty-child-process-inspection.ts @@ -1,22 +1,23 @@ -import { execFile as execFileCb } from 'node:child_process' -import { promisify } from 'node:util' - -const execFile = promisify(execFileCb) +import { runProcess } from '../shared/child-process/run-process' export async function inspectProcessChildren(pid: number): Promise<{ hasChildProcesses: boolean unavailable?: true }> { try { - const { stdout } = await execFile('pgrep', ['-P', String(pid)], { - encoding: 'utf-8', - timeout: 3000 + const result = await runProcess({ + program: 'pgrep', + args: ['-P', String(pid)], + timeoutMs: 3000 }) - return { hasChildProcesses: stdout.trim().length > 0 } - } catch (error) { - if ((error as { code?: string | number }).code === 1) { + if (result.code === 0 && result.stdout.trim().length > 0) { + return { hasChildProcesses: true } + } + if (result.code === 1 && !result.timedOut) { return { hasChildProcesses: false } } return { hasChildProcesses: false, unavailable: true } + } catch { + return { hasChildProcesses: false, unavailable: true } } }