herdrdev/herdr · error
poll encountered PTY fd error
Error message
poll encountered PTY fd error
What it means
poll_pty_and_wake treats a POLLERR revent on the PTY fd as a hard failure with ErrorKind::BrokenPipe. POLLERR indicates an error condition on the fd (commonly the peer hung up or the PTY was closed on the other end), so continuing to write/read would be wrong.
Source
Thrown at src/pty/fd.rs:205
if remaining.is_zero() {
return Ok(PtyWakeReadiness::default());
}
remaining_timeout_ms = remaining.as_millis().clamp(1, i32::MAX as u128) as i32;
continue;
}
return Err(err);
}
let pty_revents = if poll_pty { poll_fds[0].revents } else { 0 };
let wake_revents = poll_fds[1].revents;
if (pty_revents | wake_revents) & libc::POLLNVAL != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"poll encountered invalid PTY actor fd",
));
}
if pty_revents & libc::POLLERR != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"poll encountered PTY fd error",
));
}
return Ok(PtyWakeReadiness {
pty_read_ready: pty_revents & (libc::POLLIN | libc::POLLHUP) != 0,
pty_write_ready: pty_revents & (libc::POLLOUT | libc::POLLHUP) != 0,
wake_ready: wake_revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0,
});
}
}
#[cfg(unix)]
pub(crate) fn resize_pty_fd(
fd: RawFd,
rows: u16,
cols: u16,View on GitHub (pinned to f457cff4f2)
Solutions
- Handle this error kind as 'PTY ended': reap the child, mark the session closed, and exit the actor loop cleanly
- Check the child exit status when this fires to distinguish normal exit from real errors
- Ensure shutdown paths are idempotent so a second poll after close cannot occur
- For network-backed PTYs, add reconnection logic at the session layer rather than retrying the poll
Example fix
// before
match fd::poll_pty_and_wake(pty, wake, ms) {
Ok(r) => handle(r),
Err(e) => return Err(e),
}
// after
match fd::poll_pty_and_wake(pty, wake, ms) {
Ok(r) => handle(r),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
self.reap_child_and_finish(); // normal PTY termination path
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
if !actor.is_pty_open() || actor.child_finished() { skip poll and finalize; } Try / catch
match fd::poll_pty_and_wake(pty, wake, ms) {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { reap_child_and_close_session(); }
other => other?,
} Prevention
- Treat POLLERR on a PTY as session termination, not a retryable poll error
- Reap the child when the PTY reports errors
- Ensure the actor loop cannot poll a finished session
When it happens
Trigger: Calling poll_pty_and_wake when the PTY master/slave has an error condition — typically the child exited and the slave side closed, or the terminal device entered an error state.
Common situations: Child process exits while the actor loop is polling; ssh/network-backed PTY dropping; hardware/tty error states; races during session close.
Related errors
- PTY closed while draining writes before handoff
- poll encountered invalid PTY actor fd
- PTY actor was released before handoff quiesce
- timed out draining PTY writes before handoff
- stream ended mid-frame
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/c9f3e784d5832eaa.
Report an issue: GitHub.