#![forbid(unsafe_code)]
use super::{
apply_overrides, build_connection_config, load, resolve_config_path, resolve_host_jobs,
use_json, HostSelection,
};
use crate::cli::OutputFormat;
use crate::errors::SshCliError;
use crate::output;
use crate::ssh::client::{SshClient, SshClientTrait};
use anyhow::Result;
use secrecy::SecretString;
use std::path::PathBuf;
#[allow(clippy::too_many_arguments)]
pub async fn run_health_check(
selection: HostSelection,
config_override: Option<PathBuf>,
format: OutputFormat,
json_local: bool,
password_override: Option<SecretString>,
timeout_override: Option<crate::domain::TimeoutMs>,
key_override: Option<String>,
key_passphrase_override: Option<SecretString>,
replace_host_key: bool,
) -> Result<()> {
if json_local || format == OutputFormat::Json {
crate::output::set_json_errors(true);
}
if crate::signals::should_stop() {
return Err(anyhow::anyhow!(crate::i18n::t(
crate::i18n::Message::OperationCancelled
)));
}
if selection.is_batch() {
return run_health_check_all(
&selection,
config_override,
format,
json_local,
password_override,
timeout_override,
key_override,
key_passphrase_override,
replace_host_key,
)
.await;
}
let HostSelection::Single(resolved_name) = selection else {
return Err(SshCliError::InvalidArgument(
"internal: expected single-host selection for non-batch health-check".into(),
)
.into());
};
let resolved_key = resolved_name.as_str().to_owned();
let path = resolve_config_path(config_override.as_deref())?;
let mut file = load(&path)?;
let mut vps = file
.hosts
.remove(&resolved_key)
.ok_or_else(|| SshCliError::VpsNotFound(resolved_key.clone()))?;
apply_overrides(
&mut vps,
password_override,
None,
None,
timeout_override,
key_override,
key_passphrase_override,
false,
None,
);
let cfg = build_connection_config(&vps, Some(&path), replace_host_key);
let start = std::time::Instant::now();
let client: Box<dyn SshClientTrait> = <SshClient as SshClientTrait>::connect(cfg).await?;
let latency_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
client.disconnect().await?;
if use_json(json_local, format) {
output::print_health_check_json(&resolved_key, latency_ms)?;
} else {
output::print_health_check(&resolved_key, latency_ms);
}
Ok(())
}
pub(super) async fn collect_health_check_batch(
selection: &HostSelection,
config_override: Option<PathBuf>,
) -> Result<(Vec<HostHealthResult>, usize)> {
collect_health_check_batch_with_opts(
selection,
config_override,
None,
None,
None,
None,
false,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn collect_health_check_batch_with_opts(
selection: &HostSelection,
config_override: Option<PathBuf>,
password_override: Option<SecretString>,
timeout_override: Option<crate::domain::TimeoutMs>,
key_override: Option<String>,
key_passphrase_override: Option<SecretString>,
replace_host_key: bool,
) -> Result<(Vec<HostHealthResult>, usize)> {
let path = resolve_config_path(config_override.as_deref())?;
let file = load(&path)?;
let jobs = resolve_host_jobs(selection, &file)?;
let limit = crate::concurrency::effective_limit();
let path_c = path.clone();
tracing::info!(
hosts = jobs.len(),
max_concurrency = limit,
"multi-host health-check fan-out"
);
let pw = password_override;
let to = timeout_override;
let key = key_override;
let kp = key_passphrase_override;
let results = crate::concurrency::map_bounded(jobs, limit, move |(name, mut vps)| {
let path_c = path_c.clone();
let pw = pw.clone();
let key = key.clone();
let kp = kp.clone();
async move {
if crate::signals::should_stop() {
return HostHealthResult {
name,
ok: false,
latency_ms: None,
error: Some("operation cancelled by signal".into()),
};
}
apply_overrides(&mut vps, pw, None, None, to, key, kp, false, None);
let start = std::time::Instant::now();
let cfg = build_connection_config(&vps, Some(&path_c), replace_host_key);
match <SshClient as SshClientTrait>::connect(cfg).await {
Ok(client) => {
let latency_ms =
u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
let _ = client.disconnect().await;
HostHealthResult {
name,
ok: true,
latency_ms: Some(latency_ms),
error: None,
}
}
Err(e) => HostHealthResult {
name,
ok: false,
latency_ms: Some(
u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
),
error: Some(e.to_string()),
},
}
}
})
.await;
let mut host_results = Vec::with_capacity(results.len());
for r in results {
match r.outcome {
Ok(h) => host_results.push(h),
Err(e) if e.is_panic() => std::panic::resume_unwind(e.into_panic()),
Err(e) => {
host_results.push(HostHealthResult {
name: format!("task-{}", r.index),
ok: false,
latency_ms: None,
error: Some(e.to_string()),
});
}
}
}
Ok((host_results, limit))
}
#[allow(clippy::too_many_arguments)]
async fn run_health_check_all(
selection: &HostSelection,
config_override: Option<PathBuf>,
format: OutputFormat,
json_local: bool,
password_override: Option<SecretString>,
timeout_override: Option<crate::domain::TimeoutMs>,
key_override: Option<String>,
key_passphrase_override: Option<SecretString>,
replace_host_key: bool,
) -> Result<()> {
let (host_results, limit) = collect_health_check_batch_with_opts(
selection,
config_override,
password_override,
timeout_override,
key_override,
key_passphrase_override,
replace_host_key,
)
.await?;
let failures = host_results.iter().filter(|h| !h.ok).count();
let as_json = use_json(json_local, format);
output::print_health_batch(&host_results, limit, as_json)?;
if failures > 0 {
return Err(SshCliError::Config(format!(
"{failures}/{} hosts failed health-check",
host_results.len()
))
.into());
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct HostHealthResult {
pub name: String,
pub ok: bool,
pub latency_ms: Option<u64>,
pub error: Option<String>,
}