#![forbid(unsafe_code)]
#![allow(unused_imports)]
use super::*;
pub async fn run_exec(
selection: HostSelection,
command: &str,
config_override: Option<PathBuf>,
format: OutputFormat,
json: bool,
mut opts: ExecOptions,
) -> Result<()> {
if crate::signals::should_stop() {
return Err(cancelled_err());
}
if selection.is_batch() {
return run_exec_all(
&selection,
command,
config_override,
format,
json,
opts,
ExecKind::Plain,
)
.await;
}
let vps_name = expect_single(selection)?;
let target = crate::json_wire::ExecTarget::new(vps_name.clone(), opts.target_source);
let path = resolve_config_path(config_override.as_deref())?;
let mut file = load(&path)?;
let mut vps = file
.hosts
.remove(&vps_name)
.ok_or(SshCliError::VpsNotFound(vps_name))?;
crate::json_wire::set_resolved_target(&target);
apply_overrides(&mut vps, opts.take_auth_overrides());
let cmd = append_description(command, opts.description.as_deref());
validate_command_length(&cmd, vps.max_command_chars.wire())?;
for s in &opts.steps {
validate_command_length(s.as_str(), vps.max_command_chars.wire())?;
}
let cfg = build_connection_config(&vps, Some(&path), opts.replace_host_key);
let client: Box<dyn SshClientTrait> = <SshClient as SshClientTrait>::connect(cfg).await?;
run_exec_with_client_steps(&vps, &cmd, &opts.steps, client, format, json, &target).await
}
pub async fn run_exec_with_client(
vps: &VpsRecord,
command: &str,
client: Box<dyn SshClientTrait>,
format: OutputFormat,
json: bool,
) -> Result<()> {
let target =
crate::json_wire::ExecTarget::new(vps.name.as_str(), crate::json_wire::TargetSource::Argv);
run_exec_with_client_steps(vps, command, &[], client, format, json, &target).await
}
pub(crate) struct PreparedStep {
label: String,
packed: PackedCommand,
}
impl PreparedStep {
pub(crate) fn plain(command: &str) -> Self {
Self {
label: command.to_owned(),
packed: PackedCommand {
command: command.to_owned(),
stdin: None,
},
}
}
pub(crate) fn packed(label: &str, packed: PackedCommand) -> Self {
Self {
label: label.to_owned(),
packed,
}
}
}
pub(crate) fn step_labels(command: &str, steps: &[crate::domain::RemoteCommand]) -> Vec<String> {
let mut out = Vec::with_capacity(1 + steps.len());
out.push(command.to_owned());
out.extend(steps.iter().map(|s| s.as_str().to_owned()));
out
}
pub(crate) async fn run_prepared_steps(
vps: &VpsRecord,
steps: Vec<PreparedStep>,
mut client: Box<dyn SshClientTrait>,
format: OutputFormat,
json: bool,
target: &crate::json_wire::ExecTarget,
) -> Result<()> {
if crate::signals::should_stop() {
return Err(cancelled_err());
}
let max_out = effective_limit(vps.max_output_chars.wire());
let as_json = format == OutputFormat::Json || json;
let multi = steps.len() > 1;
let mut last_output: Option<ExecutionOutput> = None;
let mut failed: Option<(i32, String)> = None;
for (i, mut step) in steps.into_iter().enumerate() {
if crate::signals::should_stop() {
let _ = client.disconnect().await;
return Err(cancelled_err());
}
tracing::debug!(step = i, "exec multi-cmd step");
let stdin = step.packed.take_stdin();
match client
.run_command(&step.packed.command, max_out, stdin)
.await
{
Ok(output) => {
if let Some(code) = output.exit_code {
if code != 0 && failed.is_none() {
failed = Some((code, output.stderr.clone()));
}
if i == 0 && code == 127 {
let _ = client.disconnect().await;
return Err(SshCliError::CommandFailed {
exit_code: code,
stderr: output.stderr,
}
.into());
}
}
if multi && as_json {
let mut v = serde_json::to_value(crate::json_wire::ExecutionJson::with_target(
&output, target,
))
.unwrap_or_else(|_| serde_json::json!({}));
if let Some(obj) = v.as_object_mut() {
obj.insert("step".into(), serde_json::json!(i));
obj.insert("command".into(), serde_json::json!(step.label));
}
crate::output::print_json_value(&v)?;
} else if multi {
crate::output::write_line_fmt(format_args!(
"--- step {i}: {} ---",
step.label
))?;
crate::output::print_execution_output(&output);
} else {
last_output = Some(output);
}
}
Err(e) => {
let _ = client.disconnect().await;
return Err(e.into());
}
}
}
let _ = client.disconnect().await;
if let Some(output) = last_output {
if as_json {
crate::output::print_execution_output_json(&output, target)?;
} else {
crate::output::print_execution_output(&output);
}
}
if let Some((code, stderr)) = failed {
return Err(SshCliError::CommandFailed {
exit_code: code,
stderr,
}
.into());
}
Ok(())
}
pub async fn run_exec_with_client_steps(
vps: &VpsRecord,
command: &str,
steps: &[crate::domain::RemoteCommand],
client: Box<dyn SshClientTrait>,
format: OutputFormat,
json: bool,
target: &crate::json_wire::ExecTarget,
) -> Result<()> {
let prepared = step_labels(command, steps)
.iter()
.map(|c| PreparedStep::plain(c))
.collect();
run_prepared_steps(vps, prepared, client, format, json, target).await
}