#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum StdioPipes {
#[default]
Auto,
Shared,
Blocking,
}
#[cfg(not(unix))]
pub(super) use blocking_only::open;
#[cfg(unix)]
pub(super) use fd::open;
#[cfg(not(unix))]
mod blocking_only {
use super::StdioPipes;
pub(in super::super) fn open(_mode: StdioPipes) -> (tokio::io::Stdin, tokio::io::Stdout) {
(tokio::io::stdin(), tokio::io::stdout())
}
}
#[cfg(unix)]
mod fd {
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::pin::Pin;
use std::task::{Context, Poll, ready};
use tokio::io::unix::AsyncFd;
use tokio::io::{AsyncRead, AsyncWrite, Interest, ReadBuf};
use super::StdioPipes;
pub(in super::super) enum StdioReader {
Blocking(tokio::io::Stdin),
Fd(FdIo),
}
pub(in super::super) enum StdioWriter {
Blocking(tokio::io::Stdout),
Fd(FdIo),
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Fifo,
Socket,
}
pub(in super::super) fn open(mode: StdioPipes) -> (StdioReader, StdioWriter) {
if mode != StdioPipes::Blocking
&& let Some((reader, writer)) = open_fds(mode)
{
tracing::debug!(?mode, "stdio: using non-blocking pipes");
return (StdioReader::Fd(reader), StdioWriter::Fd(writer));
}
(
StdioReader::Blocking(tokio::io::stdin()),
StdioWriter::Blocking(tokio::io::stdout()),
)
}
fn open_fds(mode: StdioPipes) -> Option<(FdIo, FdIo)> {
let read_kind = kind(libc::STDIN_FILENO)?;
let write_kind = kind(libc::STDOUT_FILENO)?;
let reader = FdIo::open(libc::STDIN_FILENO, read_kind, Interest::READABLE, mode)?;
let writer = FdIo::open(libc::STDOUT_FILENO, write_kind, Interest::WRITABLE, mode)?;
Some((reader, writer))
}
fn kind(fd: RawFd) -> Option<Kind> {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut stat) } != 0 {
return None;
}
match stat.st_mode & libc::S_IFMT {
libc::S_IFIFO => Some(Kind::Fifo),
libc::S_IFSOCK => Some(Kind::Socket),
_ => None,
}
}
pub(in super::super) struct FdIo {
inner: AsyncFd<NonBlockingFd>,
}
struct NonBlockingFd {
fd: OwnedFd,
restore: Option<libc::c_int>,
}
impl AsRawFd for NonBlockingFd {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
impl Drop for NonBlockingFd {
fn drop(&mut self) {
if let Some(flags) = self.restore {
unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_SETFL, flags) };
}
}
}
impl FdIo {
fn open(fd: RawFd, kind: Kind, interest: Interest, mode: StdioPipes) -> Option<Self> {
let nonblocking = match private_reopen(fd, kind, interest) {
Some(fd) => NonBlockingFd { fd, restore: None },
None if mode == StdioPipes::Shared => share(fd)?,
None => return None,
};
let inner = AsyncFd::with_interest(nonblocking, interest).ok()?;
Some(Self { inner })
}
}
#[cfg(target_os = "linux")]
fn private_reopen(fd: RawFd, kind: Kind, interest: Interest) -> Option<OwnedFd> {
use std::os::unix::fs::OpenOptionsExt;
if kind != Kind::Fifo {
return None;
}
let file = std::fs::OpenOptions::new()
.read(interest.is_readable())
.write(interest.is_writable())
.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC)
.open(format!("/proc/self/fd/{fd}"))
.ok()?;
let fd = OwnedFd::from(file);
(self::kind(fd.as_raw_fd()) == Some(Kind::Fifo)).then_some(fd)
}
#[cfg(not(target_os = "linux"))]
fn private_reopen(_fd: RawFd, _kind: Kind, _interest: Interest) -> Option<OwnedFd> {
None
}
fn share(fd: RawFd) -> Option<NonBlockingFd> {
let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
if dup < 0 {
return None;
}
let dup = unsafe { OwnedFd::from_raw_fd(dup) };
let flags = unsafe { libc::fcntl(dup.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return None;
}
if flags & libc::O_NONBLOCK != 0 {
return Some(NonBlockingFd {
fd: dup,
restore: None,
});
}
if unsafe { libc::fcntl(dup.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) } != 0 {
return None;
}
Some(NonBlockingFd {
fd: dup,
restore: Some(flags),
})
}
fn retry_interrupted(mut f: impl FnMut() -> isize) -> io::Result<usize> {
loop {
let n = f();
if n >= 0 {
return Ok(n as usize);
}
let err = io::Error::last_os_error();
if err.kind() != io::ErrorKind::Interrupted {
return Err(err);
}
}
}
impl AsyncRead for StdioReader {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match self.get_mut() {
StdioReader::Blocking(stdin) => Pin::new(stdin).poll_read(cx, buf),
StdioReader::Fd(io) => loop {
let mut guard = ready!(io.inner.poll_read_ready(cx))?;
let unfilled = buf.initialize_unfilled();
let result = guard.try_io(|inner| {
let fd = inner.get_ref().as_raw_fd();
retry_interrupted(|| unsafe {
libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len())
})
});
match result {
Ok(Ok(n)) => {
buf.advance(n);
return Poll::Ready(Ok(()));
}
Ok(Err(e)) => return Poll::Ready(Err(e)),
Err(_would_block) => continue,
}
},
}
}
}
impl AsyncWrite for StdioWriter {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
match self.get_mut() {
StdioWriter::Blocking(stdout) => Pin::new(stdout).poll_write(cx, buf),
StdioWriter::Fd(io) => loop {
let mut guard = ready!(io.inner.poll_write_ready(cx))?;
let result = guard.try_io(|inner| {
let fd = inner.get_ref().as_raw_fd();
retry_interrupted(|| unsafe {
libc::write(fd, buf.as_ptr().cast(), buf.len())
})
});
match result {
Ok(result) => return Poll::Ready(result),
Err(_would_block) => continue,
}
},
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() {
StdioWriter::Blocking(stdout) => Pin::new(stdout).poll_flush(cx),
StdioWriter::Fd(_) => Poll::Ready(Ok(())),
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() {
StdioWriter::Blocking(stdout) => Pin::new(stdout).poll_shutdown(cx),
StdioWriter::Fd(_) => Poll::Ready(Ok(())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kind_classifies_pipes_and_sockets_but_not_other_files() {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let pipe = fds.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
assert!(kind(pipe[0].as_raw_fd()) == Some(Kind::Fifo));
assert_eq!(
unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) },
0
);
let socket = fds.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
assert!(kind(socket[0].as_raw_fd()) == Some(Kind::Socket));
let null = std::fs::File::open("/dev/null").unwrap();
assert!(kind(null.as_raw_fd()).is_none());
assert!(kind(-1).is_none());
}
fn flags(fd: RawFd) -> libc::c_int {
unsafe { libc::fcntl(fd, libc::F_GETFL) }
}
#[test]
fn shared_sets_nonblocking_and_restores_it_on_drop() {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let [read, write] = fds.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
assert_eq!(flags(read.as_raw_fd()) & libc::O_NONBLOCK, 0);
let shared = share(read.as_raw_fd()).unwrap();
assert_ne!(flags(read.as_raw_fd()) & libc::O_NONBLOCK, 0);
drop(shared);
assert_eq!(flags(read.as_raw_fd()) & libc::O_NONBLOCK, 0);
drop(write);
}
#[cfg(target_os = "linux")]
#[test]
fn private_reopen_leaves_the_original_description_alone() {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let [read, write] = fds.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
let reopened =
private_reopen(read.as_raw_fd(), Kind::Fifo, Interest::READABLE).unwrap();
assert_ne!(flags(reopened.as_raw_fd()) & libc::O_NONBLOCK, 0);
assert_eq!(flags(read.as_raw_fd()) & libc::O_NONBLOCK, 0);
drop(write);
}
#[cfg(target_os = "linux")]
#[test]
fn private_reopen_refuses_sockets() {
let mut fds = [0; 2];
assert_eq!(
unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) },
0
);
let [a, _b] = fds.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
assert!(private_reopen(a.as_raw_fd(), Kind::Socket, Interest::READABLE).is_none());
}
}
}