use std::sync::Arc;
use std::time::{Duration, Instant};
use russh::ChannelMsg;
use tokio::sync::Notify;
use tokio::time::{Instant as TokioInstant, sleep_until};
use crate::errors::{Result, SshError};
use crate::session::Session;
#[derive(Debug, Clone)]
pub struct ExecResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub duration_ms: u128,
pub stdout_bytes: usize,
pub stderr_bytes: usize,
pub timed_out: bool,
pub capture_capped: bool,
pub connection_lost: bool,
pub interrupted: bool,
}
pub async fn exec(
session: &Session,
cmd: &str,
deadline: Duration,
max_capture: usize,
) -> Result<ExecResult> {
let (channel, _permit, from_pool) = session.take_or_open_channel().await?;
let first = exec_on_channel(session, channel, cmd, deadline, max_capture).await;
let retry = match &first {
Ok(r) => {
from_pool
&& r.connection_lost
&& r.stdout_bytes == 0
&& r.stderr_bytes == 0
&& !r.timed_out
&& !r.interrupted
}
Err(_) => from_pool,
};
if !retry {
return first;
}
tracing::debug!("pooled channel was stale; retrying exec on a fresh channel");
let channel = session
.handle
.channel_open_session()
.await
.map_err(SshError::from)?;
exec_on_channel(session, channel, cmd, deadline, max_capture).await
}
async fn exec_on_channel(
session: &Session,
mut channel: russh::Channel<russh::client::Msg>,
cmd: &str,
deadline: Duration,
max_capture: usize,
) -> Result<ExecResult> {
let start = Instant::now();
channel.exec(false, cmd).await.map_err(SshError::from)?;
let stdout_cap = max_capture.min(16 * 1024);
let stderr_cap = max_capture.min(2 * 1024);
let mut stdout = Vec::with_capacity(stdout_cap);
let mut stderr = Vec::with_capacity(stderr_cap);
let mut exit_code: Option<i32> = None;
let mut timed_out = false;
let mut capture_capped = false;
let mut interrupted = false;
let cancel = Arc::new(Notify::new());
let cancel_id = session.register_exec(Arc::clone(&cancel)).await;
let sleep = sleep_until(TokioInstant::now() + deadline);
tokio::pin!(sleep);
let mut close_seen = false;
loop {
tokio::select! {
biased;
_ = cancel.notified() => {
interrupted = true;
let _ = channel.close().await;
break;
}
_ = &mut sleep => {
timed_out = true;
let _ = channel.close().await;
break;
}
msg = channel.wait() => {
match msg {
None => break,
Some(ChannelMsg::Data { ref data }) => append_capped(&mut stdout, data, max_capture, &mut capture_capped),
Some(ChannelMsg::ExtendedData { ref data, ext }) => {
if ext == 1 {
append_capped(&mut stderr, data, max_capture, &mut capture_capped);
} else {
append_capped(&mut stdout, data, max_capture, &mut capture_capped);
}
}
Some(ChannelMsg::ExitStatus { exit_status }) => {
exit_code = Some(exit_status as i32);
if close_seen { break; }
}
Some(ChannelMsg::Close) => {
close_seen = true;
if exit_code.is_some() { break; }
}
Some(ChannelMsg::Eof) => {
if exit_code.is_some() { break; }
}
Some(_) => {}
}
}
}
}
let connection_lost = close_seen && exit_code.is_none();
let stdout_bytes = stdout.len();
let stderr_bytes = stderr.len();
let duration_ms = start.elapsed().as_millis();
session.touch();
session.deregister_exec(cancel_id).await;
let final_exit = exit_code.unwrap_or(if interrupted {
130 } else if timed_out {
124
} else {
-1
});
Ok(ExecResult {
stdout: into_string_fast(stdout),
stderr: into_string_fast(stderr),
exit_code: final_exit,
duration_ms,
stdout_bytes,
stderr_bytes,
timed_out,
capture_capped,
connection_lost,
interrupted,
})
}
fn append_capped(buf: &mut Vec<u8>, data: &[u8], max: usize, capped: &mut bool) {
if buf.len() >= max {
*capped = true;
return;
}
let room = max - buf.len();
if data.len() <= room {
buf.extend_from_slice(data);
} else {
buf.extend_from_slice(&data[..room]);
*capped = true;
}
}
fn into_string_fast(bytes: Vec<u8>) -> String {
match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
}
}