Skip to content

Commit 9dc5272

Browse files
committed
try keeping user fd alive longer
1 parent 9f8c550 commit 9dc5272

4 files changed

Lines changed: 50 additions & 16 deletions

File tree

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ export declare class Pty {
6767
* descriptor.
6868
*/
6969
takeFd(): c_int;
70+
closeUserFd(): void;
7071
}

src/lib.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use napi::Status::GenericFailure;
1717
use napi::{self, Env};
1818
use nix::errno::Errno;
1919
use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag};
20-
use nix::libc::{self, c_int, ioctl, FIONREAD, TIOCOUTQ, TIOCSCTTY, TIOCSWINSZ};
20+
use nix::libc::{self, c_int, ioctl, FIONREAD, TIOCSCTTY, TIOCSWINSZ};
2121
use nix::pty::{openpty, Winsize};
2222
use nix::sys::termios::{self, SetArg};
2323

@@ -31,6 +31,7 @@ mod sandbox;
3131
#[allow(dead_code)]
3232
struct Pty {
3333
controller_fd: Option<OwnedFd>,
34+
user_fd: Option<OwnedFd>,
3435
/// The pid of the forked process.
3536
pub pid: u32,
3637
}
@@ -107,9 +108,7 @@ fn poll_pty_fds_until_read(controller_fd: RawFd, user_fd: RawFd) {
107108
loop {
108109
// check both input and output queues for both FDs
109110
let mut controller_inq: i32 = 0;
110-
let mut controller_outq: i32 = 0;
111111
let mut user_inq: i32 = 0;
112-
let mut user_outq: i32 = 0;
113112

114113
// safe because we're passing valid file descriptors and properly sized integers
115114
unsafe {
@@ -120,18 +119,10 @@ fn poll_pty_fds_until_read(controller_fd: RawFd, user_fd: RawFd) {
120119
// break if we can't read
121120
break;
122121
}
123-
124-
// check bytes waiting to be written (TIOCOUTQ)
125-
if ioctl(controller_fd, TIOCOUTQ, &mut controller_outq) == -1
126-
|| ioctl(user_fd, TIOCOUTQ, &mut user_outq) == -1
127-
{
128-
// break if we can't read
129-
break;
130-
}
131122
}
132123

133124
// if all queues are empty, we're done
134-
if controller_inq == 0 && controller_outq == 0 && user_inq == 0 && user_outq == 0 {
125+
if controller_inq == 0 && user_inq == 0 {
135126
break;
136127
}
137128

@@ -349,7 +340,6 @@ impl Pty {
349340

350341
// try to wait for the controller fd to be fully read
351342
poll_pty_fds_until_read(raw_controller_fd, raw_user_fd);
352-
drop(user_fd);
353343

354344
match wait_result {
355345
Ok(status) => {
@@ -379,6 +369,7 @@ impl Pty {
379369

380370
Ok(Pty {
381371
controller_fd: Some(controller_fd),
372+
user_fd: Some(user_fd),
382373
pid,
383374
})
384375
}
@@ -398,6 +389,13 @@ impl Pty {
398389
))
399390
}
400391
}
392+
393+
#[napi]
394+
#[allow(dead_code)]
395+
pub fn close_user_fd(&mut self) -> Result<(), napi::Error> {
396+
self.user_fd.take();
397+
Ok(())
398+
}
401399
}
402400

403401
/// Resize the terminal.

tests/index.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,27 @@ describe('PTY', { repeats: 500 }, () => {
354354
expect(buffer.toString().length).toBe(payload.length);
355355
});
356356

357+
test.only('doesnt miss lots of lines from bash', async () => {
358+
const payload = Array.from({ length: 4096 * 5 }, (_, i) => i).join('\n');
359+
let buffer = Buffer.from('');
360+
const onExit = vi.fn();
361+
362+
const pty = new Pty({
363+
command: 'bash',
364+
args: ['-c', `echo -n "${payload}"`],
365+
onExit,
366+
});
367+
368+
const readStream = pty.read;
369+
readStream.on('data', (data) => {
370+
buffer = Buffer.concat([buffer, data]);
371+
});
372+
373+
await vi.waitFor(() => expect(onExit).toHaveBeenCalledTimes(1));
374+
expect(onExit).toHaveBeenCalledWith(null, 0);
375+
expect(buffer.toString().trim().replace(/\r/g, '')).toBe(payload.trim());
376+
});
377+
357378
testSkipOnDarwin('does not leak files', async () => {
358379
const oldFds = getOpenFds();
359380
const promises = [];

wrapper.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ export class Pty {
7878
markReadFinished = resolve;
7979
});
8080
const mockedExit = (error: NodeJS.ErrnoException | null, code: number) => {
81+
console.log('mockedExit', { error, code });
8182
markExited({ error, code });
83+
84+
setImmediate(() => {
85+
this.#pty.closeUserFd();
86+
});
8287
};
8388

8489
// when pty exits, we should wait until the fd actually ends (end OR error)
@@ -105,8 +110,14 @@ export class Pty {
105110
realExit(result.error, result.code);
106111
};
107112

108-
this.read.once('end', markReadFinished);
109-
this.read.once('close', handleClose);
113+
this.read.once('end', () => {
114+
console.log('read end');
115+
markReadFinished();
116+
});
117+
this.read.once('close', () => {
118+
console.log('read close');
119+
handleClose();
120+
});
110121

111122
// PTYs signal their done-ness with an EIO error. we therefore need to filter them out (as well as
112123
// cleaning up other spurious errors) so that the user doesn't need to handle them and be in
@@ -135,7 +146,10 @@ export class Pty {
135146
throw err;
136147
};
137148

138-
this.read.on('error', handleError);
149+
this.read.on('error', (err) => {
150+
console.error('error:', err);
151+
handleError(err);
152+
});
139153
}
140154

141155
close() {

0 commit comments

Comments
 (0)