shrub_core/sys/unix/poll.rs
1//! File descriptor polling utilities for timeout support.
2
3use std::os::fd::BorrowedFd;
4use std::time::{Duration, Instant};
5
6use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
7
8use crate::openfiles::OpenFile;
9
10/// Polls an open file for input readability with a timeout.
11///
12/// Returns `Ok(true)` if data is available for reading, `Ok(false)` if the
13/// timeout elapsed without data becoming available.
14///
15/// For regular files, always returns `Ok(true)` immediately since they're
16/// always "ready" (matching bash behavior where `-t` has no effect on regular
17/// files).
18///
19/// # Arguments
20///
21/// * `file` - The open file to poll.
22/// * `timeout` - Maximum time to wait. Use `Duration::ZERO` to check without
23/// blocking.
24///
25/// # Errors
26///
27/// Returns an error if polling fails or the file descriptor cannot be borrowed.
28pub fn poll_for_input(file: &OpenFile, timeout: Duration) -> std::io::Result<bool> {
29 let fd = file
30 .try_borrow_as_fd()
31 .map_err(|e| std::io::Error::other(e.to_string()))?;
32
33 // Regular files are always ready - timeout has no effect (bash behavior).
34 if is_regular_file(fd) {
35 return Ok(true);
36 }
37
38 // Convert timeout to deadline for accurate time tracking across EINTR
39 // retries.
40 let deadline = if timeout.is_zero() {
41 // For zero timeout, use current instant so first check sees zero
42 // remaining.
43 Some(Instant::now())
44 } else {
45 Some(Instant::now() + timeout)
46 };
47
48 poll_fd_for_input(fd, deadline)
49}
50
51/// Polls a file descriptor for input readability with a deadline.
52///
53/// Returns `Ok(true)` if data is available, `Ok(false)` if deadline passed.
54///
55/// # Arguments
56///
57/// * `fd` - File descriptor to poll
58/// * `deadline` - Optional deadline; `None` indicates no deadline.
59fn poll_fd_for_input(fd: BorrowedFd<'_>, deadline: Option<Instant>) -> std::io::Result<bool> {
60 let mut poll_fds = [PollFd::new(fd, PollFlags::POLLIN)];
61 let mut first_iteration = true;
62
63 loop {
64 // Calculate remaining time on each iteration to handle EINTR correctly.
65 let timeout_ms = match deadline {
66 Some(d) => {
67 let remaining = d.saturating_duration_since(Instant::now());
68 // On first iteration, always do at least one poll even with
69 // zero timeout. This allows `-t 0` to check if
70 // input is immediately available.
71 if remaining.is_zero() && !first_iteration {
72 return Ok(false); // Deadline passed after initial poll.
73 }
74 i32::try_from(remaining.as_millis()).unwrap_or(i32::MAX)
75 }
76 None => -1, // Block indefinitely.
77 };
78 first_iteration = false;
79 let poll_timeout = PollTimeout::try_from(timeout_ms).unwrap_or(PollTimeout::MAX);
80
81 match poll(&mut poll_fds, poll_timeout) {
82 Ok(0) => return Ok(false), // Timeout
83 Ok(_) => {
84 let revents = poll_fds[0].revents().unwrap_or(PollFlags::empty());
85 // POLLIN means data available. POLLHUP/POLLERR without POLLIN
86 // means EOF/error - return true so caller reads
87 // and gets the proper result.
88 return Ok(
89 revents.intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR)
90 );
91 }
92 Err(nix::errno::Errno::EINTR) => (), // Retry on signal with recalculated timeout.
93 Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)),
94 }
95 }
96}
97
98/// Checks if a file descriptor refers to a regular file.
99///
100/// Regular files are always "ready" for reading (poll has no effect).
101///
102/// # Arguments
103///
104/// * `fd` - File descriptor to check
105fn is_regular_file(fd: BorrowedFd<'_>) -> bool {
106 match nix::sys::stat::fstat(fd) {
107 Ok(stat) => {
108 use nix::sys::stat::{SFlag, mode_t};
109 mode_t::try_from(stat.st_mode)
110 .is_ok_and(|mode| SFlag::from_bits_truncate(mode).contains(SFlag::S_IFREG))
111 }
112 Err(_) => false,
113 }
114}