1use std::collections::HashMap;
7use std::io::Read;
8use std::os::unix::io::{AsRawFd, RawFd};
9use std::process::{Child, Command, Stdio};
10use std::time::Duration;
11
12fn set_nonblocking(fd: RawFd) -> std::io::Result<()> {
14 unsafe {
15 let flags = libc::fcntl(fd, libc::F_GETFL);
16 if flags < 0 {
17 return Err(std::io::Error::last_os_error());
18 }
19 let result = libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
20 if result < 0 {
21 return Err(std::io::Error::last_os_error());
22 }
23 }
24 Ok(())
25}
26
27use crate::worker::{EventHandler, Result, Worker, WorkerError, WorkerState};
28
29struct ChildProcess {
31 child: Child,
33 stdout_fd: RawFd,
35 stderr_fd: Option<RawFd>,
37 node: String,
39 buf_stdout: Vec<u8>,
41 buf_stderr: Vec<u8>,
43 read_stderr: bool,
45}
46
47impl ChildProcess {
48 fn new(child: Child, node: String, read_stderr: bool) -> std::io::Result<Self> {
50 let stdout_fd = child.stdout.as_ref().map_or(-1, |s| s.as_raw_fd());
51 let stderr_fd = if read_stderr {
52 child.stderr.as_ref().map(|s| s.as_raw_fd())
53 } else {
54 None
55 };
56
57 if stdout_fd >= 0 {
59 let _ = set_nonblocking(stdout_fd);
60 }
61 if let Some(fd) = stderr_fd {
62 let _ = set_nonblocking(fd);
63 }
64
65 Ok(ChildProcess {
66 child,
67 stdout_fd,
68 stderr_fd,
69 node,
70 buf_stdout: Vec::new(),
71 buf_stderr: Vec::new(),
72 read_stderr,
73 })
74 }
75
76 #[allow(dead_code)]
78 fn read_fds(&self) -> Vec<RawFd> {
79 let mut fds = vec![self.stdout_fd];
80 if self.read_stderr {
81 if let Some(fd) = self.stderr_fd {
82 fds.push(fd);
83 }
84 }
85 fds
86 }
87
88 #[allow(dead_code)]
90 fn is_done(&mut self) -> bool {
91 matches!(self.child.try_wait(), Ok(Some(_)))
92 }
93
94 fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
96 self.child
97 .try_wait()
98 .map(|opt| opt.map(|status| status.code().unwrap_or(-1)))
99 }
100}
101
102pub struct ExecWorker {
108 nodes: Vec<String>,
110 command: String,
112 state: WorkerState,
114 handler: Option<Box<dyn EventHandler>>,
116 processes: HashMap<String, ChildProcess>,
118 retcodes: HashMap<String, i32>,
120 fanout: usize,
122 #[allow(dead_code)]
124 timeout: Option<Duration>,
125 stderr: bool,
127 pending_queue: Vec<String>,
129 running_count: usize,
131}
132
133impl ExecWorker {
134 pub fn new(
142 nodes: Vec<String>,
143 command: String,
144 fanout: usize,
145 timeout: Option<Duration>,
146 ) -> Self {
147 ExecWorker {
148 nodes: nodes.clone(),
149 command,
150 state: WorkerState::Pending,
151 handler: None,
152 processes: HashMap::new(),
153 retcodes: HashMap::new(),
154 fanout,
155 timeout,
156 stderr: false,
157 pending_queue: nodes,
158 running_count: 0,
159 }
160 }
161
162 pub fn with_stderr(mut self, stderr: bool) -> Self {
164 self.stderr = stderr;
165 self
166 }
167
168 fn substitute_node(&self, command: &str, node: &str) -> String {
170 command.replace("%h", node).replace("{node}", node)
171 }
172
173 fn spawn_node(&mut self, node: &str) -> Result<()> {
175 let cmd = self.substitute_node(&self.command, node);
176 let child = Command::new("sh")
177 .arg("-c")
178 .arg(cmd)
179 .stdout(Stdio::piped())
180 .stderr(if self.stderr {
181 Stdio::piped()
182 } else {
183 Stdio::null()
184 })
185 .spawn()
186 .map_err(|e| {
187 WorkerError::General(format!("failed to spawn process for {}: {}", node, e))
188 })?;
189
190 let process =
191 ChildProcess::new(child, node.to_string(), self.stderr).map_err(WorkerError::Io)?;
192
193 let node_name = process.node.clone();
194 self.processes.insert(node_name.clone(), process);
195 self.running_count += 1;
196
197 if let Some(mut handler) = self.handler.take() {
199 handler.on_start(self);
200 self.handler = Some(handler);
201 }
202
203 Ok(())
204 }
205
206 fn read_from_fd(&mut self, fd: RawFd) -> Result<bool> {
208 let mut read_something = false;
209 let mut events: Vec<(String, RawFd, Vec<u8>)> = Vec::new();
212
213 for (node, process) in self.processes.iter_mut() {
215 let is_stdout = process.stdout_fd == fd;
216 let is_stderr = process.read_stderr && process.stderr_fd == Some(fd);
217
218 if !is_stdout && !is_stderr {
219 continue;
220 }
221
222 let mut buffer = [0u8; 4096];
223
224 let read_result = if is_stdout {
226 if let Some(ref mut stdout) = process.child.stdout {
227 stdout.read(&mut buffer)
228 } else {
229 continue;
230 }
231 } else if let Some(ref mut stderr) = process.child.stderr {
232 stderr.read(&mut buffer)
233 } else {
234 continue;
235 };
236
237 match read_result {
238 Ok(0) => {
239 continue;
241 }
242 Ok(n) => {
243 let msg = &buffer[..n];
244 read_something = true;
245
246 let event_fd = if is_stdout { process.stdout_fd } else { fd };
247
248 if is_stdout {
250 process.buf_stdout.extend_from_slice(msg);
251 } else {
252 process.buf_stderr.extend_from_slice(msg);
253 }
254
255 events.push((node.clone(), event_fd, msg.to_vec()));
257 }
258 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
259 continue;
260 }
261 Err(e) => {
262 return Err(WorkerError::Io(e));
263 }
264 }
265 }
266
267 if let Some(ref mut handler) = self.handler {
269 for (node, event_fd, msg) in &events {
270 handler.on_read(node, *event_fd, msg);
271 }
272 }
273
274 Ok(read_something)
275 }
276
277 fn drain_process(process: &mut ChildProcess, handler: &mut Option<Box<dyn EventHandler>>) {
280 let mut buffer = [0u8; 4096];
281
282 if let Some(ref mut stdout) = process.child.stdout {
284 loop {
285 match stdout.read(&mut buffer) {
286 Ok(0) | Err(_) => break,
287 Ok(n) => {
288 let msg = &buffer[..n];
289 process.buf_stdout.extend_from_slice(msg);
290 if let Some(ref mut h) = handler {
291 h.on_read(&process.node, process.stdout_fd, msg);
292 }
293 }
294 }
295 }
296 }
297
298 if process.read_stderr {
300 if let Some(ref mut stderr) = process.child.stderr {
301 loop {
302 match stderr.read(&mut buffer) {
303 Ok(0) | Err(_) => break,
304 Ok(n) => {
305 let msg = &buffer[..n];
306 process.buf_stderr.extend_from_slice(msg);
307 if let Some(ref mut h) = handler {
308 if let Some(fd) = process.stderr_fd {
309 h.on_read(&process.node, fd, msg);
310 }
311 }
312 }
313 }
314 }
315 }
316 }
317 }
318
319 fn check_exited_processes(&mut self) -> Result<bool> {
321 let mut something_changed = false;
322 let mut exited_nodes: Vec<String> = Vec::new();
323
324 for (node, process) in self.processes.iter_mut() {
325 if let Ok(Some(rc)) = process.try_wait() {
326 exited_nodes.push(node.clone());
327 self.retcodes.insert(node.clone(), rc);
328 }
329 }
330
331 for node in &exited_nodes {
332 if let Some(process) = self.processes.get_mut(node) {
334 Self::drain_process(process, &mut self.handler);
335 }
336
337 if let Some(ref mut handler) = self.handler {
339 let rc = self.retcodes.get(node).copied().unwrap_or(-1);
340 handler.on_close(node, rc);
341 }
342 }
343
344 for node in exited_nodes {
345 self.processes.remove(&node);
346 self.running_count -= 1;
347 something_changed = true;
348 }
349
350 Ok(something_changed)
351 }
352}
353
354impl Worker for ExecWorker {
355 fn start(&mut self) -> Result<()> {
356 if self.state != WorkerState::Pending {
357 return Err(WorkerError::General(
358 "worker already started or aborted".to_string(),
359 ));
360 }
361
362 self.state = WorkerState::Running;
363
364 while self.running_count < self.fanout && !self.pending_queue.is_empty() {
366 let node = self.pending_queue.remove(0);
367 self.spawn_node(&node)?;
368 }
369
370 Ok(())
371 }
372
373 fn abort(&mut self, kill: bool) {
374 if self.state == WorkerState::Done || self.state == WorkerState::Aborted {
375 return;
376 }
377
378 if kill {
379 for (_node, process) in self.processes.iter_mut() {
381 let _ = process.child.kill();
382 }
383 }
384
385 self.processes.clear();
387 self.pending_queue.clear();
388 self.running_count = 0;
389 self.state = WorkerState::Aborted;
390 }
391
392 fn state(&self) -> WorkerState {
393 self.state
394 }
395
396 fn set_handler(&mut self, handler: Box<dyn EventHandler>) {
397 self.handler = Some(handler);
398 }
399
400 fn read_fds(&self) -> Vec<RawFd> {
401 let mut fds = Vec::new();
402 for process in self.processes.values() {
403 fds.push(process.stdout_fd);
404 if process.read_stderr {
405 if let Some(fd) = process.stderr_fd {
406 fds.push(fd);
407 }
408 }
409 }
410 fds
411 }
412
413 fn write_fds(&self) -> Vec<RawFd> {
414 Vec::new()
416 }
417
418 fn handle_read(&mut self, fd: RawFd) -> Result<()> {
419 self.read_from_fd(fd)?;
421
422 self.check_exited_processes()?;
424
425 while self.running_count < self.fanout && !self.pending_queue.is_empty() {
427 let node = self.pending_queue.remove(0);
428 self.spawn_node(&node)?;
429 }
430
431 if self.processes.is_empty() && self.pending_queue.is_empty() {
433 self.state = WorkerState::Done;
434 }
435
436 Ok(())
437 }
438
439 fn handle_write(&mut self, _fd: RawFd) -> Result<()> {
440 Ok(())
442 }
443
444 fn is_done(&self) -> bool {
445 self.state == WorkerState::Done || self.state == WorkerState::Aborted
446 }
447
448 fn retcodes(&self) -> &HashMap<String, i32> {
449 &self.retcodes
450 }
451
452 fn num_nodes(&self) -> usize {
453 self.nodes.len()
454 }
455
456 fn take_handler(&mut self) -> Option<Box<dyn EventHandler>> {
457 self.handler.take()
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use std::sync::atomic::{AtomicUsize, Ordering};
465 use std::sync::Arc;
466
467 #[test]
469 fn test_exec_worker_creation() {
470 let nodes = vec!["localhost".to_string()];
471 let worker = ExecWorker::new(nodes, "echo hello".to_string(), 1, None);
472
473 assert_eq!(worker.state(), WorkerState::Pending);
474 assert_eq!(worker.num_nodes(), 1);
475 assert!(worker.retcodes().is_empty());
476 }
477
478 #[test]
480 fn test_exec_worker_multiple_nodes() {
481 let nodes = vec![
482 "node1".to_string(),
483 "node2".to_string(),
484 "node3".to_string(),
485 ];
486 let worker = ExecWorker::new(nodes, "echo test".to_string(), 2, None);
487
488 assert_eq!(worker.state(), WorkerState::Pending);
489 assert_eq!(worker.num_nodes(), 3);
490 }
491
492 #[test]
494 fn test_exec_worker_with_timeout() {
495 let nodes = vec!["localhost".to_string()];
496 let timeout = Duration::from_secs(30);
497 let worker = ExecWorker::new(nodes, "sleep 1".to_string(), 1, Some(timeout));
498
499 assert_eq!(worker.state(), WorkerState::Pending);
500 }
501
502 #[test]
504 fn test_exec_worker_with_stderr() {
505 let nodes = vec!["localhost".to_string()];
506 let worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None).with_stderr(true);
507
508 assert!(worker.stderr);
509 }
510
511 #[test]
513 fn test_node_substitution_percent_h() {
514 let nodes = vec!["myhost".to_string()];
515 let worker = ExecWorker::new(nodes, "ssh %h 'ls'".to_string(), 1, None);
516
517 let cmd = worker.substitute_node("ssh %h 'ls'", "myhost");
518 assert_eq!(cmd, "ssh myhost 'ls'");
519 }
520
521 #[test]
523 fn test_node_substitution_braces() {
524 let nodes = vec!["myhost".to_string()];
525 let worker = ExecWorker::new(nodes, "ssh {node} 'ls'".to_string(), 1, None);
526
527 let cmd = worker.substitute_node("ssh {node} 'ls'", "myhost");
528 assert_eq!(cmd, "ssh myhost 'ls'");
529 }
530
531 #[test]
533 fn test_exec_worker_simple_echo() {
534 let nodes = vec!["localhost".to_string()];
535 let mut worker = ExecWorker::new(nodes, "echo hello".to_string(), 1, None);
536
537 assert_eq!(worker.state(), WorkerState::Pending);
539
540 let result = worker.start();
542 assert!(result.is_ok());
543
544 assert_eq!(worker.state(), WorkerState::Running);
546
547 let start = std::time::Instant::now();
549 while !worker.is_done() && start.elapsed() < Duration::from_secs(5) {
550 let fds = worker.read_fds();
551 for fd in &fds {
552 let _ = worker.handle_read(*fd);
553 }
554 std::thread::sleep(Duration::from_millis(10));
555 }
556
557 assert!(worker.is_done() || worker.state() == WorkerState::Done);
559
560 let retcodes = worker.retcodes();
562 assert!(!retcodes.is_empty());
563 assert!(retcodes.contains_key("localhost"));
564 }
565
566 #[test]
568 fn test_exec_worker_with_handler() {
569 let nodes = vec!["localhost".to_string()];
570 let mut worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None);
571
572 struct TestHandler {
573 pub on_start_count: Arc<AtomicUsize>,
574 pub on_read_count: Arc<AtomicUsize>,
575 pub on_close_count: Arc<AtomicUsize>,
576 }
577
578 impl EventHandler for TestHandler {
579 fn on_start(&mut self, _worker: &dyn Worker) {
580 self.on_start_count.fetch_add(1, Ordering::SeqCst);
581 }
582
583 fn on_read(&mut self, _node: &str, _fd: RawFd, _msg: &[u8]) {
584 self.on_read_count.fetch_add(1, Ordering::SeqCst);
585 }
586
587 fn on_close(&mut self, _node: &str, _rc: i32) {
588 self.on_close_count.fetch_add(1, Ordering::SeqCst);
589 }
590 }
591
592 let on_start_count = Arc::new(AtomicUsize::new(0));
593 let on_read_count = Arc::new(AtomicUsize::new(0));
594 let on_close_count = Arc::new(AtomicUsize::new(0));
595
596 let handler = TestHandler {
597 on_start_count: on_start_count.clone(),
598 on_read_count: on_read_count.clone(),
599 on_close_count: on_close_count.clone(),
600 };
601
602 worker.set_handler(Box::new(handler));
603
604 let _ = worker.start();
606
607 let start = std::time::Instant::now();
608 while !worker.is_done() && start.elapsed() < Duration::from_secs(5) {
609 let fds = worker.read_fds();
610 for fd in &fds {
611 let _ = worker.handle_read(*fd);
612 }
613 std::thread::sleep(Duration::from_millis(10));
614 }
615
616 assert!(
618 on_start_count.load(Ordering::SeqCst) >= 1,
619 "on_start should be called"
620 );
621 assert!(
622 on_close_count.load(Ordering::SeqCst) >= 1,
623 "on_close should be called"
624 );
625 }
626
627 #[test]
629 fn test_exec_worker_fanout() {
630 let nodes = vec![
631 "node1".to_string(),
632 "node2".to_string(),
633 "node3".to_string(),
634 "node4".to_string(),
635 ];
636 let mut worker = ExecWorker::new(nodes, "echo test".to_string(), 2, None);
637
638 let _ = worker.start();
640
641 std::thread::sleep(Duration::from_millis(50));
643
644 assert!(
646 worker.running_count <= 2,
647 "Should have at most fanout processes running"
648 );
649 }
650
651 #[test]
653 fn test_exec_worker_abort() {
654 let nodes = vec!["localhost".to_string()];
655 let mut worker = ExecWorker::new(nodes, "sleep 10".to_string(), 1, None);
656
657 let _ = worker.start();
658 assert_eq!(worker.state(), WorkerState::Running);
659
660 worker.abort(true);
661 assert_eq!(worker.state(), WorkerState::Aborted);
662 assert!(worker.processes.is_empty());
663 }
664
665 #[test]
667 fn test_exec_worker_read_fds() {
668 let nodes = vec!["localhost".to_string()];
669 let worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None);
670
671 let fds = worker.read_fds();
672 assert!(fds.is_empty(), "No fds before start");
673 }
674
675 #[test]
677 fn test_exec_worker_write_fds() {
678 let nodes = vec!["localhost".to_string()];
679 let worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None);
680
681 let fds = worker.write_fds();
682 assert!(fds.is_empty(), "ExecWorker doesn't write to children");
683 }
684
685 #[test]
689 fn test_exec_worker_all_nodes_produce_output() {
690 use std::collections::HashSet;
691 use std::sync::Mutex;
692
693 let nodes: Vec<String> = (1..=5).map(|i| format!("node{i}")).collect();
694 let mut worker =
695 ExecWorker::new(nodes.clone(), "echo output_from_%h".to_string(), 64, None);
696
697 let collected = Arc::new(Mutex::new(HashMap::<String, Vec<u8>>::new()));
698 let collected_clone = collected.clone();
699 let close_count = Arc::new(AtomicUsize::new(0));
700 let close_count_clone = close_count.clone();
701
702 struct CollectorHandler {
703 collected: Arc<Mutex<HashMap<String, Vec<u8>>>>,
704 close_count: Arc<AtomicUsize>,
705 }
706
707 impl EventHandler for CollectorHandler {
708 fn on_start(&mut self, _worker: &dyn Worker) {}
709
710 fn on_read(&mut self, node: &str, _fd: RawFd, msg: &[u8]) {
711 let mut map = self.collected.lock().unwrap();
712 map.entry(node.to_string())
713 .or_insert_with(Vec::new)
714 .extend_from_slice(msg);
715 }
716
717 fn on_close(&mut self, _node: &str, _rc: i32) {
718 self.close_count.fetch_add(1, Ordering::SeqCst);
719 }
720 }
721
722 worker.set_handler(Box::new(CollectorHandler {
723 collected: collected_clone,
724 close_count: close_count_clone,
725 }));
726
727 let _ = worker.start();
728
729 let start = std::time::Instant::now();
730 while !worker.is_done() && start.elapsed() < Duration::from_secs(10) {
731 let fds = worker.read_fds();
732 for fd in &fds {
733 let _ = worker.handle_read(*fd);
734 }
735 std::thread::sleep(Duration::from_millis(10));
736 }
737
738 assert!(worker.is_done(), "worker should finish");
739
740 assert_eq!(
742 close_count.load(Ordering::SeqCst),
743 5,
744 "all nodes should close"
745 );
746
747 let map = collected.lock().unwrap();
749 let nodes_with_output: HashSet<&String> = map.keys().collect();
750 for node in &nodes {
751 assert!(
752 nodes_with_output.contains(node),
753 "node {} should have output, got nodes: {:?}",
754 node,
755 nodes_with_output
756 );
757 let output = String::from_utf8_lossy(&map[node]);
758 assert!(
759 output.contains(&format!("output_from_{node}")),
760 "node {} should have correct output, got: {}",
761 node,
762 output
763 );
764 }
765 }
766}