use std::io::IsTerminal;
use std::process::ExitCode;
use anyhow::Result;
use bssh::cli::{
Cli, Commands, PdshCli, has_pdsh_compat_flag, is_pdsh_compat_mode, remove_pdsh_compat_flag,
};
use bssh::commands::ping::PING_SSH_LEVEL_FAILURE;
use bssh::hostlist;
use clap::Parser;
use glob::Pattern;
mod app;
#[cfg(unix)]
use app::background;
#[cfg(unix)]
use app::dispatcher::requires_background_supervision;
use app::{
background::BackgroundWorker,
cache::handle_cache_stats,
config_dump::handle_config_dump,
dispatcher::dispatch_command_with_background,
initialization::{AppContext, initialize_app},
query::{handle_query, is_supported_query},
utils::show_usage,
};
#[tokio::main]
async fn main() -> ExitCode {
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
bssh::diagnosticln!("Error: {error:?}");
ExitCode::FAILURE
}
}
}
async fn run() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let pdsh_mode = is_pdsh_compat_mode() || has_pdsh_compat_flag(&args);
if pdsh_mode {
return run_pdsh_mode(&args).await;
}
if bssh::cli::SshDumpInvocation::requests_config_dump(&args) {
if let Some(path) = bssh::cli::SshDumpInvocation::diagnostic_file(&args)
&& let Err(error) = bssh::utils::diagnostics::set_log_file(&path)
{
bssh::diagnosticln!("Error: {error:?}");
std::process::exit(255);
}
let invocation = match bssh::cli::SshDumpInvocation::from_argv(&args) {
Ok(invocation) => invocation,
Err(error) => {
bssh::diagnosticln!("Error: {error:?}");
std::process::exit(255);
}
};
if invocation.version {
eprintln!("bssh_{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if let Some(query) = invocation.query.as_deref() {
if !is_supported_query(query) {
bssh::diagnosticln!("Unsupported query \"{query}\"");
std::process::exit(255);
}
handle_query(query);
return Ok(());
}
if let Err(error) = handle_config_dump(&invocation).await {
bssh::diagnosticln!("Error: {error:?}");
std::process::exit(255);
}
return Ok(());
}
run_bssh_mode(&args).await
}
async fn dispatch_and_exit(
cli: &Cli,
ctx: &AppContext,
background_worker: Option<&BackgroundWorker>,
) -> Result<()> {
match dispatch_command_with_background(cli, ctx, background_worker).await {
Ok(0) => Ok(()),
Ok(exit_code) => std::process::exit(exit_code),
Err(e) => Err(map_hard_failure(&cli.command, cli.is_ssh_mode(), e)),
}
}
fn is_ssh_client_failure(error: &anyhow::Error) -> bool {
error.chain().any(|cause| {
cause
.downcast_ref::<bssh::ssh::tokio_client::Error>()
.is_some_and(bssh::ssh::tokio_client::Error::is_ssh_client_failure)
})
}
fn map_hard_failure(
command: &Option<Commands>,
ssh_mode: bool,
error: anyhow::Error,
) -> anyhow::Error {
if matches!(command, Some(Commands::Ping)) {
bssh::diagnosticln!("Error: {error:?}");
std::process::exit(PING_SSH_LEVEL_FAILURE);
}
if ssh_mode && is_ssh_client_failure(&error) {
bssh::diagnosticln!("{error:#}");
std::process::exit(255);
}
error
}
async fn run_pdsh_mode(args: &[String]) -> Result<()> {
let filtered_args = if has_pdsh_compat_flag(args) {
remove_pdsh_compat_flag(args)
} else {
args.to_vec()
};
let pdsh_cli = PdshCli::parse_from(filtered_args.iter());
if pdsh_cli.is_query_mode() {
return handle_pdsh_query_mode(&pdsh_cli).await;
}
let mut cli = pdsh_cli.to_bssh_cli();
bssh::ui::configure_color(cli.color);
if cli.hosts.is_none() {
bssh::diagnosticln!("Error: No hosts specified. Use -w to specify target hosts.");
bssh::diagnosticln!("Usage: pdsh -w hosts command");
std::process::exit(1);
}
if cli.command_args.is_empty() {
bssh::diagnosticln!("Error: No command specified.");
bssh::diagnosticln!("Usage: pdsh -w hosts command");
std::process::exit(1);
}
let ctx = initialize_app(&mut cli, args).await?;
dispatch_and_exit(&cli, &ctx, None).await
}
async fn handle_pdsh_query_mode(pdsh_cli: &PdshCli) -> Result<()> {
if let Some(ref hosts_str) = pdsh_cli.hosts {
let hosts: Vec<String> = hostlist::expand_host_specs(hosts_str)
.map_err(|e| anyhow::anyhow!("Failed to expand host expression: {e}"))?;
let (expanded_exclusions, glob_exclusions): (Vec<String>, Vec<Pattern>) = if let Some(
ref exclude_str,
) =
pdsh_cli.exclude
{
let mut expanded = Vec::new();
let mut globs = Vec::new();
for pattern in exclude_str.split(',').map(|s| s.trim()) {
const MAX_PATTERN_LENGTH: usize = 256;
if pattern.len() > MAX_PATTERN_LENGTH {
anyhow::bail!(
"Exclusion pattern too long (max {MAX_PATTERN_LENGTH} characters)"
);
}
if pattern.is_empty() {
continue;
}
if hostlist::is_hostlist_expression(pattern) {
let expanded_hosts = hostlist::expand_host_specs(pattern)
.map_err(|e| anyhow::anyhow!("Failed to expand exclusion pattern: {e}"))?;
expanded.extend(expanded_hosts);
} else {
let wildcard_count = pattern.chars().filter(|c| *c == '*' || *c == '?').count();
const MAX_WILDCARDS: usize = 10;
if wildcard_count > MAX_WILDCARDS {
anyhow::bail!(
"Exclusion pattern contains too many wildcards (max {MAX_WILDCARDS})"
);
}
match Pattern::new(pattern) {
Ok(p) => globs.push(p),
Err(_) => {
anyhow::bail!("Invalid exclusion pattern: {pattern}");
}
}
}
}
(expanded, globs)
} else {
(Vec::new(), Vec::new())
};
let exclusion_set: std::collections::HashSet<&str> =
expanded_exclusions.iter().map(|s| s.as_str()).collect();
for host in &hosts {
let is_excluded_by_hostlist = exclusion_set.contains(host.as_str());
let is_excluded_by_glob = glob_exclusions.iter().any(|pattern| {
let pattern_str = pattern.as_str();
if !pattern_str.contains('*')
&& !pattern_str.contains('?')
&& !pattern_str.contains('[')
{
host == pattern_str || host.contains(pattern_str)
} else {
pattern.matches(host)
}
});
if !is_excluded_by_hostlist && !is_excluded_by_glob {
println!("{host}");
}
}
} else {
bssh::diagnosticln!("Error: No hosts specified for query mode.");
bssh::diagnosticln!("Usage: pdsh -w hosts -q");
std::process::exit(1);
}
Ok(())
}
async fn run_bssh_mode(args: &[String]) -> Result<()> {
if args.len() == 1 {
show_usage();
std::process::exit(0);
}
let mut cli = Cli::parse_from(args);
let effective_args = if cli.is_ssh_mode() {
bssh::cli::normalize_ssh_option_pass(
args,
cli.destination.as_deref().unwrap_or_default(),
cli.command_args.len(),
)
} else {
args.to_vec()
};
if effective_args != args {
cli = Cli::parse_from(&effective_args);
}
bssh::utils::diagnostics::set_quiet_warnings(cli.quiet);
let background_worker = BackgroundWorker::from_environment()?;
if background_worker.is_none() && cli.log_file.is_none() && std::io::stderr().is_terminal() {
for warning in cli.short_flag_migration_warnings(&effective_args) {
bssh::warningln!("{warning}");
}
}
bssh::ui::configure_color(cli.color);
if cli.version {
eprintln!("bssh_{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if let Some(path) = &cli.log_file {
bssh::utils::diagnostics::set_log_file(path)?;
}
if let Some(ref query) = cli.query {
handle_query(query);
return Ok(());
}
if matches!(cli.command, Some(Commands::List))
|| (cli.is_multi_server_mode() && cli.destination.as_deref() == Some("list"))
{
let config = bssh::config::Config::load_with_priority(&cli.config).await?;
bssh::commands::list::list_clusters(&config);
return Ok(());
}
if let Some(Commands::CacheStats {
detailed,
clear,
maintain,
}) = &cli.command
{
handle_cache_stats(*detailed, *clear, *maintain).await;
return Ok(());
}
#[cfg(not(unix))]
if cli.is_ssh_mode() && cli.fork_after_authentication {
anyhow::bail!("-f background-after-authentication currently requires Unix");
}
let init_result = initialize_app(&mut cli, &effective_args).await;
let ctx = match init_result {
Ok(ctx) => ctx,
Err(e) => return Err(map_hard_failure(&cli.command, cli.is_ssh_mode(), e)),
};
#[cfg(unix)]
if background_worker.is_none() && requires_background_supervision(&cli, &ctx)? {
let exit_code = background::supervise(&effective_args)
.await
.map_err(|error| map_hard_failure(&cli.command, true, error))?;
if exit_code == 0 {
return Ok(());
}
std::process::exit(exit_code);
}
dispatch_and_exit(&cli, &ctx, background_worker.as_ref()).await
}