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?