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 type TakenBuffers = (
259 HashMap<String, Vec<Vec<u8>>>,
260 HashMap<String, Vec<Vec<u8>>>,
261 HashSet<String>,
262);
263
264pub trait EventHandler: Send {
268 fn on_start(&mut self, worker: &dyn Worker) {
270 let _ = worker;
271 }
272
273 fn on_read(&mut self, node: &str, fd: RawFd, msg: &[u8]) {
275 let _ = (node, fd, msg);
276 }
277
278 fn on_close(&mut self, node: &str, rc: i32) {
280 let _ = (node, rc);
281 }
282
283 fn on_timeout(&mut self, node: &str) {
285 let _ = node;
286 }
287
288 fn on_error(&mut self, node: &str, error: &WorkerError) {
290 let _ = (node, error);
291 }
292
293 fn take_buffers(&mut self) -> TakenBuffers {
297 (HashMap::new(), HashMap::new(), HashSet::new())
298 }
299}
300
301pub trait Worker: Send {
306 fn start(&mut self) -> Result<()>;
308
309 fn abort(&mut self, kill: bool);
311
312 fn state(&self) -> WorkerState;
314
315 fn set_handler(&mut self, handler: Box<dyn EventHandler>);
317
318 fn read_fds(&self) -> Vec<RawFd>;
320
321 fn write_fds(&self) -> Vec<RawFd>;
323
324 fn handle_read(&mut self, fd: RawFd) -> Result<()>;
326
327 fn handle_write(&mut self, fd: RawFd) -> Result<()>;
329
330 fn is_done(&self) -> bool;
332
333 fn retcodes(&self) -> &HashMap<String, i32>;
335
336 fn num_nodes(&self) -> usize;
338
339 fn take_handler(&mut self) -> Option<Box<dyn EventHandler>>;
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use std::time::Duration;
347
348 #[test]
350 fn test_worker_state_variants() {
351 assert_eq!(WorkerState::Pending, WorkerState::Pending);
352 assert_eq!(WorkerState::Running, WorkerState::Running);
353 assert_eq!(WorkerState::Done, WorkerState::Done);
354 assert_eq!(WorkerState::Aborted, WorkerState::Aborted);
355 }
356
357 #[test]
359 fn test_worker_state_debug() {
360 assert_eq!(format!("{:?}", WorkerState::Pending), "Pending");
361 assert_eq!(format!("{:?}", WorkerState::Running), "Running");
362 assert_eq!(format!("{:?}", WorkerState::Done), "Done");
363 assert_eq!(format!("{:?}", WorkerState::Aborted), "Aborted");
364 }
365
366 #[test]
368 fn test_worker_state_clone() {
369 let state = WorkerState::Pending;
370 let cloned = state;
371 assert_eq!(state, cloned);
372 }
373
374 #[test]
376 fn test_worker_state_copy() {
377 fn take_copy<T: Copy>(_: T) {}
378 take_copy(WorkerState::Pending);
379 }
380
381 #[test]
383 fn test_worker_state_partial_eq() {
384 assert!(WorkerState::Pending == WorkerState::Pending);
385 assert!(WorkerState::Running != WorkerState::Pending);
386 }
387
388 #[test]
390 fn test_worker_error_variants() {
391 let io_err = WorkerError::Io(std::io::Error::new(
392 std::io::ErrorKind::NotFound,
393 "not found",
394 ));
395 let gen_err = WorkerError::General("something went wrong".to_string());
396 let timeout_err = WorkerError::Timeout;
397
398 matches!(io_err, WorkerError::Io(_));
399 matches!(gen_err, WorkerError::General(_));
400 matches!(timeout_err, WorkerError::Timeout);
401 }
402
403 #[test]
405 fn test_worker_error_impls_error() {
406 use std::error::Error;
407
408 let gen_err = WorkerError::General("test".to_string());
409 assert!(gen_err.source().is_none());
410
411 let timeout_err = WorkerError::Timeout;
412 assert!(timeout_err.source().is_none());
413 }
414
415 #[test]
417 fn test_worker_error_impls_display() {
418 let io_err = WorkerError::Io(std::io::Error::new(
419 std::io::ErrorKind::NotFound,
420 "file.txt",
421 ));
422 assert_eq!(io_err.to_string(), "io error: file.txt");
423
424 let gen_err = WorkerError::General("test error".to_string());
425 assert_eq!(gen_err.to_string(), "worker error: test error");
426
427 let timeout_err = WorkerError::Timeout;
428 assert_eq!(timeout_err.to_string(), "timeout");
429 }
430
431 #[test]
433 fn test_event_handler_default_methods() {
434 struct TestHandler;
435
436 impl EventHandler for TestHandler {}
437
438 let handler = TestHandler;
439
440 let _ = handler;
443 }
444
445 #[test]
447 fn test_event_handler_object_safe() {
448 struct TestHandler;
449
450 impl EventHandler for TestHandler {}
451
452 let _handler: Box<dyn EventHandler> = Box::new(TestHandler);
453 }
454
455 #[test]
457 fn test_event_handler_is_send() {
458 fn assert_send<T: Send>() {}
459 assert_send::<Box<dyn EventHandler>>();
460 }
461
462 #[test]
464 fn test_result_type_alias() {
465 let ok: Result<()> = Ok(());
466 let err: Result<()> = Err(WorkerError::Timeout);
467
468 assert!(ok.is_ok());
469 assert!(err.is_err());
470 }
471
472 fn make_pipe() -> (RawFd, RawFd) {
481 let mut fds = [0; 2];
482 let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
483 assert_eq!(rc, 0, "pipe() failed");
484 for fd in fds {
485 unsafe {
486 libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
487 }
488 }
489 (fds[0], fds[1])
490 }
491
492 fn read_fd(fd: RawFd, buf: &mut [u8]) -> usize {
494 unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) as usize }
495 }
496
497 fn close_fd(fd: RawFd) {
498 unsafe {
499 libc::close(fd);
500 }
501 }
502
503 #[test]
511 fn test_stream_writer_broken_pipe_on_write_twice() {
512 let (rfd, wfd) = make_pipe();
513 let mut writers = StreamWriters::new();
514 writers.set_writer("test", wfd);
515
516 writers.write("test", b"initial");
518 assert_eq!(
519 writers.handle_write("test").unwrap(),
520 StreamWriteOutcome::Flushed
521 );
522 let mut buf = [0u8; 16];
523 let n = read_fd(rfd, &mut buf);
524 assert_eq!(&buf[..n], b"initial");
525
526 close_fd(rfd);
528
529 let deadline = std::time::Instant::now() + Duration::from_secs(5);
534 loop {
535 writers.write("test", b"final");
536 match writers.handle_write("test").unwrap() {
537 StreamWriteOutcome::BrokenPipe => break,
538 _ => {
539 assert!(
540 std::time::Instant::now() < deadline,
541 "pipe never reported EPIPE"
542 );
543 std::thread::sleep(Duration::from_millis(5));
544 }
545 }
546 }
547 assert!(!writers.contains("test"));
548 assert!(writers.is_empty());
549
550 writers.write("test", b"more");
552 assert_eq!(writers.buffered_len("test"), 0);
553 writers.set_write_eof("test");
554 assert_eq!(
555 writers.handle_write("test").unwrap(),
556 StreamWriteOutcome::Unknown
557 );
558
559 close_fd(wfd);
560 }
561
562 #[test]
565 fn test_stream_writer_eof_closes_after_drain() {
566 let (rfd, wfd) = make_pipe();
567 let mut writers = StreamWriters::new();
568 writers.set_writer("stdin", wfd);
569
570 writers.write("stdin", b"payload");
571 writers.set_write_eof("stdin");
572 assert_eq!(
573 writers.handle_write("stdin").unwrap(),
574 StreamWriteOutcome::Closed
575 );
576 assert!(!writers.contains("stdin"));
577
578 let mut buf = [0u8; 16];
579 let n = read_fd(rfd, &mut buf);
580 assert_eq!(&buf[..n], b"payload");
581
582 close_fd(rfd);
583 close_fd(wfd);
584 }
585
586 #[test]
588 fn test_stream_writer_eof_empty_buffer_closes() {
589 let (rfd, wfd) = make_pipe();
590 let mut writers = StreamWriters::new();
591 writers.set_writer("test", wfd);
592 writers.set_write_eof("test");
593 assert_eq!(
594 writers.handle_write("test").unwrap(),
595 StreamWriteOutcome::Closed
596 );
597 assert!(!writers.contains("test"));
598 close_fd(rfd);
599 close_fd(wfd);
600 }
601
602 #[test]
605 fn test_stream_writer_eagain_keeps_buffer() {
606 let (rfd, wfd) = make_pipe();
607 unsafe {
609 let flags = libc::fcntl(wfd, libc::F_GETFL);
610 libc::fcntl(wfd, libc::F_SETFL, flags | libc::O_NONBLOCK);
611 }
612
613 let mut writers = StreamWriters::new();
614 writers.set_writer("test", wfd);
615
616 let chunk = [b'x'; 65536];
618 let mut saw_pending = false;
619 for _ in 0..256 {
620 writers.write("test", &chunk);
621 match writers.handle_write("test").unwrap() {
622 StreamWriteOutcome::Pending => {
623 saw_pending = true;
624 break;
625 }
626 StreamWriteOutcome::Flushed => continue,
627 other => panic!("unexpected outcome {:?}", other),
628 }
629 }
630 assert!(saw_pending, "pipe should fill up and report EAGAIN");
631 assert!(
632 writers.contains("test"),
633 "stream must stay registered on EAGAIN"
634 );
635 assert!(
636 writers.buffered_len("test") > 0,
637 "unwritten data stays buffered"
638 );
639
640 close_fd(rfd);
641 close_fd(wfd);
642 }
643
644 #[test]
648 fn test_stream_writer_unknown_stream_ops_dropped() {
649 let mut writers = StreamWriters::new();
650 writers.write("nope", b"data");
651 writers.set_write_eof("nope");
652 assert_eq!(writers.buffered_len("nope"), 0);
653 assert!(!writers.contains("nope"));
654 assert_eq!(
655 writers.handle_write("nope").unwrap(),
656 StreamWriteOutcome::Unknown
657 );
658 }
659
660 #[test]
662 fn test_stream_writer_remove() {
663 let (rfd, wfd) = make_pipe();
664 let mut writers = StreamWriters::new();
665 writers.set_writer("test", wfd);
666 writers.write("test", b"queued");
667 assert!(writers.remove("test"));
668 assert!(!writers.remove("test"));
669 writers.write("test", b"more");
670 assert_eq!(writers.buffered_len("test"), 0);
671 close_fd(rfd);
672 close_fd(wfd);
673 }
674
675 #[test]
677 fn test_stream_writer_pending_fds() {
678 let (rfd, wfd) = make_pipe();
679 let mut writers = StreamWriters::new();
680 writers.set_writer("test", wfd);
681 assert!(writers.pending_fds().is_empty());
682 writers.write("test", b"x");
683 assert_eq!(writers.pending_fds(), vec![wfd]);
684 assert_eq!(
685 writers.handle_write("test").unwrap(),
686 StreamWriteOutcome::Flushed
687 );
688 assert!(writers.pending_fds().is_empty());
689 close_fd(rfd);
690 close_fd(wfd);
691 }
692}