Create new child shell?

Hello everyone !

I'm trying to spawn a new shell (bash or others) inside my rust program to then interact with it through std in and out.

Here's what I have for now :

let mut shell = Command::new("sh")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .spawn()
            .expect("");

        let mut stdin = shell.stdin.unwrap();
        let mut stdout = shell.stdout.unwrap();

But then my rust program hangs (instead of continuing and giving me the ability to interact with it). It works fine if the command is something else that stops running quickly (like "ls")

How are you reading from stdout or writing to stdin?

By using Stdio::piped(), it is now your program's responsibility to pass input from the terminal to sh and then pass any output from sh's stdout back to your terminal. Not passing this input/output back and forth means sh will have no way of knowing what you enter in the terminal or having its output displayed to the user, so for all intents and purposes it'll look like it's hanging.

You could use Stdio::inherit() to ask the OS to directly wire up sh's STDOUT and STDIN streams to the terminal (or whatever your Rust program is connected to), but then because the information is no longer going through your Rust code you won't be able to inspect it.

The thing is I DO NOT WANT to use the default terminal. I want this to be a completely isolated terminal that my rust program has the control of, reading info ONLY when it needs with stdout.read_to_string(), to get all the NEW output. The thing is, I don't know how to make this in a "non-blocking" way, so my rust program doesn't hang when I invoke "sh". It should launch it in the background and I should be able to just get the output from it, to then render it in some different way, not directly onto the terminal.

For now, my program looks like this : https://imgur.com/a/osXjwiw
What I would like to have is one terminal running inside each window separately.

And I know it's hanging, because if I kill the command right after launching it, I have the windows displayed (but no text inside). If I do not I don't even get to the windows.

So essentially Rust version of pexpect? You may want to check out rexpect to see how they interactively control a subprocess from Rust.

The code you originally posted will spawn sh in the background and give you stdin and stdout streams that can be used to write to the subprocess's input or read generated output, so that's not a problem... It sounds like whatever you do next is blocking the entire system. For example, you try to read from stdout, but sh never generated any output so the kernel puts your program to sleep until sh does something.

There are a couple ways to get around this. You could use asynchronous IO (see tokio::process), or you can spawn a pair of threads in the background that do the blocking reads/writes and coordinate them using channels. How do you do non-blocking console I/O on Linux in C? mentions some approaches you could try, although you'd need to find the Rust equivalents (the nix crate might be a good starting point).

Alternatively, if you don't want to take those approaches and are looking for an easy fix, you could avoid using stdout.read_to_string(). The read_to_string() method will try to keep reading until it reaches an EOF, but because that'll only happen when the subprocess dies or it closes stdout, you'll end up blocking forever. You may have some success by wrapping stdout in a std::io::BufReader and reading one line at a time, but it won't fix the underlying problem that trying to read when there is no more output will block.

I'm a little confused... Starting sh just begins a new process in the background, it doesn't create a new terminal window or render anything to the screen. Are you talking about starting up a new terminal emulator window and communicating with that?

1 Like

Yes, kind of.
I think my problem lies in the blocking read from stdout. I'm going to try to fix that.

Ok I managed to do that with the crossbeam crate and SegQueues. Now, I would like to input things into the running sh comand. What I currently have is this piece of code

stdin.write("echo hello\n".as_bytes());

but I doesn't show any output to the windows (while if I run "ls" instead of "sh", it outputs the result of "ls").
Any ideas about how to fix that ?

Tried adding stdin.flush(), doesn't make a difference

I just played around with the tokio variant of Command a bit ⟶here.

3 Likes

Found out why my code wasn't working : turns out, the read_to_string method, well... wasn't reading to a string. I replaced my code with this :

let lines: Vec<String> = vec![];
        let mut shell = Command::new("sh")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .spawn()
            .unwrap();
        let mut stdin = shell.stdin.take().unwrap();
        let queue: Arc<SegQueue<String>> = Arc::new(SegQueue::new());
        let q = queue.clone();
        let h = thread::spawn(move || {
            let mut shellout = shell.stdout.take().unwrap();
            loop {
                let mut buf = [0; 1024];
                let r = shellout.read(&mut buf);
                let n = r.unwrap();
                let str = str::from_utf8(&buf[..n]).unwrap().to_string();
                q.deref().push(str.clone());
            }
        });
        stdin.write_all(b"ls\n").unwrap();
        stdin.write_all(b"echo ---\n").unwrap();
        stdin.write_all(b"ls -a\n").unwrap();

And now everything works as expected, new strings are pushed to the seg queue, from where I can read in a non-blocking way by checking the length

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.