Rust PTY Output Hangs When Trying to Read Command Output in Terminal Emulator

I'm working on creating a terminal emulator in Rust, and I'm currently experimenting with pseudo-terminals (PTY) and trying to capture the output of shell commands executed within the PTY. However, I'm encountering an issue where the program hangs when trying to read the output from the PTY, and no output is printed to the terminal.

Here's the relevant part of my code:

use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use std::io::Write;
use std::process::Command;
use anyhow::Result;
use anyhow::Error;

pub fn pyt() -> Result<(), Error> {

    // Use the native pty implementation for the system
    let pty_system = native_pty_system();
    
    // Create a new pty
    let mut pair = pty_system.openpty(PtySize {
        rows: 24,
        cols: 80,
        // Not all systems support pixel_width, pixel_height,
        // but it is good practice to set it to something
        // that matches the size of the selected font.  That
        // is more complex than can be shown here in this
        // brief example though!
        pixel_width: 0,
        pixel_height: 0,
    })?;
    
    // Spawn a shell into the pty
    let cmd = CommandBuilder::new("bash");
    let child = pair.slave.spawn_command(cmd)?;
    
    // Read and parse output from the pty with reader
    let mut reader = pair.master.try_clone_reader()?;
    
    // Send data to the pty by writing to the master
    writeln!(pair.master.take_writer()?, "dir\r\n")?;

    //output from the pty
    let mut output = String::new();
    reader.read_to_string(&mut output)?;
    println!("output: {}", output);

    Ok(())
}

I'm using the portable_pty crate to handle the PTY interactions. The code is supposed to:

  1. Create a new PTY.
  2. Spawn a bash shell into the PTY.
  3. Write the command dir to the PTY.
  4. Read and print the output from the PTY.

However, when I run this function, it hangs at the point of reading the output (reader.read_to_string(&mut output)), and nothing gets printed to the console. I'm not sure why it's not capturing the output from the dir command or why it hangs.

What I've Tried:

  • Ensuring that the dir command is correctly written to the PTY.
  • Verifying that the bash shell is spawning correctly within the PTY.

My Question:
How can I correctly read the output from the PTY without causing the program to hang? Is there a specific technique or method in Rust for handling PTY output asynchronously or non-blocking?

Also cross-posted on Stack Overflow.

When asking questions, please be so kind and indicate cross-posts, so there isn’t a chance that people answering your question would need to come up with answers that were already given elsewhere. It’s the least you can do to value everyone’s time. Furthermore, seeing replies others have already given can allow someone to write better answers themself, it allows elaborating on or correcting things others have said, and it ensures all the context from things like questions directed at the OP, etc… is present.

1 Like

TLDR (only works if you are on Windows):

the workaround is don't use read_to_string() (or rather anything that wait for the end of stream), just use regular read(). also, you probably want to offload the reading of the pipe to another thread, or use some async runtime, because the pseudo terminal pipes are not being closed even the child process closes all the corresponding stdout and stderr.

some explanation

if you are on Windows, I guess it's probably related to a long standing issue with Microsoft's conpty API and it's implementation, that is, the pty handle and the IO handles are not directly tied together in terms of life cycles. on the one hand, if the terminal emulator closes the input end of the pipes, the stdin file handle of the child process doesn't get closed; on the other hand, when the child process closes the stdout and stderr handles, or even if all child processes exits, the output end of the pseudo terminal pipe is still open.

possible related issues:

I think this will be fixed in future Windows updates, (or maybe they already fixed it, I'm not sure, cause it's most likely only fixed for Windows 11, and Windows 10 is stuck with faulty implementation. e.g. the API ConptyReleasePseudoConsole is in the github repo but not in MSDN or the SDK yet. currently you have to workaround this.

if your task is just to monitor child process live or dead status, you can just wait [1] on the handle returned by CreateProcessW when you spawn the child process. however, if you need to handle situations where child process voluntarily closes the stdout/stderr handle but keeps running without terminating, the only method I know of is to use a shim or proxy program that execute the real program but intercepts stdin/stdout/stderr and talks to the parent process (i.e. the terminal emulator) via some out of band communications.


  1. e.g. WaitForSingleObject β†©οΈŽ

1 Like

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.