wsi-dicom 0.7.0

Convert whole-slide imaging files to DICOM VL Whole Slide Microscopy
Documentation
use std::ffi::OsString;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};

use super::staged_dicom3tools_command;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CommandOutcome {
    pub(crate) success: bool,
    pub(crate) timed_out: bool,
    pub(crate) stdout: String,
    pub(crate) stderr: String,
    pub(crate) stdout_truncated: bool,
    pub(crate) stderr_truncated: bool,
}

pub(crate) trait ValidationCommandRunner {
    fn find_command(&self, name: &str) -> Option<PathBuf>;
    fn run(
        &self,
        program: &Path,
        args: &[OsString],
        timeout: Duration,
        max_output_bytes: usize,
    ) -> Result<CommandOutcome, io::Error>;
}

pub(super) struct SystemCommandRunner;

impl ValidationCommandRunner for SystemCommandRunner {
    fn find_command(&self, name: &str) -> Option<PathBuf> {
        let command = Path::new(name);
        if command.is_absolute() {
            return command.is_file().then(|| command.to_path_buf());
        }
        std::env::var_os("PATH")
            .and_then(|paths| {
                std::env::split_paths(&paths)
                    .map(|path| path.join(name))
                    .find(|path| path.is_file())
            })
            .or_else(|| staged_dicom3tools_command(name))
    }

    fn run(
        &self,
        program: &Path,
        args: &[OsString],
        timeout: Duration,
        max_output_bytes: usize,
    ) -> Result<CommandOutcome, io::Error> {
        let mut command = Command::new(program);
        command
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());
        #[cfg(unix)]
        {
            use std::os::unix::process::CommandExt;
            command.process_group(0);
        }
        let mut child = command.spawn()?;
        let stdout = child
            .stdout
            .take()
            .ok_or_else(|| io::Error::other("child stdout pipe was not captured"))?;
        let stderr = child
            .stderr
            .take()
            .ok_or_else(|| io::Error::other("child stderr pipe was not captured"))?;
        let stdout_reader = read_child_pipe(stdout, max_output_bytes);
        let stderr_reader = read_child_pipe(stderr, max_output_bytes);
        let started = Instant::now();
        loop {
            match child.try_wait() {
                Ok(Some(status)) => {
                    let (stdout, stderr, stdout_truncated, stderr_truncated) =
                        collect_child_pipes(stdout_reader, stderr_reader)?;
                    return Ok(CommandOutcome {
                        success: status.success(),
                        timed_out: false,
                        stdout,
                        stderr,
                        stdout_truncated,
                        stderr_truncated,
                    });
                }
                Ok(None) => {}
                Err(err) => {
                    let mut cleanup_errors = Vec::new();
                    if let Err(cleanup_err) = terminate_validation_process_tree(&mut child) {
                        cleanup_errors.push(format!("terminate process tree: {cleanup_err}"));
                    }
                    if let Err(cleanup_err) = child.wait() {
                        cleanup_errors.push(format!("wait for child: {cleanup_err}"));
                    }
                    if let Err(cleanup_err) = collect_child_pipes(stdout_reader, stderr_reader) {
                        cleanup_errors.push(format!("collect child output: {cleanup_err}"));
                    }
                    if cleanup_errors.is_empty() {
                        return Err(err);
                    }
                    return Err(io::Error::new(
                        err.kind(),
                        format!(
                            "{err}; cleanup after child wait failure also failed: {}",
                            cleanup_errors.join("; ")
                        ),
                    ));
                }
            }
            if started.elapsed() >= timeout {
                terminate_validation_process_tree(&mut child)?;
                child.wait()?;
                let (stdout, stderr, stdout_truncated, stderr_truncated) =
                    collect_child_pipes(stdout_reader, stderr_reader)?;
                return Ok(CommandOutcome {
                    success: false,
                    timed_out: true,
                    stdout,
                    stderr,
                    stdout_truncated,
                    stderr_truncated,
                });
            }
            thread::sleep(Duration::from_millis(10));
        }
    }
}

#[cfg(unix)]
fn terminate_validation_process_tree(child: &mut std::process::Child) -> io::Result<()> {
    let process_group = rustix::process::Pid::from_child(child);
    match rustix::process::kill_process_group(process_group, rustix::process::Signal::KILL) {
        Ok(()) => Ok(()),
        Err(_) => child.kill(),
    }
}

#[cfg(windows)]
fn terminate_validation_process_tree(child: &mut std::process::Child) -> io::Result<()> {
    let status = Command::new("taskkill")
        .args(["/PID", &child.id().to_string(), "/T", "/F"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();
    match status {
        Ok(status) if status.success() => Ok(()),
        Ok(_) | Err(_) => child.kill(),
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CapturedOutput {
    bytes: Vec<u8>,
    truncated: bool,
}

fn read_child_pipe(
    mut pipe: impl Read + Send + 'static,
    max_output_bytes: usize,
) -> JoinHandle<io::Result<CapturedOutput>> {
    thread::spawn(move || {
        let mut bytes = Vec::new();
        let mut limited = pipe.by_ref().take(
            u64::try_from(max_output_bytes)
                .unwrap_or(u64::MAX)
                .saturating_add(1),
        );
        limited.read_to_end(&mut bytes)?;
        let truncated = bytes.len() > max_output_bytes;
        if truncated {
            bytes.truncate(max_output_bytes);
            io::copy(&mut pipe, &mut io::sink())?;
        }
        Ok(CapturedOutput { bytes, truncated })
    })
}

fn collect_child_pipes(
    stdout_reader: JoinHandle<io::Result<CapturedOutput>>,
    stderr_reader: JoinHandle<io::Result<CapturedOutput>>,
) -> io::Result<(String, String, bool, bool)> {
    let stdout = collect_child_pipe(stdout_reader)?;
    let stderr = collect_child_pipe(stderr_reader)?;
    Ok((
        String::from_utf8_lossy(&stdout.bytes).into_owned(),
        String::from_utf8_lossy(&stderr.bytes).into_owned(),
        stdout.truncated,
        stderr.truncated,
    ))
}

fn collect_child_pipe(
    reader: JoinHandle<io::Result<CapturedOutput>>,
) -> io::Result<CapturedOutput> {
    reader
        .join()
        .map_err(|_| io::Error::other("child output reader thread panicked"))?
}