This commit is contained in:
Hiroki 2026-01-30 11:55:31 +00:00 committed by GitHub
commit fd897e3fe6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 PtyExitEvent = { exitCode: number; signal?: number };
type PtyListener<T> = (event: T) => void; type PtyListener<T> = (event: T) => void;
type PtyDisposable = { dispose: () => void };
type PtyHandle = { type PtyHandle = {
pid: number; pid: number;
write: (data: string | Buffer) => void; write: (data: string | Buffer) => void;
onData: (listener: PtyListener<string>) => void; onData: (listener: PtyListener<string>) => PtyDisposable;
onExit: (listener: PtyListener<PtyExitEvent>) => void; onExit: (listener: PtyListener<PtyExitEvent>) => PtyDisposable;
kill: (signal?: string) => void;
}; };
type PtySpawn = ( type PtySpawn = (
file: string, file: string,
@ -361,6 +363,8 @@ async function runExecProcess(opts: {
let child: ChildProcessWithoutNullStreams | null = null; let child: ChildProcessWithoutNullStreams | null = null;
let pty: PtyHandle | null = null; let pty: PtyHandle | null = null;
let stdin: SessionStdin | undefined; let stdin: SessionStdin | undefined;
let ptyDataDisposable: PtyDisposable | null = null;
let ptyExitDisposable: PtyDisposable | null = null;
if (opts.sandbox) { if (opts.sandbox) {
const { child: spawned } = await spawnWithFallback({ const { child: spawned } = await spawnWithFallback({
@ -601,7 +605,7 @@ async function runExecProcess(opts: {
if (pty) { if (pty) {
const cursorResponse = buildCursorPositionResponse(); const cursorResponse = buildCursorPositionResponse();
pty.onData((data) => { ptyDataDisposable = pty.onData((data) => {
const raw = data.toString(); const raw = data.toString();
const { cleaned, requests } = stripDsrRequests(raw); const { cleaned, requests } = stripDsrRequests(raw);
if (requests > 0) { if (requests > 0) {
@ -664,10 +668,18 @@ async function runExecProcess(opts: {
}; };
if (pty) { if (pty) {
pty.onExit((event) => { ptyExitDisposable = pty.onExit((event) => {
const rawSignal = event.signal ?? null; const rawSignal = event.signal ?? null;
const normalizedSignal = rawSignal === 0 ? null : rawSignal; const normalizedSignal = rawSignal === 0 ? null : rawSignal;
handleExit(event.exitCode ?? null, normalizedSignal); 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) { } else if (child) {
child.once("close", (code, exitSignal) => { child.once("close", (code, exitSignal) => {

View File

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