use std::collections::HashMap;
use std::io::Read;
use std::os::unix::io::{AsRawFd, RawFd};
use std::process::{Child, Command, Stdio};
use std::time::Duration;
fn set_nonblocking(fd: RawFd) -> std::io::Result<()> {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
let result = libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
if result < 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
use crate::worker::{EventHandler, Result, Worker, WorkerError, WorkerState};
struct ChildProcess {
child: Child,
stdout_fd: RawFd,
stderr_fd: Option<RawFd>,
node: String,
buf_stdout: Vec<u8>,
buf_stderr: Vec<u8>,
read_stderr: bool,
}
impl ChildProcess {
fn new(child: Child, node: String, read_stderr: bool) -> std::io::Result<Self> {
let stdout_fd = child.stdout.as_ref().map_or(-1, |s| s.as_raw_fd());
let stderr_fd = if read_stderr {
child.stderr.as_ref().map(|s| s.as_raw_fd())
} else {
None
};
if stdout_fd >= 0 {
let _ = set_nonblocking(stdout_fd);
}
if let Some(fd) = stderr_fd {
let _ = set_nonblocking(fd);
}
Ok(ChildProcess {
child,
stdout_fd,
stderr_fd,
node,
buf_stdout: Vec::new(),
buf_stderr: Vec::new(),
read_stderr,
})
}
#[allow(dead_code)]
fn read_fds(&self) -> Vec<RawFd> {
let mut fds = vec![self.stdout_fd];
if self.read_stderr {
if let Some(fd) = self.stderr_fd {
fds.push(fd);
}
}
fds
}
#[allow(dead_code)]
fn is_done(&mut self) -> bool {
matches!(self.child.try_wait(), Ok(Some(_)))
}
fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
self.child
.try_wait()
.map(|opt| opt.map(|status| status.code().unwrap_or(-1)))
}
}
pub struct ExecWorker {
nodes: Vec<String>,
command: String,
state: WorkerState,
handler: Option<Box<dyn EventHandler>>,
processes: HashMap<String, ChildProcess>,
retcodes: HashMap<String, i32>,
fanout: usize,
#[allow(dead_code)]
timeout: Option<Duration>,
stderr: bool,
pending_queue: Vec<String>,
running_count: usize,
}
impl ExecWorker {
pub fn new(
nodes: Vec<String>,
command: String,
fanout: usize,
timeout: Option<Duration>,
) -> Self {
ExecWorker {
nodes: nodes.clone(),
command,
state: WorkerState::Pending,
handler: None,
processes: HashMap::new(),
retcodes: HashMap::new(),
fanout,
timeout,
stderr: false,
pending_queue: nodes,
running_count: 0,
}
}
pub fn with_stderr(mut self, stderr: bool) -> Self {
self.stderr = stderr;
self
}
fn substitute_node(&self, command: &str, node: &str) -> String {
command.replace("%h", node).replace("{node}", node)
}
fn spawn_node(&mut self, node: &str) -> Result<()> {
let cmd = self.substitute_node(&self.command, node);
let child = Command::new("sh")
.arg("-c")
.arg(cmd)
.stdout(Stdio::piped())
.stderr(if self.stderr {
Stdio::piped()
} else {
Stdio::null()
})
.spawn()
.map_err(|e| {
WorkerError::General(format!("failed to spawn process for {}: {}", node, e))
})?;
let process =
ChildProcess::new(child, node.to_string(), self.stderr).map_err(WorkerError::Io)?;
let node_name = process.node.clone();
self.processes.insert(node_name.clone(), process);
self.running_count += 1;
if let Some(mut handler) = self.handler.take() {
handler.on_start(self);
self.handler = Some(handler);
}
Ok(())
}
fn read_from_fd(&mut self, fd: RawFd) -> Result<bool> {
let mut read_something = false;
let mut events: Vec<(String, RawFd, Vec<u8>)> = Vec::new();
for (node, process) in self.processes.iter_mut() {
let is_stdout = process.stdout_fd == fd;
let is_stderr = process.read_stderr && process.stderr_fd == Some(fd);
if !is_stdout && !is_stderr {
continue;
}
let mut buffer = [0u8; 4096];
let read_result = if is_stdout {
if let Some(ref mut stdout) = process.child.stdout {
stdout.read(&mut buffer)
} else {
continue;
}
} else if let Some(ref mut stderr) = process.child.stderr {
stderr.read(&mut buffer)
} else {
continue;
};
match read_result {
Ok(0) => {
continue;
}
Ok(n) => {
let msg = &buffer[..n];
read_something = true;
let event_fd = if is_stdout { process.stdout_fd } else { fd };
if is_stdout {
process.buf_stdout.extend_from_slice(msg);
} else {
process.buf_stderr.extend_from_slice(msg);
}
events.push((node.clone(), event_fd, msg.to_vec()));
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
}
Err(e) => {
return Err(WorkerError::Io(e));
}
}
}
if let Some(ref mut handler) = self.handler {
for (node, event_fd, msg) in &events {
handler.on_read(node, *event_fd, msg);
}
}
Ok(read_something)
}
fn drain_process(process: &mut ChildProcess, handler: &mut Option<Box<dyn EventHandler>>) {
let mut buffer = [0u8; 4096];
if let Some(ref mut stdout) = process.child.stdout {
loop {
match stdout.read(&mut buffer) {
Ok(0) | Err(_) => break,
Ok(n) => {
let msg = &buffer[..n];
process.buf_stdout.extend_from_slice(msg);
if let Some(ref mut h) = handler {
h.on_read(&process.node, process.stdout_fd, msg);
}
}
}
}
}
if process.read_stderr {
if let Some(ref mut stderr) = process.child.stderr {
loop {
match stderr.read(&mut buffer) {
Ok(0) | Err(_) => break,
Ok(n) => {
let msg = &buffer[..n];
process.buf_stderr.extend_from_slice(msg);
if let Some(ref mut h) = handler {
if let Some(fd) = process.stderr_fd {
h.on_read(&process.node, fd, msg);
}
}
}
}
}
}
}
}
fn check_exited_processes(&mut self) -> Result<bool> {
let mut something_changed = false;
let mut exited_nodes: Vec<String> = Vec::new();
for (node, process) in self.processes.iter_mut() {
if let Ok(Some(rc)) = process.try_wait() {
exited_nodes.push(node.clone());
self.retcodes.insert(node.clone(), rc);
}
}
for node in &exited_nodes {
if let Some(process) = self.processes.get_mut(node) {
Self::drain_process(process, &mut self.handler);
}
if let Some(ref mut handler) = self.handler {
let rc = self.retcodes.get(node).copied().unwrap_or(-1);
handler.on_close(node, rc);
}
}
for node in exited_nodes {
self.processes.remove(&node);
self.running_count -= 1;
something_changed = true;
}
Ok(something_changed)
}
}
impl Worker for ExecWorker {
fn start(&mut self) -> Result<()> {
if self.state != WorkerState::Pending {
return Err(WorkerError::General(
"worker already started or aborted".to_string(),
));
}
self.state = WorkerState::Running;
while self.running_count < self.fanout && !self.pending_queue.is_empty() {
let node = self.pending_queue.remove(0);
self.spawn_node(&node)?;
}
Ok(())
}
fn abort(&mut self, kill: bool) {
if self.state == WorkerState::Done || self.state == WorkerState::Aborted {
return;
}
if kill {
for (_node, process) in self.processes.iter_mut() {
let _ = process.child.kill();
}
}
self.processes.clear();
self.pending_queue.clear();
self.running_count = 0;
self.state = WorkerState::Aborted;
}
fn state(&self) -> WorkerState {
self.state
}
fn set_handler(&mut self, handler: Box<dyn EventHandler>) {
self.handler = Some(handler);
}
fn read_fds(&self) -> Vec<RawFd> {
let mut fds = Vec::new();
for process in self.processes.values() {
fds.push(process.stdout_fd);
if process.read_stderr {
if let Some(fd) = process.stderr_fd {
fds.push(fd);
}
}
}
fds
}
fn write_fds(&self) -> Vec<RawFd> {
Vec::new()
}
fn handle_read(&mut self, fd: RawFd) -> Result<()> {
self.read_from_fd(fd)?;
self.check_exited_processes()?;
while self.running_count < self.fanout && !self.pending_queue.is_empty() {
let node = self.pending_queue.remove(0);
self.spawn_node(&node)?;
}
if self.processes.is_empty() && self.pending_queue.is_empty() {
self.state = WorkerState::Done;
}
Ok(())
}
fn handle_write(&mut self, _fd: RawFd) -> Result<()> {
Ok(())
}
fn is_done(&self) -> bool {
self.state == WorkerState::Done || self.state == WorkerState::Aborted
}
fn retcodes(&self) -> &HashMap<String, i32> {
&self.retcodes
}
fn num_nodes(&self) -> usize {
self.nodes.len()
}
fn take_handler(&mut self) -> Option<Box<dyn EventHandler>> {
self.handler.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[test]
fn test_exec_worker_creation() {
let nodes = vec!["localhost".to_string()];
let worker = ExecWorker::new(nodes, "echo hello".to_string(), 1, None);
assert_eq!(worker.state(), WorkerState::Pending);
assert_eq!(worker.num_nodes(), 1);
assert!(worker.retcodes().is_empty());
}
#[test]
fn test_exec_worker_multiple_nodes() {
let nodes = vec![
"node1".to_string(),
"node2".to_string(),
"node3".to_string(),
];
let worker = ExecWorker::new(nodes, "echo test".to_string(), 2, None);
assert_eq!(worker.state(), WorkerState::Pending);
assert_eq!(worker.num_nodes(), 3);
}
#[test]
fn test_exec_worker_with_timeout() {
let nodes = vec!["localhost".to_string()];
let timeout = Duration::from_secs(30);
let worker = ExecWorker::new(nodes, "sleep 1".to_string(), 1, Some(timeout));
assert_eq!(worker.state(), WorkerState::Pending);
}
#[test]
fn test_exec_worker_with_stderr() {
let nodes = vec!["localhost".to_string()];
let worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None).with_stderr(true);
assert!(worker.stderr);
}
#[test]
fn test_node_substitution_percent_h() {
let nodes = vec!["myhost".to_string()];
let worker = ExecWorker::new(nodes, "ssh %h 'ls'".to_string(), 1, None);
let cmd = worker.substitute_node("ssh %h 'ls'", "myhost");
assert_eq!(cmd, "ssh myhost 'ls'");
}
#[test]
fn test_node_substitution_braces() {
let nodes = vec!["myhost".to_string()];
let worker = ExecWorker::new(nodes, "ssh {node} 'ls'".to_string(), 1, None);
let cmd = worker.substitute_node("ssh {node} 'ls'", "myhost");
assert_eq!(cmd, "ssh myhost 'ls'");
}
#[test]
fn test_exec_worker_simple_echo() {
let nodes = vec!["localhost".to_string()];
let mut worker = ExecWorker::new(nodes, "echo hello".to_string(), 1, None);
assert_eq!(worker.state(), WorkerState::Pending);
let result = worker.start();
assert!(result.is_ok());
assert_eq!(worker.state(), WorkerState::Running);
let start = std::time::Instant::now();
while !worker.is_done() && start.elapsed() < Duration::from_secs(5) {
let fds = worker.read_fds();
for fd in &fds {
let _ = worker.handle_read(*fd);
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(worker.is_done() || worker.state() == WorkerState::Done);
let retcodes = worker.retcodes();
assert!(!retcodes.is_empty());
assert!(retcodes.contains_key("localhost"));
}
#[test]
fn test_exec_worker_with_handler() {
let nodes = vec!["localhost".to_string()];
let mut worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None);
struct TestHandler {
pub on_start_count: Arc<AtomicUsize>,
pub on_read_count: Arc<AtomicUsize>,
pub on_close_count: Arc<AtomicUsize>,
}
impl EventHandler for TestHandler {
fn on_start(&mut self, _worker: &dyn Worker) {
self.on_start_count.fetch_add(1, Ordering::SeqCst);
}
fn on_read(&mut self, _node: &str, _fd: RawFd, _msg: &[u8]) {
self.on_read_count.fetch_add(1, Ordering::SeqCst);
}
fn on_close(&mut self, _node: &str, _rc: i32) {
self.on_close_count.fetch_add(1, Ordering::SeqCst);
}
}
let on_start_count = Arc::new(AtomicUsize::new(0));
let on_read_count = Arc::new(AtomicUsize::new(0));
let on_close_count = Arc::new(AtomicUsize::new(0));
let handler = TestHandler {
on_start_count: on_start_count.clone(),
on_read_count: on_read_count.clone(),
on_close_count: on_close_count.clone(),
};
worker.set_handler(Box::new(handler));
let _ = worker.start();
let start = std::time::Instant::now();
while !worker.is_done() && start.elapsed() < Duration::from_secs(5) {
let fds = worker.read_fds();
for fd in &fds {
let _ = worker.handle_read(*fd);
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(
on_start_count.load(Ordering::SeqCst) >= 1,
"on_start should be called"
);
assert!(
on_close_count.load(Ordering::SeqCst) >= 1,
"on_close should be called"
);
}
#[test]
fn test_exec_worker_fanout() {
let nodes = vec![
"node1".to_string(),
"node2".to_string(),
"node3".to_string(),
"node4".to_string(),
];
let mut worker = ExecWorker::new(nodes, "echo test".to_string(), 2, None);
let _ = worker.start();
std::thread::sleep(Duration::from_millis(50));
assert!(
worker.running_count <= 2,
"Should have at most fanout processes running"
);
}
#[test]
fn test_exec_worker_abort() {
let nodes = vec!["localhost".to_string()];
let mut worker = ExecWorker::new(nodes, "sleep 10".to_string(), 1, None);
let _ = worker.start();
assert_eq!(worker.state(), WorkerState::Running);
worker.abort(true);
assert_eq!(worker.state(), WorkerState::Aborted);
assert!(worker.processes.is_empty());
}
#[test]
fn test_exec_worker_read_fds() {
let nodes = vec!["localhost".to_string()];
let worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None);
let fds = worker.read_fds();
assert!(fds.is_empty(), "No fds before start");
}
#[test]
fn test_exec_worker_write_fds() {
let nodes = vec!["localhost".to_string()];
let worker = ExecWorker::new(nodes, "echo test".to_string(), 1, None);
let fds = worker.write_fds();
assert!(fds.is_empty(), "ExecWorker doesn't write to children");
}
#[test]
fn test_exec_worker_all_nodes_produce_output() {
use std::collections::HashSet;
use std::sync::Mutex;
let nodes: Vec<String> = (1..=5).map(|i| format!("node{i}")).collect();
let mut worker =
ExecWorker::new(nodes.clone(), "echo output_from_%h".to_string(), 64, None);
let collected = Arc::new(Mutex::new(HashMap::<String, Vec<u8>>::new()));
let collected_clone = collected.clone();
let close_count = Arc::new(AtomicUsize::new(0));
let close_count_clone = close_count.clone();
struct CollectorHandler {
collected: Arc<Mutex<HashMap<String, Vec<u8>>>>,
close_count: Arc<AtomicUsize>,
}
impl EventHandler for CollectorHandler {
fn on_start(&mut self, _worker: &dyn Worker) {}
fn on_read(&mut self, node: &str, _fd: RawFd, msg: &[u8]) {
let mut map = self.collected.lock().unwrap();
map.entry(node.to_string())
.or_insert_with(Vec::new)
.extend_from_slice(msg);
}
fn on_close(&mut self, _node: &str, _rc: i32) {
self.close_count.fetch_add(1, Ordering::SeqCst);
}
}
worker.set_handler(Box::new(CollectorHandler {
collected: collected_clone,
close_count: close_count_clone,
}));
let _ = worker.start();
let start = std::time::Instant::now();
while !worker.is_done() && start.elapsed() < Duration::from_secs(10) {
let fds = worker.read_fds();
for fd in &fds {
let _ = worker.handle_read(*fd);
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(worker.is_done(), "worker should finish");
assert_eq!(
close_count.load(Ordering::SeqCst),
5,
"all nodes should close"
);
let map = collected.lock().unwrap();
let nodes_with_output: HashSet<&String> = map.keys().collect();
for node in &nodes {
assert!(
nodes_with_output.contains(node),
"node {} should have output, got nodes: {:?}",
node,
nodes_with_output
);
let output = String::from_utf8_lossy(&map[node]);
assert!(
output.contains(&format!("output_from_{node}")),
"node {} should have correct output, got: {}",
node,
output
);
}
}
}