fix(exec): clean up PTY resources to prevent FD leaks

Add proper cleanup of node-pty resources when PTY process exits:
- Dispose onData and onExit listeners
- Call pty.kill() to release file descriptors

This prevents gradual FD accumulation that leads to EBADF errors
after extended gateway uptime.

Closes #4102
This commit is contained in:
HirokiKobayashi-R 2026-01-30 04:36:19 +09:00
parent 6372242da7
commit a0d1af93e4
2 changed files with 20 additions and 6 deletions

View File

@ -79,11 +79,13 @@ const APPROVAL_SLUG_LENGTH = 8;
type PtyExitEvent = { exitCode: number; signal?: number };
type PtyListener<T> = (event: T) => void;
type PtyDisposable = { dispose: () => void };
type PtyHandle = {
pid: number;
write: (data: string | Buffer) => void;
onData: (listener: PtyListener<string>) => void;
onExit: (listener: PtyListener<PtyExitEvent>) => void;
onData: (listener: PtyListener<string>) => PtyDisposable;
onExit: (listener: PtyListener<PtyExitEvent>) => PtyDisposable;
kill: (signal?: string) => void;
};
type PtySpawn = (
file: string,
@ -361,6 +363,8 @@ async function runExecProcess(opts: {
let child: ChildProcessWithoutNullStreams | null = null;
let pty: PtyHandle | null = null;
let stdin: SessionStdin | undefined;
let ptyDataDisposable: PtyDisposable | null = null;
let ptyExitDisposable: PtyDisposable | null = null;
if (opts.sandbox) {
const { child: spawned } = await spawnWithFallback({
@ -601,7 +605,7 @@ async function runExecProcess(opts: {
if (pty) {
const cursorResponse = buildCursorPositionResponse();
pty.onData((data) => {
ptyDataDisposable = pty.onData((data) => {
const raw = data.toString();
const { cleaned, requests } = stripDsrRequests(raw);
if (requests > 0) {
@ -664,10 +668,18 @@ async function runExecProcess(opts: {
};
if (pty) {
pty.onExit((event) => {
ptyExitDisposable = pty.onExit((event) => {
const rawSignal = event.signal ?? null;
const normalizedSignal = rawSignal === 0 ? null : rawSignal;
handleExit(event.exitCode ?? null, normalizedSignal);
// Clean up PTY resources to prevent FD leaks
try {
ptyDataDisposable?.dispose();
ptyExitDisposable?.dispose();
pty.kill();
} catch {
// Ignore cleanup errors
}
});
} else if (child) {
child.once("close", (code, exitSignal) => {

View File

@ -1,11 +1,13 @@
declare module "@lydell/node-pty" {
export type PtyExitEvent = { exitCode: number; signal?: number };
export type PtyListener<T> = (event: T) => void;
export type PtyDisposable = { dispose: () => void };
export type PtyHandle = {
pid: number;
write: (data: string | Buffer) => void;
onData: (listener: PtyListener<string>) => void;
onExit: (listener: PtyListener<PtyExitEvent>) => void;
onData: (listener: PtyListener<string>) => PtyDisposable;
onExit: (listener: PtyListener<PtyExitEvent>) => PtyDisposable;
kill: (signal?: string) => void;
};
export type PtySpawn = (