serialport/posix/
poll.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#![allow(non_camel_case_types, dead_code)]

use std::io;
use std::os::unix::io::RawFd;
use std::slice;
use std::time::Duration;

use nix::libc::c_int;
use nix::poll::{PollFd, PollFlags};
#[cfg(target_os = "linux")]
use nix::sys::signal::SigSet;
#[cfg(any(target_os = "linux", test))]
use nix::sys::time::TimeSpec;

pub fn wait_read_fd(fd: RawFd, timeout: Duration) -> io::Result<()> {
    wait_fd(fd, PollFlags::POLLIN, timeout)
}

pub fn wait_write_fd(fd: RawFd, timeout: Duration) -> io::Result<()> {
    wait_fd(fd, PollFlags::POLLOUT, timeout)
}

fn wait_fd(fd: RawFd, events: PollFlags, timeout: Duration) -> io::Result<()> {
    use nix::errno::Errno::{EIO, EPIPE};

    let mut fd = PollFd::new(fd, events);

    let wait = match poll_clamped(&mut fd, timeout) {
        Ok(r) => r,
        Err(e) => return Err(io::Error::from(crate::Error::from(e))),
    };
    // All errors generated by poll or ppoll are already caught by the nix wrapper around libc, so
    // here we only need to check if there's at least 1 event
    if wait != 1 {
        return Err(io::Error::new(
            io::ErrorKind::TimedOut,
            "Operation timed out",
        ));
    }

    // Check the result of ppoll() by looking at the revents field
    match fd.revents() {
        Some(e) if e == events => return Ok(()),
        // If there was a hangout or invalid request
        Some(e) if e.contains(PollFlags::POLLHUP) || e.contains(PollFlags::POLLNVAL) => {
            return Err(io::Error::new(io::ErrorKind::BrokenPipe, EPIPE.desc()));
        }
        Some(_) | None => (),
    }

    Err(io::Error::new(io::ErrorKind::Other, EIO.desc()))
}

/// Poll with a duration clamped to the maximum value representable by the `TimeSpec` used by
/// `ppoll`.
#[cfg(target_os = "linux")]
fn poll_clamped(fd: &mut PollFd, timeout: Duration) -> nix::Result<c_int> {
    let spec = clamped_time_spec(timeout);
    nix::poll::ppoll(slice::from_mut(fd), Some(spec), Some(SigSet::empty()))
}

#[cfg(any(target_os = "linux", test))]
// The type time_t is deprecaten on musl. The nix crate internally uses this type and makes an
// exeption for the deprecation for musl. And so do we.
//
// See https://github.com/rust-lang/libc/issues/1848 which is referenced from every exemption used
// in nix.
#[cfg_attr(target_env = "musl", allow(deprecated))]
fn clamped_time_spec(duration: Duration) -> TimeSpec {
    use nix::libc::c_long;
    use nix::sys::time::time_t;

    // We need to clamp manually as TimeSpec::from_duration translates durations with more than
    // i64::MAX seconds to negative timespans. This happens due to casting to i64 and is still the
    // case as of nix 0.29.
    let secs_limit = time_t::MAX as u64;
    let secs = duration.as_secs();
    if secs <= secs_limit {
        TimeSpec::new(secs as time_t, duration.subsec_nanos() as c_long)
    } else {
        TimeSpec::new(time_t::MAX, 999_999_999)
    }
}

// Poll with a duration clamped to the maximum millisecond value representable by the `c_int` used
// by `poll`.
#[cfg(not(target_os = "linux"))]
fn poll_clamped(fd: &mut PollFd, timeout: Duration) -> nix::Result<c_int> {
    let millis = clamped_millis_c_int(timeout);
    nix::poll::poll(slice::from_mut(fd), millis)
}

#[cfg(any(not(target_os = "linux"), test))]
fn clamped_millis_c_int(duration: Duration) -> c_int {
    let secs_limit = (c_int::MAX as u64) / 1000;
    let secs = duration.as_secs();

    if secs <= secs_limit {
        secs as c_int * 1000 + duration.subsec_millis() as c_int
    } else {
        c_int::MAX
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tests::timeout::MONOTONIC_DURATIONS;

    #[test]
    fn clamped_millis_c_int_is_monotonic() {
        let mut last = clamped_millis_c_int(Duration::ZERO);

        for (i, d) in MONOTONIC_DURATIONS.iter().enumerate() {
            let next = clamped_millis_c_int(*d);
            assert!(
                next >= last,
                "{next} >= {last} failed for {d:?} at index {i}"
            );
            last = next;
        }
    }

    #[test]
    fn clamped_millis_c_int_zero_is_zero() {
        assert_eq!(0, clamped_millis_c_int(Duration::ZERO));
    }

    #[test]
    fn clamped_time_spec_is_monotonic() {
        let mut last = clamped_time_spec(Duration::ZERO);

        for (i, d) in MONOTONIC_DURATIONS.iter().enumerate() {
            let next = clamped_time_spec(*d);
            assert!(
                next >= last,
                "{next} >= {last} failed for {d:?} at index {i}"
            );
            last = next;
        }
    }

    #[test]
    fn clamped_time_spec_zero_is_zero() {
        let spec = clamped_time_spec(Duration::ZERO);
        assert_eq!(0, spec.tv_sec());
        assert_eq!(0, spec.tv_nsec());
    }
}