5

I have created a simple pty setup, however I am unsure on how to actually write to the master or slave sides once it is created. I am also unsure if my setup is correct, because upon inspection, the Stdin, Stdout, and Stderr of the child process for the pty are all None instead of being set to the slave file descriptor. Would anyone be able to clarify if this is correct and if not do you have any suggestion on how to fix it?

use libc::{self};
use nix::pty::openpty;
use std::os::unix::io::FromRawFd;
use std::process::{Child, Command, Stdio};

#[derive(Debug)]
pub struct Pty {
    process: Child,
    fd: i32,
}

fn create_pty(process: &str) -> Pty {
    let ends = openpty(None, None).expect("openpty failed");
    let master = ends.master;
    let slave = ends.slave;

    let mut builder = Command::new(process);
    builder.stdin(unsafe { Stdio::from_raw_fd(slave) });
    builder.stdout(unsafe { Stdio::from_raw_fd(slave) });
    builder.stderr(unsafe { Stdio::from_raw_fd(slave) });

    match builder.spawn() {
        Ok(process) => {
            let pty = Pty {
                process,
                fd: master,
            };

            pty
        }
        Err(e) => {
            panic!("Failed to create pty: {}", e);
        }
    }
}

fn main() {
    let shell = "/bin/bash";

    let pty = create_pty(shell);
    println!("{:?}", pty);

    println!("{}", pty.process.id());
}
Ethalot
  • 57
  • 6

1 Answers1

6

This is expected. std::process::Child::stdin and friends are not set for raw file handles (because Rust doesn't know what they are, the builder doesn't have the master end of your pty).

You can construct your rusty file handle for the master yourself:

fn main() {
    let shell = "/bin/bash";

    let pty = create_pty(shell);
    println!("{:?}", pty);

    let mut output = unsafe { File::from_raw_fd(pty.fd) };
    write!(output, "touch /tmp/itworks\n");
    output.flush();

    std::thread::sleep_ms(1000);

    println!("{}", pty.process.id());
}

You'll see that this does indeed create a file "/tmp/itworks".

(Permalink to the playground)

mcarton
  • 27,633
  • 5
  • 85
  • 95
  • So if I understand correctly, the reason that the stdout, stdin, and stderr register as none is because the ```std::process::Child``` type doesn't understand the raw file handlers I am passing into the .stdin() and others methods? Given that this is the case, would I be better off creating a file from the slave fd and operating with that instead? – Ethalot Feb 24 '20 at 07:13
  • 1
    Well, on your side, you should use the master, not the slave. `Child` doesn't know the master since you only pass raw handles to the slave (which is what the spawned process needs). – mcarton Feb 24 '20 at 11:03
  • Wouldn't `(Child).std*` being `None` be caused by the fact that new `Stdio` instances are being passed for the `std*` handles, instead of using `Stdio::piped()`? I was under the impression that using `Stdio::piped()` was required in order to access `std*` from the child. – Sean Kelleher May 17 '22 at 21:29