Using shell() from ssh2-rs without busy-wait-loop?

While I believe to understand the first sentence of this quote from ssh2/Session, I'm uncertain of how to understand the second sentence:

This means that a blocking read from a Channel or Stream will block all other calls on objects created from the same underlying Session. If you need the ability to perform concurrent operations then you will need to create separate Session instances, or employ non-blocking mode.

The first part makes sense. Every channel or stream passing through an SSH session gets multiplexed into the same TCP connection. When it comes to the second part, two suggestions are given. Non-blocking mode seems far from ideal. As for separate sessions, isn't that only usable with two completely independent Streams? Each Session requires exclusive mutable ownership of the TCP object, right? Or could one setup say three Sessions for stdin, stdout and stderr of the same Channel? Reading the API docs and all my cognitive attempts strongly leads me to believe one can not, but it seems like a basic enough use case that I suspect there might be something I'm missing?

In case the question becomes more clear with some example code, I wish to do something similar to this trivial interactive ssh client, but in a cleaner blocking fashion. Only executing when some traffic actually appears, avoiding wasting cycles like this. Does the API allow for that in some way that I fail to understand?

use { anyhow::Result, ssh2::Session, std::{ env::var, io::{ Read, Write, stdin, stdout, },
    net::TcpStream, thread, time::Duration, }, termion::{ raw::IntoRawMode, terminal_size, }, };

fn main() -> Result<()> {
    IntoRawMode::into_raw_mode(stdout())?;
    let mut sess = Session::new()?;
    sess.set_tcp_stream(TcpStream::connect("localhost:22")?);
    sess.handshake()?;
    sess.userauth_agent(&var("USER")?)?;
    let mut channel = sess.channel_session()?;
    let (width, height) = terminal_size()?;
    channel.request_pty("xterm", None, Some((width as u32, height as u32, 0, 0)))?;
    channel.shell()?;
    sess.set_blocking(false);
    let mut ssh_stdin = channel.stream(0);
    let mut stderr = channel.stderr();

    let stdin_thread = thread::spawn(move || {
        let mut stdin = stdin();
        let mut buf = [0; 1024];
        loop {
            let size = stdin.read(&mut buf).unwrap();
            if buf[0] == b'\x1d' { break; } // ^] To exit loop
            let _ = ssh_stdin.write_all(&buf[0..size]);
        }
    });

    let _stderr_thread = thread::spawn(move || {
        let mut buf = [0u8; 1024];
        loop { if stderr.read(&mut buf).is_err() { thread::sleep(Duration::from_millis(200)); } }
    });
    let stdout_thread = thread::spawn(move || {
        loop {
            let mut buf = [0u8; 1024];

            match channel.read(&mut buf) {
                Ok(c) if c > 0 => {
                    print!("{}", String::from_utf8_lossy(&buf[0..c]));
                    let _ = stdout().flush();
                }
                Ok(0) => break,
                _ => thread::sleep(Duration::from_millis(200)),
            }
        }
        channel.close().unwrap();
    });

    [ stdout_thread, stdin_thread ].into_iter().for_each(|t| { t.join(); } );
    Ok(())
}

This example could clearly be simplified to perform all three read+writes in a single loop. My attempts to obtain a more satisfactory solution has involved three threads, but the first blocked read() on any channel does indeed cause a deadlock.

I am aware of the existence of the alternative crates openssh and thrussh, but have not looked all too close at them since ssh2-rs initally seemed to be the best bet to go with. Maybe it is not?

(This is without experience and only glancing at docs.) wait_eof reads like it is just a wait for receiving stdin/stderr when not blocking. As it takes &mut both reads probably best on same thread.

Thanks for replying. To my understanding, the documentation for wait_eof() states it is used to determine whether a channel is still active or not. What I wish to do is to read and write the actual payload data of the channel.

If guessing wildly, the closest I've found to something having a remote chance of working is the method suggested in Issue #167 for solving a completely different problem. I do not fully understand it, and might just be confused, but could it be possible to use a socketpair to obtain blocking I/O? The datatype implements the AsRawFd trait required for set_tcp_stream(), and it seem one should be able to detect changes using calls to its C-style poll() function. Unfortunately I have not managed becoming successful when trying it.