Skip to main content

rpi_plugin_sdk/
stdio.rs

1//! Pollable input for an exclusive stdio server. No background stdin reader survives shutdown.
2use std::io;
3
4pub enum InputLine {
5    Pending,
6    Line(String),
7    Eof,
8}
9
10#[derive(Default)]
11pub struct StdinLines {
12    buffer: Vec<u8>,
13    eof: bool,
14    scanned: usize,
15}
16
17impl StdinLines {
18    /// Poll without waiting for input. Callers can wait on their own cancellation signal.
19    pub fn poll_line(&mut self) -> io::Result<InputLine> {
20        if let Some(line) = self.take_line()? {
21            return Ok(InputLine::Line(line));
22        }
23        if self.eof {
24            return Ok(InputLine::Eof);
25        }
26        let mut bytes = [0u8; 4096];
27        // Bound work between cancellation checks while allowing large JSON lines.
28        for _ in 0..16 {
29            let Some(count) = read_available(&mut bytes)? else {
30                return Ok(InputLine::Pending);
31            };
32            if count == 0 {
33                self.eof = true;
34            } else {
35                self.buffer.extend_from_slice(&bytes[..count]);
36            }
37            if let Some(line) = self.take_line()? {
38                return Ok(InputLine::Line(line));
39            }
40            if self.eof {
41                return Ok(InputLine::Eof);
42            }
43        }
44        Ok(InputLine::Pending)
45    }
46
47    fn take_line(&mut self) -> io::Result<Option<String>> {
48        let length = self.buffer[self.scanned..]
49            .iter()
50            .position(|byte| *byte == b'\n')
51            .map(|index| self.scanned + index + 1)
52            .or_else(|| (self.eof && !self.buffer.is_empty()).then_some(self.buffer.len()));
53        let Some(length) = length else {
54            self.scanned = self.buffer.len();
55            return Ok(None);
56        };
57        self.scanned = 0;
58        let mut line: Vec<u8> = self.buffer.drain(..length).collect();
59        if line.last() == Some(&b'\n') {
60            line.pop();
61        }
62        if line.last() == Some(&b'\r') {
63            line.pop();
64        }
65        String::from_utf8(line)
66            .map(Some)
67            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
68    }
69}
70
71#[cfg(unix)]
72fn read_available(bytes: &mut [u8]) -> io::Result<Option<usize>> {
73    read_fd_available(libc::STDIN_FILENO, bytes)
74}
75
76#[cfg(unix)]
77fn read_fd_available(fd: libc::c_int, bytes: &mut [u8]) -> io::Result<Option<usize>> {
78    let mut descriptor = libc::pollfd {
79        fd,
80        events: libc::POLLIN,
81        revents: 0,
82    };
83    // SAFETY: the descriptor and writable byte slice are valid for each syscall.
84    let ready = unsafe { libc::poll(&mut descriptor, 1, 0) };
85    if ready < 0 {
86        return interrupted_or_error();
87    }
88    if ready == 0 {
89        return Ok(None);
90    }
91    if descriptor.revents & libc::POLLNVAL != 0 {
92        return Err(io::Error::from_raw_os_error(libc::EBADF));
93    }
94    // The caller exclusively owns stdin. After poll reports input/EOF, read cannot
95    // race another consumer for those bytes. Each read is bounded to this chunk.
96    let count = unsafe { libc::read(fd, bytes.as_mut_ptr().cast(), bytes.len()) };
97    if count < 0 {
98        interrupted_or_error()
99    } else {
100        Ok(Some(count as usize))
101    }
102}
103
104#[cfg(unix)]
105fn interrupted_or_error() -> io::Result<Option<usize>> {
106    let error = io::Error::last_os_error();
107    if matches!(
108        error.kind(),
109        io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
110    ) {
111        Ok(None)
112    } else {
113        Err(error)
114    }
115}
116
117#[cfg(windows)]
118fn read_available(bytes: &mut [u8]) -> io::Result<Option<usize>> {
119    use windows_sys::Win32::System::Console::{GetStdHandle, STD_INPUT_HANDLE};
120    // SAFETY: GetStdHandle returns a borrowed process handle; never close it.
121    read_handle_available(unsafe { GetStdHandle(STD_INPUT_HANDLE) }, bytes)
122}
123
124#[cfg(windows)]
125fn read_handle_available(
126    input: windows_sys::Win32::Foundation::HANDLE,
127    bytes: &mut [u8],
128) -> io::Result<Option<usize>> {
129    use windows_sys::Win32::Foundation::{
130        GetLastError, ERROR_BROKEN_PIPE, ERROR_HANDLE_EOF, INVALID_HANDLE_VALUE,
131    };
132    use windows_sys::Win32::Storage::FileSystem::{
133        GetFileType, ReadFile, FILE_TYPE_DISK, FILE_TYPE_PIPE,
134    };
135    use windows_sys::Win32::System::Pipes::PeekNamedPipe;
136    if input.is_null() || input == INVALID_HANDLE_VALUE {
137        return Err(io::Error::last_os_error());
138    }
139    let kind = unsafe { GetFileType(input) };
140    let capacity = if kind == FILE_TYPE_PIPE {
141        let mut available = 0;
142        // Peek works on anonymous and named pipes and never consumes input.
143        let ok = unsafe {
144            PeekNamedPipe(
145                input,
146                std::ptr::null_mut(),
147                0,
148                std::ptr::null_mut(),
149                &mut available,
150                std::ptr::null_mut(),
151            )
152        };
153        if ok == 0 {
154            let error = unsafe { GetLastError() };
155            return if matches!(error, ERROR_BROKEN_PIPE | ERROR_HANDLE_EOF) {
156                Ok(Some(0))
157            } else {
158                Err(io::Error::from_raw_os_error(error as i32))
159            };
160        }
161        if available == 0 {
162            return Ok(None);
163        }
164        available.min(bytes.len() as u32)
165    } else if kind == FILE_TYPE_DISK {
166        bytes.len() as u32
167    } else {
168        return Err(io::Error::new(
169            io::ErrorKind::Unsupported,
170            "stdio server requires piped or file stdin",
171        ));
172    };
173    let mut count = 0;
174    // SAFETY: capacity is within the valid buffer and available pipe bytes.
175    let ok = unsafe {
176        ReadFile(
177            input,
178            bytes.as_mut_ptr(),
179            capacity,
180            &mut count,
181            std::ptr::null_mut(),
182        )
183    };
184    if ok == 0 {
185        let error = unsafe { GetLastError() };
186        return if matches!(error, ERROR_BROKEN_PIPE | ERROR_HANDLE_EOF) {
187            Ok(Some(0))
188        } else {
189            Err(io::Error::from_raw_os_error(error as i32))
190        };
191    }
192    Ok(Some(count as usize))
193}
194
195#[cfg(not(any(unix, windows)))]
196fn read_available(_: &mut [u8]) -> io::Result<Option<usize>> {
197    Err(io::Error::new(
198        io::ErrorKind::Unsupported,
199        "stdio polling is unsupported on this platform",
200    ))
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    #[cfg(windows)]
207    #[test]
208    fn pipe_poll_is_idle_then_readable_then_eof_without_a_reader_thread() {
209        use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
210        use windows_sys::Win32::Storage::FileSystem::WriteFile;
211        use windows_sys::Win32::System::Pipes::CreatePipe;
212        let mut input = std::ptr::null_mut();
213        let mut output = std::ptr::null_mut();
214        assert_ne!(
215            unsafe { CreatePipe(&mut input, &mut output, std::ptr::null(), 0) },
216            0
217        );
218        let input = unsafe { OwnedHandle::from_raw_handle(input) };
219        let output = unsafe { OwnedHandle::from_raw_handle(output) };
220        let mut bytes = [0u8; 16];
221        assert_eq!(
222            read_handle_available(input.as_raw_handle(), &mut bytes).unwrap(),
223            None
224        );
225        let mut written = 0;
226        assert_ne!(
227            unsafe {
228                WriteFile(
229                    output.as_raw_handle(),
230                    b"abc".as_ptr(),
231                    3,
232                    &mut written,
233                    std::ptr::null_mut(),
234                )
235            },
236            0
237        );
238        assert_eq!(
239            read_handle_available(input.as_raw_handle(), &mut bytes).unwrap(),
240            Some(3)
241        );
242        assert_eq!(&bytes[..3], b"abc");
243        drop(output);
244        assert_eq!(
245            read_handle_available(input.as_raw_handle(), &mut bytes).unwrap(),
246            Some(0)
247        );
248    }
249
250    #[cfg(unix)]
251    #[test]
252    fn pipe_poll_is_idle_then_readable_then_eof_without_a_reader_thread() {
253        use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
254        let mut descriptors = [0; 2];
255        assert_eq!(unsafe { libc::pipe(descriptors.as_mut_ptr()) }, 0);
256        let input = unsafe { OwnedFd::from_raw_fd(descriptors[0]) };
257        let output = unsafe { OwnedFd::from_raw_fd(descriptors[1]) };
258        let mut bytes = [0u8; 16];
259        assert_eq!(
260            read_fd_available(input.as_raw_fd(), &mut bytes).unwrap(),
261            None
262        );
263        assert_eq!(
264            unsafe { libc::write(output.as_raw_fd(), b"abc".as_ptr().cast(), 3) },
265            3
266        );
267        assert_eq!(
268            read_fd_available(input.as_raw_fd(), &mut bytes).unwrap(),
269            Some(3)
270        );
271        assert_eq!(&bytes[..3], b"abc");
272        drop(output);
273        assert_eq!(
274            read_fd_available(input.as_raw_fd(), &mut bytes).unwrap(),
275            Some(0)
276        );
277    }
278
279    #[test]
280    fn retains_partial_utf8_and_final_line_without_newline() {
281        let mut reader = StdinLines::default();
282        reader
283            .buffer
284            .extend_from_slice(&"中文\r\nnext".as_bytes()[..2]);
285        assert!(reader.take_line().unwrap().is_none());
286        reader
287            .buffer
288            .extend_from_slice(&"中文\r\nnext".as_bytes()[2..]);
289        assert_eq!(reader.take_line().unwrap().as_deref(), Some("中文"));
290        assert!(reader.take_line().unwrap().is_none());
291        reader.eof = true;
292        assert_eq!(reader.take_line().unwrap().as_deref(), Some("next"));
293        assert!(reader.take_line().unwrap().is_none());
294    }
295}