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

  1. Handle this error kind as 'PTY ended': reap the child, mark the session closed, and exit the actor loop cleanly
  2. Check the child exit status when this fires to distinguish normal exit from real errors
  3. Ensure shutdown paths are idempotent so a second poll after close cannot occur
  4. 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

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


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/c9f3e784d5832eaa. Report an issue: GitHub.