Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/core/src/services/shellExecutionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,25 @@ describe('ShellExecutionService', () => {
expect(mockHeadlessTerminal.scrollLines).toHaveBeenCalledWith(10);
});

it('should ignore EBADF when the master fd was already closed', async () => {
// node-pty's native resize throws "ioctl(2) failed, EBADF" when the
// master fd has been closed by destroyPtyProcess() ahead of the
// activePtys map cleanup. The race is wider under Bun.
const resizeError = new Error('ioctl(2) failed, EBADF');
mockPtyProcess.resize.mockImplementation(() => {
throw resizeError;
});

await expect(
simulateExecution('ls -l', (pty) => {
ShellExecutionService.resizePty(pty.pid, 100, 40);
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
}),
).resolves.not.toThrow();

expect(mockPtyProcess.resize).toHaveBeenCalledWith(100, 40);
});

it('should not throw when resizing a pty that has already exited (Windows)', () => {
const resizeError = new Error(
'Cannot resize a pty that has already exited',
Expand Down
15 changes: 10 additions & 5 deletions packages/core/src/services/shellExecutionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1473,11 +1473,16 @@ export class ShellExecutionService {
const isWindowsPtyError = err.message?.includes(
'Cannot resize a pty that has already exited',
);

if (isEsrch || isWindowsPtyError) {
// On Unix, we get an ESRCH error.
// On Windows, we get a message-based error.
// In both cases, it's safe to ignore.
// node-pty's native resize throws "ioctl(2) failed, EBADF" (or ENOTTY)
// when the master fd has already been closed. This is the same race as
// ESRCH: destroyPtyProcess() in onExit closes the fd before we remove
// the entry from activePtys, so a concurrent resizePty() can land in
// between. The race is wider under Bun, which is where this surfaces.
const isClosedFdError =
err.message?.includes('EBADF') || err.message?.includes('ENOTTY');

if (isEsrch || isWindowsPtyError || isClosedFdError) {
// pty is gone; nothing to resize.
} else {
throw e;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/utils/getPty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export const getPty = async (): Promise<PtyImplementation> => {
if (process.env['GEMINI_PTY_INFO'] === 'child_process') {
return null;
}
// node-pty's master-fd tty.ReadStream wrapper doesn't deliver data to onData
// under Bun, and resize hits EBADF when the fd is closed early. Fall back to
// the child_process path (which already has Bun-specific handling for
// detached/SIGHUP). Set GEMINI_PTY_INFO=node-pty to override and test.
if (
'bun' in process.versions &&
process.env['GEMINI_PTY_INFO'] !== 'node-pty'
) {
return null;
}
try {
const lydell = '@lydell/node-pty';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Expand Down
Loading