1pub mod exec;
13pub mod ssh;
14pub mod tree;
15
16use std::collections::{HashMap, HashSet};
17use std::os::unix::io::RawFd;
18use thiserror::Error;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum StreamWriteOutcome {
31 Flushed,
33 Pending,
36 BrokenPipe,
40 Closed,
42 Unknown,
44}
45
46#[derive(Debug)]
49struct WriteStream {
50 fd: RawFd,
52 wbuf: Vec<u8>,
54 eof: bool,
57}
58
59#[derive(Debug, Default)]
72pub struct StreamWriters {
73 streams: HashMap<String, WriteStream>,
74}
75
76impl StreamWriters {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn set_writer(&mut self, sname: &str, fd: RawFd) {
85 self.streams.insert(
86 sname.to_string(),
87 WriteStream {
88 fd,
89 wbuf: Vec::new(),
90 eof: false,
91 },
92 );
93 }
94
95 pub fn remove(&mut self, sname: &str) -> bool {
98 self.streams.remove(sname).is_some()
99 }
100
101 pub fn contains(&self, sname: &str) -> bool {
103 self.streams.contains_key(sname)
104 }
105
106 pub fn len(&self) -> usize {
108 self.streams.len()
109 }
110
111 pub fn is_empty(&self) -> bool {
113 self.streams.is_empty()
114 }
115
116 pub fn write(&mut self, sname: &str, buf: &[u8]) {
122 if let Some(stream) = self.streams.get_mut(sname) {
123 stream.wbuf.extend_from_slice(buf);
124 }
125 }
128
129 pub fn set_write_eof(&mut self, sname: &str) {
132 if let Some(stream) = self.streams.get_mut(sname) {
133 stream.eof = true;
134 }
135 }
136
137 pub fn buffered_len(&self, sname: &str) -> usize {
139 self.streams.get(sname).map_or(0, |s| s.wbuf.len())
140 }
141
142 pub fn pending_fds(&self) -> Vec<RawFd> {
144 self.streams
145 .values()
146 .filter(|s| !s.wbuf.is_empty() || s.eof)
147 .map(|s| s.fd)
148 .collect()
149 }
150
151 pub fn handle_write(&mut self, sname: &str) -> std::io::Result<StreamWriteOutcome> {
163 let Some(stream) = self.streams.get_mut(sname) else {
164 return Ok(StreamWriteOutcome::Unknown);
165 };
166
167 if stream.wbuf.is_empty() {
168 if stream.eof {
169 self.streams.remove(sname);
171 return Ok(StreamWriteOutcome::Closed);
172 }
173 return Ok(StreamWriteOutcome::Flushed);
174 }
175
176 let fd = stream.fd;
177 let written = unsafe {
178 libc::write(
179 fd,
180 stream.wbuf.as_ptr() as *const libc::c_void,
181 stream.wbuf.len(),
182 )
183 };
184
185 if written < 0 {
186 let err = std::io::Error::last_os_error();
187 match err.raw_os_error() {
188 Some(libc::EAGAIN) => {
189 return Ok(StreamWriteOutcome::Pending);
191 }
192 Some(libc::EPIPE) => {
193 self.streams.remove(sname);
197 return Ok(StreamWriteOutcome::BrokenPipe);
198 }
199 _ => return Err(err),
200 }
201 }
202
203 let wcnt = written as usize;
204 if wcnt == 0 {
205 return Ok(StreamWriteOutcome::Pending);
206 }
207
208 let stream = self
209 .streams
210 .get_mut(sname)
211 .expect("stream removed during write");
212 stream.wbuf.drain(..wcnt);
213
214 if stream.wbuf.is_empty() && stream.eof {
215 self.streams.remove(sname);
216 return Ok(StreamWriteOutcome::Closed);
217 }
218 if stream.wbuf.is_empty() {
219 Ok(StreamWriteOutcome::Flushed)
220 } else {
221 Ok(StreamWriteOutcome::Pending)
222 }
223 }
224}
225
226#[derive(Debug, Error)]
228pub enum WorkerError {
229 #[error("io error: {0}")]
231 Io(#[from] std::io::Error),
232 #[error("worker error: {0}")]
234 General(String),
235 #[error("timeout")]
237 Timeout,
238}
239
240pub type Result<T> = std::result::Result<T, WorkerError>;
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum WorkerState {
246 Pending,
248 Running,
250 Done,
252 Aborted,
254}
255
256pub trait EventHandler: Send {
260 fn on_start(&mut self, worker: &dyn Worker) {
262 let _ = worker;
263 }
264
265 fn on_read(&mut self, node: &str, fd: RawFd, msg: &[u8]) {
267 let _ = (node, fd, msg);
268 }
269
270 fn on_close(&mut self, node: &str, rc: i32) {
272 let _ = (node, rc);
273 }
274
275 fn on_timeout(&mut self, node: &str) {
277 let _ = node;
278 }
279
280 fn on_error(&mut self, node: &str, error: &WorkerError) {
282 let _ = (node, error);
283 }
284
285 fn take_buffers(
289 &mut self,
290 ) -> (
291 HashMap<String, Vec<Vec<u8>>>,
292 HashMap<String, Vec<Vec<u8>>>,
293 HashSet<String>,
294 ) {
295 (HashMap::new(), HashMap::new(), HashSet::new())
296 }
297}
298
299pub trait Worker: Send {
304 fn start(&mut self) -> Result<()>;
306
307 fn abort(&mut self, kill: bool);
309
310 fn state(&self) -> WorkerState;
312
313 fn set_handler(&mut self, handler: Box<dyn EventHandler>);
315
316 fn read_fds(&self) -> Vec<RawFd>;
318
319 fn write_fds(&self) -> Vec<RawFd>;
321
322 fn handle_read(&mut self, fd: RawFd) -> Result<()>;
324
325 fn handle_write(&mut self, fd: RawFd) -> Result<()>;
327
328 fn is_done(&self) -> bool;
330
331 fn retcodes(&self) -> &HashMap<String, i32>;
333
334 fn num_nodes(&self) -> usize;
336
337 fn take_handler(&mut self) -> Option<Box<dyn EventHandler>>;
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use std::time::Duration;
345
346 #[test]
348 fn test_worker_state_variants() {
349 assert_eq!(WorkerState::Pending, WorkerState::Pending);
350 assert_eq!(WorkerState::Running, WorkerState::Running);
351 assert_eq!(WorkerState::Done, WorkerState::Done);
352 assert_eq!(WorkerState::Aborted, WorkerState::Aborted);
353 }
354
355 #[test]
357 fn test_worker_state_debug() {
358 assert_eq!(format!("{:?}", WorkerState::Pending), "Pending");
359 assert_eq!(format!("{:?}", WorkerState::Running), "Running");
360 assert_eq!(format!("{:?}", WorkerState::Done), "Done");
361 assert_eq!(format!("{:?}", WorkerState::Aborted), "Aborted");
362 }
363
364 #[test]
366 fn test_worker_state_clone() {
367 let state = WorkerState::Pending;
368 let cloned = state.clone();
369 assert_eq!(state, cloned);
370 }
371
372 #[test]
374 fn test_worker_state_copy() {
375 fn take_copy<T: Copy>(_: T) {}
376 take_copy(WorkerState::Pending);
377 }
378
379 #[test]
381 fn test_worker_state_partial_eq() {
382 assert!(WorkerState::Pending == WorkerState::Pending);
383 assert!(WorkerState::Running != WorkerState::Pending);
384 }
385
386 #[test]
388 fn test_worker_error_variants() {
389 let io_err = WorkerError::Io(std::io::Error::new(
390 std::io::ErrorKind::NotFound,
391 "not found",
392 ));
393 let gen_err = WorkerError::General("something went wrong".to_string());
394 let timeout_err = WorkerError::Timeout;
395
396 matches!(io_err, WorkerError::Io(_));
397 matches!(gen_err, WorkerError::General(_));
398 matches!(timeout_err, WorkerError::Timeout);
399 }
400
401 #[test]
403 fn test_worker_error_impls_error() {
404 use std::error::Error;
405
406 let gen_err = WorkerError::General("test".to_string());
407 assert!(gen_err.source().is_none());
408
409 let timeout_err = WorkerError::Timeout;
410 assert!(timeout_err.source().is_none());
411 }
412
413 #[test]
415 fn test_worker_error_impls_display() {
416 let io_err = WorkerError::Io(std::io::Error::new(
417 std::io::ErrorKind::NotFound,
418 "file.txt",
419 ));
420 assert_eq!(io_err.to_string(), "io error: file.txt");
421
422 let gen_err = WorkerError::General("test error".to_string());
423 assert_eq!(gen_err.to_string(), "worker error: test error");
424
425 let timeout_err = WorkerError::Timeout;
426 assert_eq!(timeout_err.to_string(), "timeout");
427 }
428
429 #[test]
431 fn test_event_handler_default_methods() {
432 struct TestHandler;
433
434 impl EventHandler for TestHandler {}
435
436 let handler = TestHandler;
437
438 let _ = handler;
441 }
442
443 #[test]
445 fn test_event_handler_object_safe() {
446 struct TestHandler;
447
448 impl EventHandler for TestHandler {}
449
450 let _handler: Box<dyn EventHandler> = Box::new(TestHandler);
451 }
452
453 #[test]
455 fn test_event_handler_is_send() {
456 fn assert_send<T: Send>() {}
457 assert_send::<Box<dyn EventHandler>>();
458 }
459
460 #[test]
462 fn test_result_type_alias() {
463 let ok: Result<()> = Ok(());
464 let err: Result<()> = Err(WorkerError::Timeout);
465
466 assert!(ok.is_ok());
467 assert!(err.is_err());
468 }
469
470 fn make_pipe() -> (RawFd, RawFd) {
479 let mut fds = [0; 2];
480 let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
481 assert_eq!(rc, 0, "pipe() failed");
482 for fd in fds {
483 unsafe {
484 libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
485 }
486 }
487 (fds[0], fds[1])
488 }
489
490 fn read_fd(fd: RawFd, buf: &mut [u8]) -> usize {
492 unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) as usize }
493 }
494
495 fn close_fd(fd: RawFd) {
496 unsafe {
497 libc::close(fd);
498 }
499 }
500
501 #[test]
509 fn test_stream_writer_broken_pipe_on_write_twice() {
510 let (rfd, wfd) = make_pipe();
511 let mut writers = StreamWriters::new();
512 writers.set_writer("test", wfd);
513
514 writers.write("test", b"initial");
516 assert_eq!(
517 writers.handle_write("test").unwrap(),
518 StreamWriteOutcome::Flushed
519 );
520 let mut buf = [0u8; 16];
521 let n = read_fd(rfd, &mut buf);
522 assert_eq!(&buf[..n], b"initial");
523
524 close_fd(rfd);
526
527 let deadline = std::time::Instant::now() + Duration::from_secs(5);
532 loop {
533 writers.write("test", b"final");
534 match writers.handle_write("test").unwrap() {
535 StreamWriteOutcome::BrokenPipe => break,
536 _ => {
537 assert!(
538 std::time::Instant::now() < deadline,
539 "pipe never reported EPIPE"
540 );
541 std::thread::sleep(Duration::from_millis(5));
542 }
543 }
544 }
545 assert!(!writers.contains("test"));
546 assert!(writers.is_empty());
547
548 writers.write("test", b"more");
550 assert_eq!(writers.buffered_len("test"), 0);
551 writers.set_write_eof("test");
552 assert_eq!(
553 writers.handle_write("test").unwrap(),
554 StreamWriteOutcome::Unknown
555 );
556
557 close_fd(wfd);
558 }
559
560 #[test]
563 fn test_stream_writer_eof_closes_after_drain() {
564 let (rfd, wfd) = make_pipe();
565 let mut writers = StreamWriters::new();
566 writers.set_writer("stdin", wfd);
567
568 writers.write("stdin", b"payload");
569 writers.set_write_eof("stdin");
570 assert_eq!(
571 writers.handle_write("stdin").unwrap(),
572 StreamWriteOutcome::Closed
573 );
574 assert!(!writers.contains("stdin"));
575
576 let mut buf = [0u8; 16];
577 let n = read_fd(rfd, &mut buf);
578 assert_eq!(&buf[..n], b"payload");
579
580 close_fd(rfd);
581 close_fd(wfd);
582 }
583
584 #[test]
586 fn test_stream_writer_eof_empty_buffer_closes() {
587 let (rfd, wfd) = make_pipe();
588 let mut writers = StreamWriters::new();
589 writers.set_writer("test", wfd);
590 writers.set_write_eof("test");
591 assert_eq!(
592 writers.handle_write("test").unwrap(),
593 StreamWriteOutcome::Closed
594 );
595 assert!(!writers.contains("test"));
596 close_fd(rfd);
597 close_fd(wfd);
598 }
599
600 #[test]
603 fn test_stream_writer_eagain_keeps_buffer() {
604 let (rfd, wfd) = make_pipe();
605 unsafe {
607 let flags = libc::fcntl(wfd, libc::F_GETFL);
608 libc::fcntl(wfd, libc::F_SETFL, flags | libc::O_NONBLOCK);
609 }
610
611 let mut writers = StreamWriters::new();
612 writers.set_writer("test", wfd);
613
614 let chunk = [b'x'; 65536];
616 let mut saw_pending = false;
617 for _ in 0..256 {
618 writers.write("test", &chunk);
619 match writers.handle_write("test").unwrap() {
620 StreamWriteOutcome::Pending => {
621 saw_pending = true;
622 break;
623 }
624 StreamWriteOutcome::Flushed => continue,
625 other => panic!("unexpected outcome {:?}", other),
626 }
627 }
628 assert!(saw_pending, "pipe should fill up and report EAGAIN");
629 assert!(
630 writers.contains("test"),
631 "stream must stay registered on EAGAIN"
632 );
633 assert!(
634 writers.buffered_len("test") > 0,
635 "unwritten data stays buffered"
636 );
637
638 close_fd(rfd);
639 close_fd(wfd);
640 }
641
642 #[test]
646 fn test_stream_writer_unknown_stream_ops_dropped() {
647 let mut writers = StreamWriters::new();
648 writers.write("nope", b"data");
649 writers.set_write_eof("nope");
650 assert_eq!(writers.buffered_len("nope"), 0);
651 assert!(!writers.contains("nope"));
652 assert_eq!(
653 writers.handle_write("nope").unwrap(),
654 StreamWriteOutcome::Unknown
655 );
656 }
657
658 #[test]
660 fn test_stream_writer_remove() {
661 let (rfd, wfd) = make_pipe();
662 let mut writers = StreamWriters::new();
663 writers.set_writer("test", wfd);
664 writers.write("test", b"queued");
665 assert!(writers.remove("test"));
666 assert!(!writers.remove("test"));
667 writers.write("test", b"more");
668 assert_eq!(writers.buffered_len("test"), 0);
669 close_fd(rfd);
670 close_fd(wfd);
671 }
672
673 #[test]
675 fn test_stream_writer_pending_fds() {
676 let (rfd, wfd) = make_pipe();
677 let mut writers = StreamWriters::new();
678 writers.set_writer("test", wfd);
679 assert!(writers.pending_fds().is_empty());
680 writers.write("test", b"x");
681 assert_eq!(writers.pending_fds(), vec![wfd]);
682 assert_eq!(
683 writers.handle_write("test").unwrap(),
684 StreamWriteOutcome::Flushed
685 );
686 assert!(writers.pending_fds().is_empty());
687 close_fd(rfd);
688 close_fd(wfd);
689 }
690}