Skip to main content

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