runcc 2.0.3

run commands concurrently with rust and cargo
Documentation
use std::fmt::Display;
use std::sync::{Arc, Mutex};
use tokio::io::AsyncBufReadExt;
use tokio::{io::BufReader, task::JoinHandle};

use crate::run::{kill, CommandStopped, CommandSystemPlugin, LabeledCommandData};

pub struct CommandSystemLogPlugin(Mutex<Vec<JoinHandle<()>>>);

impl CommandSystemLogPlugin {
    pub fn new() -> Self {
        Self(Default::default())
    }
}

impl CommandSystemPlugin<LabeledCommandData> for CommandSystemLogPlugin {
    type CommandInitialData = LabeledCommandData;

    fn initialize_command_data(
        &self,
        data: Self::CommandInitialData,
        stdout: tokio::process::ChildStdout,
        stderr: tokio::process::ChildStderr,
    ) -> LabeledCommandData {
        let label = data.label.display().to_string();

        let join = tokio::spawn(async move {
            tokio::join!(
                async {
                    let stdout = BufReader::new(stdout);
                    let mut lines = stdout.lines();
                    loop {
                        match lines.next_line().await {
                            Ok(line) => {
                                if let Some(line) = line {
                                    #[cfg(feature = "auto_ansi_escape")]
                                    let line = crate::ansi_escape::process_ansi_escape_line(
                                        label.len() + 3,
                                        &line,
                                    );

                                    let line = format!("[{}] {}", label, line);
                                    println!("{}", line);
                                } else {
                                    break;
                                }
                            }
                            Err(err) => {
                                eprintln!(
                                    "[runcc error] failed to read line from [{}] stdout: {}",
                                    label, err
                                );
                                break;
                            }
                        }
                    }
                },
                async {
                    let stderr = BufReader::new(stderr);
                    let mut lines = stderr.lines();
                    loop {
                        match lines.next_line().await {
                            Ok(line) => {
                                if let Some(line) = line {
                                    #[cfg(feature = "auto_ansi_escape")]
                                    let line = crate::ansi_escape::process_ansi_escape_line(
                                        label.len() + 3,
                                        &line,
                                    );

                                    let line = format!("[{}] {}", label, line);
                                    eprintln!("{}", line);
                                } else {
                                    break;
                                }
                            }
                            Err(err) => {
                                eprintln!(
                                    "[runcc error] failed to read line from [{}] stderr: {}",
                                    label, err
                                );
                                break;
                            }
                        }
                    }
                }
            );
        });

        let mut joins = self.0.lock().unwrap();
        joins.push(join);

        data
    }

    fn on_command_exited(&self, cmd: Arc<CommandStopped<LabeledCommandData, LabeledCommandData>>) {
        let label = cmd.data.label.display();
        let status = &cmd.exit_status;
        let killed = &cmd.killed;
        let status = match status {
            Ok(s) => format!(
                "code {}",
                s.code()
                    .map_or_else(|| "None".to_string(), |code| format!("{}", code))
            ),
            Err(err) => format!("error: {}", err),
        };

        let killed: std::borrow::Cow<str> = match killed {
            Some(kill_status) => {
                use crate::run::kill::KillJoinHandleFinalStatus as KS;
                match kill_status {
                    KS::Killed(reason) => format!(" (killed due to {})", reason).into(),
                    KS::FailedToKill { reason, error } => {
                        format!(" (tried to kill due to {} but failed: {})", reason, error).into()
                    }
                    _ => "".into(),
                }
            }
            None => "".into(),
        };

        let line = format!("[{}] exited with status {}{}", label, status, killed);
        eprintln!("{}", line);
    }

    fn join(&self) -> Option<tokio::task::JoinHandle<()>> {
        let mut joins = self.0.lock().unwrap();

        let mut joins: Vec<_> = joins.drain(0..).collect();

        Some(tokio::spawn(async move {
            for join in joins.iter_mut() {
                let _ = join.await;
            }
        }))
    }

    fn initialize_spawn_failed_command_data(
        &self,
        data: Self::CommandInitialData,
    ) -> LabeledCommandData {
        data
    }
}

impl Display for kill::KillCommandReason<LabeledCommandData> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            kill::KillCommandReason::OtherCommandExited(cmd) => {
                write!(f, "command[{}] exited", cmd.data.label.label())
            }
            kill::KillCommandReason::MainProcessGotSignal => write!(f, "Ctrl-C signal"),
        }
    }
}