Create a program passing (and modifying) stdin to a process while printing (modified) stdout

I'm new to Rust and want to create a program for interacting with chess engines via ssh. The point is that a chess engine uses the UCI protocol basically accepting commands like "go" and "stop" and printing analysis continuously. I found this post "Russh just pipe stdin and stdout to remote host" for connecting via ssh and just passing stdin and stdout without editing, but have no idea how you would make that work after reading that thread. Actually I managed to make it work in Python, but it took me "a lot" of time, so I would appreciate if someone just knows how to do it. :slight_smile:
In Python it looks like this:

import sys
import time
import subprocess
import threading


class ChessEngine:

    def __init__(self):
        self.lock = threading.Lock()

    def write(self, command):
        with self.lock:
            self.engine.stdin.write(command + "\n")
            self.engine.stdin.flush()

    def read_stdout(self):
        while True:
            text = self.engine.stdout.readline()
            text = text.replace("foo", "bar")
            print(text, end="", flush=True)

    def read_stdin(self):
        while True:
            command = input()
            command = command.replace("foo", "bar")
            self.write(command)

    def run(self):
        self.engine = subprocess.Popen(
            "/usr/games/stockfish",
            shell=True,
            universal_newlines=True,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            bufsize=1,
            cwd=self.folder,
        )
        threading.Thread(target=self.read_stdout, daemon=True).start()
        threading.Thread(target=self.read_stdin, daemon=True).start()
        while True:
            time.sleep(1)
            if not self.engine.poll() is None:
                sys.exit(0)


if __name__ == "__main__":
    chess_engine = ChessEngine()
    chess_engine.run()

It's mostly the same, but with more error handling. There's much that can be done to improve performance, but I don't think UCI messages are very large, so this is probably sufficient.

use std::io::{self, BufRead};
use std::process::{self, ChildStdin, ChildStdout, ExitCode, Stdio};

struct ChessEngine {
    engine: process::Child,
    engine_stdin: ChildStdin,
    engine_stdout: ChildStdout,
}

impl ChessEngine {
    fn new() -> io::Result<Self> {
        let mut engine = process::Command::new("/usr/games/stockfish")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::null())
            .spawn()?;
        let engine_stdin = engine.stdin.take().unwrap();
        let engine_stdout = engine.stdout.take().unwrap();

        Ok(Self {
            engine,
            engine_stdin,
            engine_stdout,
        })
    }

    fn write_stdout(engine_stdout: ChildStdout) -> io::Result<()> {
        for line in io::BufReader::new(engine_stdout).lines() {
            let line = line?;
            let line = line.replace("foo", "bar");

            println!("{line}");
        }
        Ok(())
    }

    fn read_stdin(mut engine_stdin: ChildStdin) -> io::Result<()> {
        for line in io::stdin().lines() {
            let line = line?;
            let line = line.replace("foo", "bar");

            use io::Write;
            writeln!(engine_stdin, "{line}")?;
            engine_stdin.flush()?;
        }
        Ok(())
    }

    fn run(mut self) -> io::Result<process::ExitStatus> {
        std::thread::scope(|scope| {
            let stdout_thread = scope.spawn(|| Self::write_stdout(self.engine_stdout));
            let stdin_thread = scope.spawn(|| Self::read_stdin(self.engine_stdin));

            let exit = self.engine.wait()?;
            stdout_thread.join().unwrap()?;
            stdin_thread.join().unwrap()?;
            Ok(exit)
        })
    }
}

fn main() -> ExitCode {
    let chess_engine = ChessEngine::new().unwrap();
    let exit = chess_engine.run().unwrap();
    ExitCode::from(exit.code().unwrap_or(0).try_into().unwrap_or(1))
}
2 Likes

Thank you, thank you, thank you! :smiley:
I removed the

stdout_thread.join().unwrap()?;
stdin_thread.join().unwrap()?;

part, because it produces a broken pipe error if the program quits, but I guess the "scope" already ensures the threads are joined properly.
As I said, thank you so much, that helped a lot!

(and I'm happy I can ask questions at a place other than the commercial Stackoverflow :smiley:)

1 Like

For those who are interested in the final code, this is it:

This is my first (and not last! :slight_smile: ) program in Rust. Thanks again, drewtato, for the tremendous help!

1 Like