use std::{process::ExitStatus, sync::Arc};
use tokio::{
io::{AsyncBufReadExt, AsyncRead, BufReader},
task::JoinHandle,
};
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputKind {
Stdout,
Stderr,
}
#[derive(Debug, Clone)]
pub struct OutputLine {
pub kind: OutputKind,
pub line: String,
}
impl OutputLine {
pub fn is_stderr(&self) -> bool {
self.kind == OutputKind::Stderr
}
}
pub type OutputFn = Arc<dyn Fn(OutputLine) + Send + Sync + 'static>;
pub fn no_output() -> OutputFn {
Arc::new(|_| {})
}
pub struct GameProcess {
child: tokio::process::Child,
pid: Option<u32>,
readers: Vec<JoinHandle<()>>,
}
impl GameProcess {
pub(crate) fn new(mut child: tokio::process::Child, sink: OutputFn) -> Self {
let pid = child.id();
let mut readers = Vec::new();
if let Some(stdout) = child.stdout.take() {
readers.push(spawn_pump(stdout, OutputKind::Stdout, sink.clone()));
}
if let Some(stderr) = child.stderr.take() {
readers.push(spawn_pump(stderr, OutputKind::Stderr, sink));
}
Self { child, pid, readers }
}
pub fn id(&self) -> Option<u32> {
self.pid
}
pub async fn wait(&mut self) -> Result<ExitStatus> {
let status = self.child.wait().await?;
for reader in self.readers.drain(..) {
let _ = reader.await;
}
Ok(status)
}
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
Ok(self.child.try_wait()?)
}
pub async fn kill(&mut self) -> Result<()> {
self.child.kill().await?;
Ok(())
}
pub fn child_mut(&mut self) -> &mut tokio::process::Child {
&mut self.child
}
}
fn spawn_pump<R>(reader: R, kind: OutputKind, sink: OutputFn) -> JoinHandle<()>
where
R: AsyncRead + Unpin + Send + 'static,
{
tokio::spawn(async move {
let mut buf = BufReader::new(reader);
let mut bytes = Vec::new();
loop {
bytes.clear();
match buf.read_until(b'\n', &mut bytes).await {
Ok(0) | Err(_) => break,
Ok(_) => {
while matches!(bytes.last(), Some(b'\n') | Some(b'\r')) {
bytes.pop();
}
let line = String::from_utf8_lossy(&bytes).into_owned();
sink(OutputLine { kind, line });
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[tokio::test]
async fn pump_splits_lines_and_strips_crlf() {
let data: &[u8] = b"first\r\nsecond\nthird-no-newline";
let collected = Arc::new(Mutex::new(Vec::new()));
let sink_store = collected.clone();
let sink: OutputFn = Arc::new(move |l: OutputLine| {
sink_store.lock().unwrap().push(l.line);
});
spawn_pump(data, OutputKind::Stdout, sink).await.unwrap();
let lines = collected.lock().unwrap().clone();
assert_eq!(lines, vec!["first", "second", "third-no-newline"]);
}
#[tokio::test]
async fn pump_survives_invalid_utf8() {
let data: &[u8] = b"ok\n\xff\xfe bad\n";
let collected = Arc::new(Mutex::new(Vec::new()));
let sink_store = collected.clone();
let sink: OutputFn = Arc::new(move |l: OutputLine| {
sink_store.lock().unwrap().push(l.line);
});
spawn_pump(data, OutputKind::Stderr, sink).await.unwrap();
let lines = collected.lock().unwrap().clone();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "ok");
assert!(lines[1].ends_with(" bad"));
}
}