traceviewer 0.1.0

Run a command and inspect its logs in a scrollable terminal viewer
use std::{
    io::{self, BufRead, BufReader},
    process::{Child, Command, Stdio},
    sync::{
        Arc, Mutex,
        mpsc::{self, Receiver, Sender},
    },
    thread,
};

use anyhow::{Context, Result, anyhow};

use crate::model::{AppEvent, Stream};

pub(crate) struct RunningCommand {
    pub(crate) events: Receiver<AppEvent>,
    child: Arc<Mutex<Child>>,
}

impl RunningCommand {
    pub(crate) fn terminate(&self) {
        let Ok(mut child) = self.child.lock() else {
            return;
        };

        if matches!(child.try_wait(), Ok(Some(_))) {
            return;
        }

        let _ = child.kill();
    }
}

pub(crate) fn spawn_command(command: &[String]) -> Result<RunningCommand> {
    let (program, args) = command
        .split_first()
        .ok_or_else(|| anyhow!("missing command"))?;

    let mut child = Command::new(program)
        .args(args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .with_context(|| format!("failed to spawn `{program}`"))?;

    let stdout = child.stdout.take().context("failed to capture stdout")?;
    let stderr = child.stderr.take().context("failed to capture stderr")?;

    let (tx, rx) = mpsc::channel();
    spawn_reader(Stream::Stdout, stdout, tx.clone());
    spawn_reader(Stream::Stderr, stderr, tx.clone());

    let child = Arc::new(Mutex::new(child));
    let waiter_child = Arc::clone(&child);
    thread::spawn(move || {
        let event = wait_for_child(waiter_child);
        let _ = tx.send(event);
    });

    Ok(RunningCommand { events: rx, child })
}

fn wait_for_child(child: Arc<Mutex<Child>>) -> AppEvent {
    loop {
        let status = match child.lock() {
            Ok(mut child) => child.try_wait(),
            Err(err) => {
                return AppEvent::ReaderFailed(Stream::Stderr, format!("wait lock failed: {err}"));
            }
        };

        match status {
            Ok(Some(status)) => return AppEvent::ProcessExited(status),
            Ok(None) => thread::sleep(std::time::Duration::from_millis(50)),
            Err(err) => {
                return AppEvent::ReaderFailed(Stream::Stderr, format!("wait failed: {err}"));
            }
        }
    }
}

fn spawn_reader<R>(stream: Stream, reader: R, tx: Sender<AppEvent>)
where
    R: io::Read + Send + 'static,
{
    thread::spawn(move || {
        let reader = BufReader::new(reader);
        for line in reader.lines() {
            match line {
                Ok(line) => {
                    if tx.send(AppEvent::Line(stream, line)).is_err() {
                        break;
                    }
                }
                Err(err) => {
                    let _ = tx.send(AppEvent::ReaderFailed(stream, err.to_string()));
                    break;
                }
            }
        }
    });
}