honzasp / makiko

Asynchronous SSH client library in pure Rust
https://honzasp.github.io/makiko/
The Unlicense
39 stars 3 forks source link

shell execute empty stdout command will block the session receiver. #5

Closed h4cnull closed 1 year ago

h4cnull commented 1 year ago

When I start the shell and execute a command with no output, such as 'cat empty.txt,' the session event receiver gets blocked and doesn't return empty stdout data. ` let stream = TcpStream::connect("192.168.255.167:22").await?;

let config = makiko::ClientConfig::default_compatible_less_secure();

let (client, mut client_rx, client_fut) = makiko::Client::open(stream, config)?;
let h1 = tokio::spawn(async move {
    let e = client_fut.await;
});
let h2 = tokio::spawn(async move {
    loop {
        // Wait for the next event.
        let event = client_rx.recv().await;
        let Ok(Some(event)) = event else { break };
        match event {
            // Handle the server public key: for now, we just accept all keys, but this makes
            // us susceptible to man-in-the-middle attacks!
            makiko::ClientEvent::ServerPubkey(_pubkey, accept) => {
                // println!("Server pubkey type {}, fingerprint {}",pubkey.type_str(),pubkey.fingerprint());
                accept.accept();
            }
            makiko::ClientEvent::AuthBanner(b) => {
                println!("{:?}", b);
            }
            _ => {}
        }
    }
});
let r = client.auth_password("root".into(), "123456".into()).await;
println!("{:?}",r);
let channel_config = makiko::ChannelConfig::default();
let (session, mut session_rx) = client.open_session(channel_config).await?;
let session_wait = tokio::spawn(async move {
    loop {
        // Wait for the next event.
        let event = session_rx
            .recv()
            .await.unwrap();
        println!("{:?}",event);
        // Exit the loop when the session has closed.
        let Some(event) = event else {
            break;
        };
    }
});
let r  = session.shell().unwrap().ignore();
session.send_stdin(Bytes::copy_from_slice(b"ls -hl\n")).await?;
session.send_stdin(Bytes::copy_from_slice(b"echo test > test.txt\n")).await?;
session.send_stdin(Bytes::copy_from_slice(b"cat empty.txt\n")).await?;
let r = session_wait.await?;
println!("finished");`
honzasp commented 1 year ago

Hi, thank you for your interest in this library! :) When you execute a command that does not produce any stdout data, then there is no data to be received, so the SessionReceiver does not produce any event. The output from the session is a byte stream, and the boundaries between the chunks returned in SessionEvent::StdoutData are arbitrary.

Executing a shell is equivalent to executing any other program, so there is no guarantee that the individual chunks that you receive from stdout correspond to the commands executed in the shell. If you need to execute a sequence of commands and capture their output, you can open a new session for each command and use Session::exec() to execute the command directly, without using the shell.

h4cnull commented 1 year ago

This is not an issue. I understand this behavior now. When I request a shell, the SSH server actually starts a default shell program like Bash, which then waits for input commands. After execution, if there's no stdout data, Bash won't produce any output either. To address this, I should first request a pty (instead of directly requesting a shell without a pty), or a custom shell that echoes the command prompt or entered commands. Then, I need to handle the stdout data myself. Some shells might include both the input command and the command prompt in their output. By removing the command prompt and entered command, I'll obtain the actual command output (which might be empty).

honzasp commented 1 year ago

Hi, I'm glad that the behavior is more clear now! However, I would strongly suggest that if you need to execute multiple commands and capture their outputs, it will be much cleaner and more robust if you execute each command in its own session, rather than trying to parse the output from shell.