rustipcinterprocess

Alternative to stdin/stdout for communicating with a child process?


I am spawning a child process that I control the internal code of and need bidirectional communication between the child and host processes.

Right now I have a simple implementation that has the host send JSON to the child via stdin and receives responses as JSON via stdout.

static CHILD_CODE: &str = r#"
  /* To parent */
  function send_to_host(data) {
    process.stdout.write(JSON.stringify(data))
    process.stdout.write('\n')
  }

  /* From parent */
  process.stdin.on('data', data => {
    const parsed = JSON.parse(data)
    send_to_host(parsed)
  })

  process.stdin.on('end', () => {
    process.exit(0)
  });

  send_to_host({ baz: 'buz' })
"#;

fn main() {
  let mut command = Command::new("node");
  command.args(["-e", &format!("eval(atob('{}'))", BASE64_STANDARD.encode(CHILD_CODE))]);
  command.stdout(Stdio::piped());
  command.stdin(Stdio::piped());

  let mut child = command.spawn().unwrap();
  let child_stdout = child.stdout.take().unwrap();
  let mut child_stdin = child.stdin.take().unwrap();

  thread::spawn(move || {
    let mut reader = BufReader::new(child_stdout);
    loop {
      let mut buff = String::new();
      let Ok(result) = reader.read_line(&mut buff) else {
        println!("Error reading line");
        continue;
      };
      if result == 0 {
          break;
      }
      // From child
      print!("From Child: {}", String::from(buff));
    }
  });

  // To child
  let msg = r#"{ "foo": "bar" }"#;
  child_stdin.write_all(msg.as_bytes()).unwrap();

  child.wait().unwrap();
}

While this works, I'd like to preserve the child's ability to output to the terminal directly so I am looking for an alternative approach but it also needs to be performant.

Initially I was thinking about having the child talk to an http server on the host process - but that has the overhead of a network stack, but also I have to manage port numbers and add authorization to avoid interception.

I'd eventually like to send a more space/parse/write efficient format between the processes (like bson or protobuf), so communications would need to support binary data.

It also needs to work on Windows.

I am unfamiliar with other types of IPC outside of stdio and network communications - can I get some suggestions on alternatives to learn about?


Solution

  • Sockets and named pipes are the most commonly used IPC mechanisms outside std in/out streams and http(s).

    You will need to consider if the security situation on the target systems makes that the best choice for you, or if you're better off with the overhead of a lightweight embedded http server, since most environments are more forgiving if you communicate over http(s).

    Below is an example of how to achieve TCP socket communication (which is a good choice if one or all of your systems are running Windows).

    main.rs:

    mod client;
    mod server;
    
    fn main() {
        // check if --server was passed on the command line
        let args: Vec<String> = std::env::args().collect();
        if args.len() > 1 && args[1] == "--server" {
            server::run().expect("Some error occurred");
            return;
        } else {
            // if not, run the client
            client::run().expect("Some error occurred");
            return;
        }
    }
    

    server.rs:

    use std::io::Read;
    use std::net::{TcpListener, TcpStream};
    use std::thread;
    
    fn handle_client(mut stream: TcpStream) {
        let mut buffer = [0; 128];
        match stream.read(&mut buffer) {
            Ok(_) => {
                let msg = String::from_utf8_lossy(&buffer);
                println!("Received: {}", msg);
            }
            Err(e) => println!("Error: {}", e),
        }
    }
    
    pub fn run() -> std::io::Result<()> {
        let listener = TcpListener::bind("127.0.0.1:5842")?;
    
        for stream in listener.incoming() {
            match stream {
                Ok(stream) => {
                    thread::spawn(|| handle_client(stream));
                }
                Err(err) => {
                    println!("Error: {}", err);
                    break;
                }
            }
        }
        Ok(())
    }
    

    And client.rs:

    use std::io::Write;
    use std::net::TcpStream;
    
    pub fn run() -> std::io::Result<()> {
        let mut stream = TcpStream::connect("127.0.0.1:5842")?;
        stream.write_all(b"Hello from client!")?;
        Ok(())
    }