use super::config::ConnectionConfig;
use super::core::SshClient;
use super::result::CommandResult;
use crate::security::SudoPassword;
use crate::ssh::known_hosts::StrictHostKeyChecking;
use crate::ssh::tokio_client::CommandOutput;
use anyhow::{Context, Result};
use std::path::Path;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
const DEFAULT_COMMAND_TIMEOUT_SECS: u64 = 300;
impl SshClient {
pub async fn connect_and_execute(
&mut self,
command: &str,
key_path: Option<&Path>,
use_agent: bool,
) -> Result<CommandResult> {
self.connect_and_execute_with_host_check(command, key_path, None, use_agent, false, None)
.await
}
pub async fn connect_and_execute_with_host_check(
&mut self,
command: &str,
key_path: Option<&Path>,
strict_mode: Option<StrictHostKeyChecking>,
use_agent: bool,
use_password: bool,
timeout_seconds: Option<u64>,
) -> Result<CommandResult> {
let config = ConnectionConfig {
key_path,
strict_mode,
use_agent,
use_password,
#[cfg(target_os = "macos")]
use_keychain: false, timeout_seconds,
connect_timeout_seconds: None, jump_hosts_spec: None, ssh_connection_config: None,
};
self.connect_and_execute_with_jump_hosts(command, &config)
.await
}
pub async fn connect_and_execute_with_jump_hosts(
&mut self,
command: &str,
config: &ConnectionConfig<'_>,
) -> Result<CommandResult> {
tracing::debug!("Connecting to {}:{}", self.host, self.port);
let auth_method = self
.determine_auth_method(
config.key_path,
config.use_agent,
config.use_password,
#[cfg(target_os = "macos")]
config.use_keychain,
)
.await?;
let strict_mode = config
.strict_mode
.unwrap_or(StrictHostKeyChecking::AcceptNew);
let client = self
.establish_connection(
&auth_method,
strict_mode,
config.jump_hosts_spec,
config.key_path,
config.use_agent,
config.use_password,
config.connect_timeout_seconds,
config.ssh_connection_config,
)
.await?;
tracing::debug!("Connected and authenticated successfully");
tracing::debug!("Executing command: {}", command);
let result = self
.execute_with_timeout(&client, command, config.timeout_seconds)
.await?;
tracing::debug!(
"Command execution completed with status: {}",
result.exit_status
);
Ok(CommandResult {
host: self.host.clone(),
output: result.stdout.into_bytes(),
stderr: result.stderr.into_bytes(),
exit_status: result.exit_status,
})
}
async fn execute_with_timeout(
&self,
client: &crate::ssh::tokio_client::Client,
command: &str,
timeout_seconds: Option<u64>,
) -> Result<crate::ssh::tokio_client::CommandExecutedResult> {
if let Some(timeout_secs) = timeout_seconds {
if timeout_secs == 0 {
tracing::debug!("Executing command with no timeout (unlimited)");
client.execute(command)
.await
.with_context(|| format!("Failed to execute command '{}' on {}:{}. The SSH connection was successful but the command could not be executed.", command, self.host, self.port))
} else {
let command_timeout = Duration::from_secs(timeout_secs);
tracing::debug!("Executing command with timeout of {} seconds", timeout_secs);
tokio::time::timeout(
command_timeout,
client.execute(command)
)
.await
.with_context(|| format!("Command execution timeout: The command '{}' did not complete within {} seconds on {}:{}", command, timeout_secs, self.host, self.port))?
.with_context(|| format!("Failed to execute command '{}' on {}:{}. The SSH connection was successful but the command could not be executed.", command, self.host, self.port))
}
} else {
let command_timeout = Duration::from_secs(DEFAULT_COMMAND_TIMEOUT_SECS);
tracing::debug!("Executing command with default timeout of 300 seconds");
tokio::time::timeout(
command_timeout,
client.execute(command)
)
.await
.with_context(|| format!("Command execution timeout: The command '{}' did not complete within 5 minutes on {}:{}", command, self.host, self.port))?
.with_context(|| format!("Failed to execute command '{}' on {}:{}. The SSH connection was successful but the command could not be executed.", command, self.host, self.port))
}
}
pub async fn connect_and_execute_with_output_streaming(
&mut self,
command: &str,
config: &ConnectionConfig<'_>,
output_sender: Sender<CommandOutput>,
) -> Result<u32> {
tracing::debug!("Connecting to {}:{}", self.host, self.port);
let auth_method = self
.determine_auth_method(
config.key_path,
config.use_agent,
config.use_password,
#[cfg(target_os = "macos")]
config.use_keychain,
)
.await?;
let strict_mode = config
.strict_mode
.unwrap_or(StrictHostKeyChecking::AcceptNew);
let client = self
.establish_connection(
&auth_method,
strict_mode,
config.jump_hosts_spec,
config.key_path,
config.use_agent,
config.use_password,
config.connect_timeout_seconds,
config.ssh_connection_config,
)
.await?;
tracing::debug!("Connected and authenticated successfully");
tracing::debug!("Executing command with streaming: {}", command);
let exit_status = self
.execute_streaming_with_timeout(&client, command, config.timeout_seconds, output_sender)
.await?;
tracing::debug!("Command execution completed with status: {}", exit_status);
Ok(exit_status)
}
async fn execute_streaming_with_timeout(
&self,
client: &crate::ssh::tokio_client::Client,
command: &str,
timeout_seconds: Option<u64>,
output_sender: Sender<CommandOutput>,
) -> Result<u32> {
if let Some(timeout_secs) = timeout_seconds {
if timeout_secs == 0 {
tracing::debug!("Executing command with streaming, no timeout (unlimited)");
client.execute_streaming(command, output_sender)
.await
.with_context(|| format!("Failed to execute command '{}' on {}:{}. The SSH connection was successful but the command could not be executed.", command, self.host, self.port))
} else {
let command_timeout = Duration::from_secs(timeout_secs);
tracing::debug!(
"Executing command with streaming, timeout of {} seconds",
timeout_secs
);
tokio::time::timeout(
command_timeout,
client.execute_streaming(command, output_sender)
)
.await
.with_context(|| format!("Command execution timeout: The command '{}' did not complete within {} seconds on {}:{}", command, timeout_secs, self.host, self.port))?
.with_context(|| format!("Failed to execute command '{}' on {}:{}. The SSH connection was successful but the command could not be executed.", command, self.host, self.port))
}
} else {
let command_timeout = Duration::from_secs(DEFAULT_COMMAND_TIMEOUT_SECS);
tracing::debug!("Executing command with streaming, default timeout of 300 seconds");
tokio::time::timeout(
command_timeout,
client.execute_streaming(command, output_sender)
)
.await
.with_context(|| format!("Command execution timeout: The command '{}' did not complete within 5 minutes on {}:{}", command, self.host, self.port))?
.with_context(|| format!("Failed to execute command '{}' on {}:{}. The SSH connection was successful but the command could not be executed.", command, self.host, self.port))
}
}
pub async fn connect_and_execute_with_sudo(
&mut self,
command: &str,
config: &ConnectionConfig<'_>,
output_sender: Sender<CommandOutput>,
sudo_password: &SudoPassword,
) -> Result<u32> {
tracing::debug!(
"Connecting to {}:{} for sudo execution",
self.host,
self.port
);
let auth_method = self
.determine_auth_method(
config.key_path,
config.use_agent,
config.use_password,
#[cfg(target_os = "macos")]
config.use_keychain,
)
.await?;
let strict_mode = config
.strict_mode
.unwrap_or(StrictHostKeyChecking::AcceptNew);
let client = self
.establish_connection(
&auth_method,
strict_mode,
config.jump_hosts_spec,
config.key_path,
config.use_agent,
config.use_password,
config.connect_timeout_seconds,
config.ssh_connection_config,
)
.await?;
tracing::debug!("Connected and authenticated successfully");
tracing::debug!("Executing command with sudo support: {}", command);
let exit_status = self
.execute_sudo_with_timeout(
&client,
command,
config.timeout_seconds,
output_sender,
sudo_password,
)
.await?;
tracing::debug!("Command execution completed with status: {}", exit_status);
Ok(exit_status)
}
async fn execute_sudo_with_timeout(
&self,
client: &crate::ssh::tokio_client::Client,
command: &str,
timeout_seconds: Option<u64>,
output_sender: Sender<CommandOutput>,
sudo_password: &SudoPassword,
) -> Result<u32> {
if let Some(timeout_secs) = timeout_seconds {
if timeout_secs == 0 {
tracing::debug!("Executing sudo command with no timeout (unlimited)");
client
.execute_with_sudo(command, output_sender, sudo_password)
.await
.with_context(|| {
format!(
"Failed to execute sudo command '{}' on {}:{}",
command, self.host, self.port
)
})
} else {
let command_timeout = Duration::from_secs(timeout_secs);
tracing::debug!(
"Executing sudo command with timeout of {} seconds",
timeout_secs
);
tokio::time::timeout(
command_timeout,
client.execute_with_sudo(command, output_sender, sudo_password)
)
.await
.with_context(|| format!("Command execution timeout: The sudo command '{}' did not complete within {} seconds on {}:{}", command, timeout_secs, self.host, self.port))?
.with_context(|| format!("Failed to execute sudo command '{}' on {}:{}", command, self.host, self.port))
}
} else {
let command_timeout = Duration::from_secs(DEFAULT_COMMAND_TIMEOUT_SECS);
tracing::debug!("Executing sudo command with default timeout of 300 seconds");
tokio::time::timeout(
command_timeout,
client.execute_with_sudo(command, output_sender, sudo_password)
)
.await
.with_context(|| format!("Command execution timeout: The sudo command '{}' did not complete within 5 minutes on {}:{}", command, self.host, self.port))?
.with_context(|| format!("Failed to execute sudo command '{}' on {}:{}", command, self.host, self.port))
}
}
}