use crate::CoreError;
use crate::fd::{Fd, Token};
use crate::io::buffer::{BufferState, ChunkSink, ReadState};
use crate::io::writer::WriterState;
const WRITE_INPUT_POLL_TIMEOUT_MS: i32 = 2_000;
#[inline(always)]
fn errno() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}
pub(crate) struct FdSlot {
pub token: Option<Token>,
pub fd: Fd,
pub readable: bool,
pub writable: bool,
}
#[repr(align(64))]
pub struct DrainState<F>
where
F: FnMut(&[u8]) -> bool,
{
pub(crate) stdout_slot: Option<FdSlot>,
pub(crate) stderr_slot: Option<FdSlot>,
pub(crate) stdin_slot: Option<FdSlot>,
pub(crate) buffer: BufferState,
pub(crate) writer: WriterState,
pub(crate) early_exit: Option<F>,
pub(crate) pty_master: bool,
}
impl<F> DrainState<F>
where
F: FnMut(&[u8]) -> bool,
{
#[allow(clippy::too_many_arguments)] pub fn new(
stdin_fd: Option<Fd>,
stdin_buf: Option<Box<[u8]>>,
stdout_fd: Option<Fd>,
stderr_fd: Option<Fd>,
limit: usize,
early_exit: Option<F>,
chunk_sink: Option<ChunkSink>,
pty_master: bool,
) -> Result<Self, CoreError> {
let stdin_slot = if stdin_buf.is_some() {
if let Some(fd) = stdin_fd {
fd.set_nonblock()?;
Some(FdSlot {
token: None,
fd,
readable: false,
writable: false,
})
} else {
None
}
} else {
None
};
let stdout_slot = if let Some(fd) = stdout_fd {
fd.set_nonblock()?;
Some(FdSlot {
token: None,
fd,
readable: false,
writable: false,
})
} else {
None
};
let stderr_slot = if let Some(fd) = stderr_fd {
fd.set_nonblock()?;
Some(FdSlot {
token: None,
fd,
readable: false,
writable: false,
})
} else {
None
};
Ok(Self {
stdin_slot,
stdout_slot,
stderr_slot,
buffer: BufferState::new(limit, chunk_sink),
writer: WriterState::new(stdin_buf),
early_exit,
pty_master,
})
}
#[inline(always)]
pub fn is_done(&self) -> bool {
self.stdin_slot.is_none() && self.stdout_slot.is_none() && self.stderr_slot.is_none()
}
pub(crate) fn resize_pty(&self, rows: u16, cols: u16) -> Result<(), CoreError> {
if rows == 0 || cols == 0 {
return Err(CoreError::sys(
libc::EINVAL,
"resize_pty: rows and cols must be non-zero",
));
}
let Some(slot) = &self.stdout_slot else {
return Err(CoreError::sys(libc::EINVAL, "resize_pty: no pty master"));
};
let ws = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
let r = unsafe { libc::ioctl(slot.fd.raw(), libc::TIOCSWINSZ as libc::Ioctl, &ws) };
crate::error::syscall_ret(r, "TIOCSWINSZ")
}
pub(crate) fn write_input(&self, bytes: &[u8]) -> Result<usize, CoreError> {
if !self.pty_master {
return Err(CoreError::sys(libc::EINVAL, "write_input: not a pty spawn"));
}
let Some(slot) = &self.stdout_slot else {
return Err(CoreError::sys(
libc::EINVAL,
"write_input: pty master closed",
));
};
let fd = slot.fd.raw();
let mut written = 0usize;
while written < bytes.len() {
let n = unsafe {
libc::write(
fd,
bytes[written..].as_ptr() as *const libc::c_void,
bytes.len() - written,
)
};
if n < 0 {
let e = errno();
if e == libc::EINTR {
continue;
}
if e == libc::EAGAIN {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLOUT,
revents: 0,
};
let rc = unsafe { libc::poll(&mut pfd, 1, WRITE_INPUT_POLL_TIMEOUT_MS) };
if rc < 0 {
let pe = errno();
if pe == libc::EINTR {
continue;
}
return Err(CoreError::sys(pe, "write_input:poll"));
}
if rc == 0 {
return Err(CoreError::sys(
libc::ETIMEDOUT,
"write_input: tty input buffer stayed full",
));
}
continue;
}
return Err(CoreError::sys(e, "write_input"));
}
written += n as usize;
}
Ok(written)
}
pub(crate) fn write_input_nonblock(&self, bytes: &[u8]) -> Result<Option<usize>, CoreError> {
if !self.pty_master {
return Err(CoreError::sys(
libc::EINVAL,
"write_input_nonblock: not a pty spawn",
));
}
let Some(slot) = &self.stdout_slot else {
return Err(CoreError::sys(
libc::EINVAL,
"write_input_nonblock: pty master closed",
));
};
let fd = slot.fd.raw();
let mut written = 0usize;
while written < bytes.len() {
let n = unsafe {
libc::write(
fd,
bytes[written..].as_ptr() as *const libc::c_void,
bytes.len() - written,
)
};
if n < 0 {
let e = errno();
if e == libc::EINTR {
continue;
}
if e == libc::EAGAIN {
return Ok(if written == 0 { None } else { Some(written) });
}
return Err(CoreError::sys(e, "write_input_nonblock"));
}
written += n as usize;
}
Ok(Some(written))
}
#[inline(always)]
pub fn write_stdin(&mut self) -> Result<bool, CoreError> {
let fd = if let Some(s) = &self.stdin_slot {
&s.fd
} else {
return Ok(true);
};
let done = self.writer.write_to_fd(fd)?;
if done {
self.stdin_slot.take();
return Ok(true);
}
Ok(false)
}
#[inline(always)]
fn read_from_slot(
buffer: &mut BufferState,
fd: &Fd,
is_stdout: bool,
early_exit: &mut Option<F>,
pty_master: bool,
) -> Result<ReadState, CoreError> {
match buffer.read_from_fd(fd, is_stdout, early_exit) {
Err(e) if pty_master && is_stdout && e.raw_os_error() == Some(libc::EIO) => {
Ok(ReadState::Eof)
}
other => other,
}
}
#[inline(always)]
pub fn read_fd(&mut self, is_stdout: bool) -> Result<bool, CoreError> {
let pty_master = self.pty_master;
let read_state = {
let slot = if is_stdout {
&self.stdout_slot
} else {
&self.stderr_slot
};
let fd = if let Some(s) = slot {
&s.fd
} else {
return Ok(true);
};
Self::read_from_slot(
&mut self.buffer,
fd,
is_stdout,
&mut self.early_exit,
pty_master,
)?
};
match read_state {
ReadState::Open => Ok(false),
ReadState::Paused => Ok(false),
ReadState::Eof | ReadState::EarlyExit => {
if is_stdout {
self.stdout_slot.take();
} else {
self.stderr_slot.take();
}
Ok(true)
}
}
}
pub(crate) fn take_all_slots(&mut self) -> Vec<FdSlot> {
let mut slots = Vec::new();
if let Some(slot) = self.stdin_slot.take() {
slots.push(slot);
}
if let Some(slot) = self.stdout_slot.take() {
slots.push(slot);
}
if let Some(slot) = self.stderr_slot.take() {
slots.push(slot);
}
slots
}
pub(crate) fn register_with_reactor(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
register_slot(reactor, &mut self.stdin_slot, false, true)?;
register_slot(reactor, &mut self.stdout_slot, true, false)?;
register_slot(reactor, &mut self.stderr_slot, true, false)?;
Ok(())
}
pub(crate) fn stdout_matches(&self, token: Token) -> bool {
self.stdout_slot
.as_ref()
.is_some_and(|slot| slot.token == Some(token))
}
pub(crate) fn stderr_matches(&self, token: Token) -> bool {
self.stderr_slot
.as_ref()
.is_some_and(|slot| slot.token == Some(token))
}
pub(crate) fn stdin_matches(&self, token: Token) -> bool {
self.stdin_slot
.as_ref()
.is_some_and(|slot| slot.token == Some(token))
}
pub(crate) fn drop_stdout(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
if let Some(slot) = self.stdout_slot.take() {
del_slot(reactor, &slot)?;
}
Ok(())
}
pub(crate) fn drop_stderr(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
if let Some(slot) = self.stderr_slot.take() {
del_slot(reactor, &slot)?;
}
Ok(())
}
pub(crate) fn drop_stdin(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
if let Some(slot) = self.stdin_slot.take() {
del_slot(reactor, &slot)?;
}
self.writer.buf = None;
Ok(())
}
pub(crate) fn handle_stdout_ready(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
if let Some(slot) = &self.stdout_slot {
let read_state = Self::read_from_slot(
&mut self.buffer,
&slot.fd,
true,
&mut self.early_exit,
self.pty_master,
)?;
match read_state {
ReadState::Open => {}
ReadState::Paused => {
self.pause_stdout(reactor)?;
}
ReadState::Eof | ReadState::EarlyExit => {
self.drop_stdout(reactor)?;
}
}
}
Ok(())
}
pub(crate) fn handle_stderr_ready(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
if let Some(slot) = &self.stderr_slot {
let read_state = Self::read_from_slot(
&mut self.buffer,
&slot.fd,
false,
&mut self.early_exit,
self.pty_master,
)?;
match read_state {
ReadState::Open => {}
ReadState::Paused => {
self.pause_stderr(reactor)?;
}
ReadState::Eof | ReadState::EarlyExit => {
self.drop_stderr(reactor)?;
}
}
}
Ok(())
}
pub(crate) fn pause_stdout(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
pause_slot(reactor, &mut self.stdout_slot)
}
pub(crate) fn pause_stderr(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
pause_slot(reactor, &mut self.stderr_slot)
}
pub fn stdout_paused(&self) -> bool {
self.buffer.stdout_paused()
}
pub fn stderr_paused(&self) -> bool {
self.buffer.stderr_paused()
}
pub fn resume_stdout(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<bool, CoreError> {
if !self.buffer.deliver_pending_stdout()? {
return Ok(false);
}
register_slot(reactor, &mut self.stdout_slot, true, false)?;
Ok(true)
}
pub fn resume_stderr(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<bool, CoreError> {
if !self.buffer.deliver_pending_stderr()? {
return Ok(false);
}
register_slot(reactor, &mut self.stderr_slot, true, false)?;
Ok(true)
}
pub fn set_pty_writable(
&mut self,
reactor: &mut crate::reactor::Reactor,
writable: bool,
) -> Result<(), CoreError> {
if !self.pty_master {
return Err(CoreError::sys(libc::EINVAL, "set_pty_writable: not a pty"));
}
register_slot(reactor, &mut self.stdout_slot, true, writable)
}
pub fn pty_input_token(&self) -> Option<Token> {
if !self.pty_master {
return None;
}
self.stdout_slot.as_ref().and_then(|s| s.token)
}
pub(crate) fn take_stdout_pending(&mut self) -> Option<Vec<u8>> {
self.buffer.take_stdout_pending()
}
pub(crate) fn take_stderr_pending(&mut self) -> Option<Vec<u8>> {
self.buffer.take_stderr_pending()
}
pub(crate) fn handle_stdin_writable(
&mut self,
reactor: &mut crate::reactor::Reactor,
) -> Result<(), CoreError> {
if let Some(slot) = &self.stdin_slot {
let done = self.writer.write_to_fd(&slot.fd)?;
if done {
self.drop_stdin(reactor)?;
}
}
Ok(())
}
pub fn into_parts(mut self) -> (Vec<u8>, Vec<u8>) {
let (stdout, stderr, _, _) = std::mem::take(&mut self.buffer).into_parts();
(stdout, stderr)
}
#[inline(always)]
pub fn output_limit_exceeded(&self) -> bool {
self.buffer.output_limit_exceeded()
}
#[inline(always)]
pub fn stdout_early_exited(&self) -> bool {
self.buffer.stdout_early_exited()
}
pub(crate) fn into_parts_with_state(mut self) -> (Vec<u8>, Vec<u8>, bool, bool) {
std::mem::take(&mut self.buffer).into_parts()
}
}
fn register_slot(
reactor: &mut crate::reactor::Reactor,
slot: &mut Option<FdSlot>,
readable: bool,
writable: bool,
) -> Result<(), CoreError> {
let Some(s) = slot.as_mut() else {
return Ok(());
};
if let Some(token) = s.token {
if s.readable == readable && s.writable == writable {
return Ok(());
}
reactor.mod_(&s.fd, token, readable, writable)?;
s.readable = readable;
s.writable = writable;
return Ok(());
}
s.token = Some(reactor.add(&s.fd, readable, writable)?);
s.readable = readable;
s.writable = writable;
Ok(())
}
fn del_slot(reactor: &crate::reactor::Reactor, slot: &FdSlot) -> Result<(), CoreError> {
if slot.token.is_none() {
return Ok(());
}
match reactor.del(&slot.fd) {
Ok(()) => Ok(()),
Err(e) if e.raw_os_error() == Some(libc::ENOENT) => Ok(()),
Err(e) => Err(e),
}
}
fn pause_slot(
reactor: &mut crate::reactor::Reactor,
slot: &mut Option<FdSlot>,
) -> Result<(), CoreError> {
let Some(s) = slot.as_mut() else {
return Ok(());
};
let Some(token) = s.token else {
return Ok(());
};
if !s.readable {
return Ok(());
}
match reactor.mod_(&s.fd, token, false, s.writable) {
Ok(()) => {
s.readable = false;
Ok(())
}
Err(e) if e.raw_os_error() == Some(libc::ENOENT) => {
s.token = None;
s.readable = false;
Ok(())
}
Err(e) => Err(e),
}
}