{"record":{"id":"c9f3e784d5832eaa","repo":"herdrdev/herdr","slug":"poll-encountered-pty-fd-error","errorCode":null,"errorMessage":"poll encountered PTY fd error","messagePattern":"poll encountered PTY fd error","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/pty/fd.rs","lineNumber":205,"sourceCode":"                if remaining.is_zero() {\n                    return Ok(PtyWakeReadiness::default());\n                }\n                remaining_timeout_ms = remaining.as_millis().clamp(1, i32::MAX as u128) as i32;\n                continue;\n            }\n            return Err(err);\n        }\n\n        let pty_revents = if poll_pty { poll_fds[0].revents } else { 0 };\n        let wake_revents = poll_fds[1].revents;\n        if (pty_revents | wake_revents) & libc::POLLNVAL != 0 {\n            return Err(std::io::Error::new(\n                std::io::ErrorKind::BrokenPipe,\n                \"poll encountered invalid PTY actor fd\",\n            ));\n        }\n        if pty_revents & libc::POLLERR != 0 {\n            return Err(std::io::Error::new(\n                std::io::ErrorKind::BrokenPipe,\n                \"poll encountered PTY fd error\",\n            ));\n        }\n\n        return Ok(PtyWakeReadiness {\n            pty_read_ready: pty_revents & (libc::POLLIN | libc::POLLHUP) != 0,\n            pty_write_ready: pty_revents & (libc::POLLOUT | libc::POLLHUP) != 0,\n            wake_ready: wake_revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0,\n        });\n    }\n}\n\n#[cfg(unix)]\npub(crate) fn resize_pty_fd(\n    fd: RawFd,\n    rows: u16,\n    cols: u16,","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/herdrdev/herdr/blob/f457cff4f2648eee85d176f8a41861241d4e8428/src/pty/fd.rs#L187-L223","documentation":"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.","triggerScenarios":"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.","commonSituations":"Child process exits while the actor loop is polling; ssh/network-backed PTY dropping; hardware/tty error states; races during session close.","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"],"exampleFix":"// before\nmatch fd::poll_pty_and_wake(pty, wake, ms) {\n    Ok(r) => handle(r),\n    Err(e) => return Err(e),\n}\n\n// after\nmatch fd::poll_pty_and_wake(pty, wake, ms) {\n    Ok(r) => handle(r),\n    Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {\n        self.reap_child_and_finish(); // normal PTY termination path\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"try-catch","validationCode":"if !actor.is_pty_open() || actor.child_finished() { skip poll and finalize; }","typeGuard":null,"tryCatchPattern":"match fd::poll_pty_and_wake(pty, wake, ms) {\n    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { reap_child_and_close_session(); }\n    other => other?,\n}","preventionTips":["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"],"tags":["pty","unix","poll","pollerr","eof"],"backgroundTag":"poll-error-hangup","analyzedSha":"f457cff4f2648eee85d176f8a41861241d4e8428","analyzedAt":"2026-08-28T15:41:09.197Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}