pub mod exec;
pub mod ssh;
pub mod tree;
use std::collections::{HashMap, HashSet};
use std::os::unix::io::RawFd;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamWriteOutcome {
Flushed,
Pending,
BrokenPipe,
Closed,
Unknown,
}
#[derive(Debug)]
struct WriteStream {
fd: RawFd,
wbuf: Vec<u8>,
eof: bool,
}
#[derive(Debug, Default)]
pub struct StreamWriters {
streams: HashMap<String, WriteStream>,
}
impl StreamWriters {
pub fn new() -> Self {
Self::default()
}
pub fn set_writer(&mut self, sname: &str, fd: RawFd) {
self.streams.insert(
sname.to_string(),
WriteStream {
fd,
wbuf: Vec::new(),
eof: false,
},
);
}
pub fn remove(&mut self, sname: &str) -> bool {
self.streams.remove(sname).is_some()
}
pub fn contains(&self, sname: &str) -> bool {
self.streams.contains_key(sname)
}
pub fn len(&self) -> usize {
self.streams.len()
}
pub fn is_empty(&self) -> bool {
self.streams.is_empty()
}
pub fn write(&mut self, sname: &str, buf: &[u8]) {
if let Some(stream) = self.streams.get_mut(sname) {
stream.wbuf.extend_from_slice(buf);
}
}
pub fn set_write_eof(&mut self, sname: &str) {
if let Some(stream) = self.streams.get_mut(sname) {
stream.eof = true;
}
}
pub fn buffered_len(&self, sname: &str) -> usize {
self.streams.get(sname).map_or(0, |s| s.wbuf.len())
}
pub fn pending_fds(&self) -> Vec<RawFd> {
self.streams
.values()
.filter(|s| !s.wbuf.is_empty() || s.eof)
.map(|s| s.fd)
.collect()
}
pub fn handle_write(&mut self, sname: &str) -> std::io::Result<StreamWriteOutcome> {
let Some(stream) = self.streams.get_mut(sname) else {
return Ok(StreamWriteOutcome::Unknown);
};
if stream.wbuf.is_empty() {
if stream.eof {
self.streams.remove(sname);
return Ok(StreamWriteOutcome::Closed);
}
return Ok(StreamWriteOutcome::Flushed);
}
let fd = stream.fd;
let written = unsafe {
libc::write(
fd,
stream.wbuf.as_ptr() as *const libc::c_void,
stream.wbuf.len(),
)
};
if written < 0 {
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::EAGAIN) => {
return Ok(StreamWriteOutcome::Pending);
}
Some(libc::EPIPE) => {
self.streams.remove(sname);
return Ok(StreamWriteOutcome::BrokenPipe);
}
_ => return Err(err),
}
}
let wcnt = written as usize;
if wcnt == 0 {
return Ok(StreamWriteOutcome::Pending);
}
let stream = self
.streams
.get_mut(sname)
.expect("stream removed during write");
stream.wbuf.drain(..wcnt);
if stream.wbuf.is_empty() && stream.eof {
self.streams.remove(sname);
return Ok(StreamWriteOutcome::Closed);
}
if stream.wbuf.is_empty() {
Ok(StreamWriteOutcome::Flushed)
} else {
Ok(StreamWriteOutcome::Pending)
}
}
}
#[derive(Debug, Error)]
pub enum WorkerError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("worker error: {0}")]
General(String),
#[error("timeout")]
Timeout,
}
pub type Result<T> = std::result::Result<T, WorkerError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkerState {
Pending,
Running,
Done,
Aborted,
}
pub trait EventHandler: Send {
fn on_start(&mut self, worker: &dyn Worker) {
let _ = worker;
}
fn on_read(&mut self, node: &str, fd: RawFd, msg: &[u8]) {
let _ = (node, fd, msg);
}
fn on_close(&mut self, node: &str, rc: i32) {
let _ = (node, rc);
}
fn on_timeout(&mut self, node: &str) {
let _ = node;
}
fn on_error(&mut self, node: &str, error: &WorkerError) {
let _ = (node, error);
}
fn take_buffers(
&mut self,
) -> (
HashMap<String, Vec<Vec<u8>>>,
HashMap<String, Vec<Vec<u8>>>,
HashSet<String>,
) {
(HashMap::new(), HashMap::new(), HashSet::new())
}
}
pub trait Worker: Send {
fn start(&mut self) -> Result<()>;
fn abort(&mut self, kill: bool);
fn state(&self) -> WorkerState;
fn set_handler(&mut self, handler: Box<dyn EventHandler>);
fn read_fds(&self) -> Vec<RawFd>;
fn write_fds(&self) -> Vec<RawFd>;
fn handle_read(&mut self, fd: RawFd) -> Result<()>;
fn handle_write(&mut self, fd: RawFd) -> Result<()>;
fn is_done(&self) -> bool;
fn retcodes(&self) -> &HashMap<String, i32>;
fn num_nodes(&self) -> usize;
fn take_handler(&mut self) -> Option<Box<dyn EventHandler>>;
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_worker_state_variants() {
assert_eq!(WorkerState::Pending, WorkerState::Pending);
assert_eq!(WorkerState::Running, WorkerState::Running);
assert_eq!(WorkerState::Done, WorkerState::Done);
assert_eq!(WorkerState::Aborted, WorkerState::Aborted);
}
#[test]
fn test_worker_state_debug() {
assert_eq!(format!("{:?}", WorkerState::Pending), "Pending");
assert_eq!(format!("{:?}", WorkerState::Running), "Running");
assert_eq!(format!("{:?}", WorkerState::Done), "Done");
assert_eq!(format!("{:?}", WorkerState::Aborted), "Aborted");
}
#[test]
fn test_worker_state_clone() {
let state = WorkerState::Pending;
let cloned = state.clone();
assert_eq!(state, cloned);
}
#[test]
fn test_worker_state_copy() {
fn take_copy<T: Copy>(_: T) {}
take_copy(WorkerState::Pending);
}
#[test]
fn test_worker_state_partial_eq() {
assert!(WorkerState::Pending == WorkerState::Pending);
assert!(WorkerState::Running != WorkerState::Pending);
}
#[test]
fn test_worker_error_variants() {
let io_err = WorkerError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"not found",
));
let gen_err = WorkerError::General("something went wrong".to_string());
let timeout_err = WorkerError::Timeout;
matches!(io_err, WorkerError::Io(_));
matches!(gen_err, WorkerError::General(_));
matches!(timeout_err, WorkerError::Timeout);
}
#[test]
fn test_worker_error_impls_error() {
use std::error::Error;
let gen_err = WorkerError::General("test".to_string());
assert!(gen_err.source().is_none());
let timeout_err = WorkerError::Timeout;
assert!(timeout_err.source().is_none());
}
#[test]
fn test_worker_error_impls_display() {
let io_err = WorkerError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file.txt",
));
assert_eq!(io_err.to_string(), "io error: file.txt");
let gen_err = WorkerError::General("test error".to_string());
assert_eq!(gen_err.to_string(), "worker error: test error");
let timeout_err = WorkerError::Timeout;
assert_eq!(timeout_err.to_string(), "timeout");
}
#[test]
fn test_event_handler_default_methods() {
struct TestHandler;
impl EventHandler for TestHandler {}
let handler = TestHandler;
let _ = handler;
}
#[test]
fn test_event_handler_object_safe() {
struct TestHandler;
impl EventHandler for TestHandler {}
let _handler: Box<dyn EventHandler> = Box::new(TestHandler);
}
#[test]
fn test_event_handler_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Box<dyn EventHandler>>();
}
#[test]
fn test_result_type_alias() {
let ok: Result<()> = Ok(());
let err: Result<()> = Err(WorkerError::Timeout);
assert!(ok.is_ok());
assert!(err.is_err());
}
fn make_pipe() -> (RawFd, RawFd) {
let mut fds = [0; 2];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
assert_eq!(rc, 0, "pipe() failed");
for fd in fds {
unsafe {
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
}
}
(fds[0], fds[1])
}
fn read_fd(fd: RawFd, buf: &mut [u8]) -> usize {
unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) as usize }
}
fn close_fd(fd: RawFd) {
unsafe {
libc::close(fd);
}
}
#[test]
fn test_stream_writer_broken_pipe_on_write_twice() {
let (rfd, wfd) = make_pipe();
let mut writers = StreamWriters::new();
writers.set_writer("test", wfd);
writers.write("test", b"initial");
assert_eq!(
writers.handle_write("test").unwrap(),
StreamWriteOutcome::Flushed
);
let mut buf = [0u8; 16];
let n = read_fd(rfd, &mut buf);
assert_eq!(&buf[..n], b"initial");
close_fd(rfd);
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
writers.write("test", b"final");
match writers.handle_write("test").unwrap() {
StreamWriteOutcome::BrokenPipe => break,
_ => {
assert!(
std::time::Instant::now() < deadline,
"pipe never reported EPIPE"
);
std::thread::sleep(Duration::from_millis(5));
}
}
}
assert!(!writers.contains("test"));
assert!(writers.is_empty());
writers.write("test", b"more");
assert_eq!(writers.buffered_len("test"), 0);
writers.set_write_eof("test");
assert_eq!(
writers.handle_write("test").unwrap(),
StreamWriteOutcome::Unknown
);
close_fd(wfd);
}
#[test]
fn test_stream_writer_eof_closes_after_drain() {
let (rfd, wfd) = make_pipe();
let mut writers = StreamWriters::new();
writers.set_writer("stdin", wfd);
writers.write("stdin", b"payload");
writers.set_write_eof("stdin");
assert_eq!(
writers.handle_write("stdin").unwrap(),
StreamWriteOutcome::Closed
);
assert!(!writers.contains("stdin"));
let mut buf = [0u8; 16];
let n = read_fd(rfd, &mut buf);
assert_eq!(&buf[..n], b"payload");
close_fd(rfd);
close_fd(wfd);
}
#[test]
fn test_stream_writer_eof_empty_buffer_closes() {
let (rfd, wfd) = make_pipe();
let mut writers = StreamWriters::new();
writers.set_writer("test", wfd);
writers.set_write_eof("test");
assert_eq!(
writers.handle_write("test").unwrap(),
StreamWriteOutcome::Closed
);
assert!(!writers.contains("test"));
close_fd(rfd);
close_fd(wfd);
}
#[test]
fn test_stream_writer_eagain_keeps_buffer() {
let (rfd, wfd) = make_pipe();
unsafe {
let flags = libc::fcntl(wfd, libc::F_GETFL);
libc::fcntl(wfd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let mut writers = StreamWriters::new();
writers.set_writer("test", wfd);
let chunk = [b'x'; 65536];
let mut saw_pending = false;
for _ in 0..256 {
writers.write("test", &chunk);
match writers.handle_write("test").unwrap() {
StreamWriteOutcome::Pending => {
saw_pending = true;
break;
}
StreamWriteOutcome::Flushed => continue,
other => panic!("unexpected outcome {:?}", other),
}
}
assert!(saw_pending, "pipe should fill up and report EAGAIN");
assert!(
writers.contains("test"),
"stream must stay registered on EAGAIN"
);
assert!(
writers.buffered_len("test") > 0,
"unwritten data stays buffered"
);
close_fd(rfd);
close_fd(wfd);
}
#[test]
fn test_stream_writer_unknown_stream_ops_dropped() {
let mut writers = StreamWriters::new();
writers.write("nope", b"data");
writers.set_write_eof("nope");
assert_eq!(writers.buffered_len("nope"), 0);
assert!(!writers.contains("nope"));
assert_eq!(
writers.handle_write("nope").unwrap(),
StreamWriteOutcome::Unknown
);
}
#[test]
fn test_stream_writer_remove() {
let (rfd, wfd) = make_pipe();
let mut writers = StreamWriters::new();
writers.set_writer("test", wfd);
writers.write("test", b"queued");
assert!(writers.remove("test"));
assert!(!writers.remove("test"));
writers.write("test", b"more");
assert_eq!(writers.buffered_len("test"), 0);
close_fd(rfd);
close_fd(wfd);
}
#[test]
fn test_stream_writer_pending_fds() {
let (rfd, wfd) = make_pipe();
let mut writers = StreamWriters::new();
writers.set_writer("test", wfd);
assert!(writers.pending_fds().is_empty());
writers.write("test", b"x");
assert_eq!(writers.pending_fds(), vec![wfd]);
assert_eq!(
writers.handle_write("test").unwrap(),
StreamWriteOutcome::Flushed
);
assert!(writers.pending_fds().is_empty());
close_fd(rfd);
close_fd(wfd);
}
}