ssh_cli/vps/exec_ops/
single.rs1#![forbid(unsafe_code)]
4#![allow(unused_imports)]
5use super::*;
6
7pub async fn run_exec(
12 selection: HostSelection,
13 command: &str,
14 config_override: Option<PathBuf>,
15 format: OutputFormat,
16 json: bool,
17 mut opts: ExecOptions,
18) -> Result<()> {
19 if crate::signals::should_stop() {
20 return Err(cancelled_err());
21 }
22 if selection.is_batch() {
23 return run_exec_all(
24 &selection,
25 command,
26 config_override,
27 format,
28 json,
29 opts,
30 ExecKind::Plain,
31 )
32 .await;
33 }
34 let vps_name = expect_single(selection)?;
35 let target = crate::json_wire::ExecTarget::new(vps_name.clone(), opts.target_source);
38 let path = resolve_config_path(config_override.as_deref())?;
39 let mut file = load(&path)?;
40 let mut vps = file
42 .hosts
43 .remove(&vps_name)
44 .ok_or(SshCliError::VpsNotFound(vps_name))?;
45 crate::json_wire::set_resolved_target(&target);
49
50 apply_overrides(&mut vps, opts.take_auth_overrides());
51 let cmd = append_description(command, opts.description.as_deref());
52 validate_command_length(&cmd, vps.max_command_chars.wire())?;
53 for s in &opts.steps {
54 validate_command_length(s.as_str(), vps.max_command_chars.wire())?;
55 }
56 let cfg = build_connection_config(&vps, Some(&path), opts.replace_host_key);
57 let client: Box<dyn SshClientTrait> = <SshClient as SshClientTrait>::connect(cfg).await?;
58 run_exec_with_client_steps(&vps, &cmd, &opts.steps, client, format, json, &target).await
59}
60
61pub async fn run_exec_with_client(
63 vps: &VpsRecord,
64 command: &str,
65 client: Box<dyn SshClientTrait>,
66 format: OutputFormat,
67 json: bool,
68) -> Result<()> {
69 let target =
70 crate::json_wire::ExecTarget::new(vps.name.as_str(), crate::json_wire::TargetSource::Argv);
71 run_exec_with_client_steps(vps, command, &[], client, format, json, &target).await
72}
73
74pub(crate) struct PreparedStep {
81 label: String,
83 packed: PackedCommand,
85}
86
87impl PreparedStep {
88 pub(crate) fn plain(command: &str) -> Self {
90 Self {
91 label: command.to_owned(),
92 packed: PackedCommand {
93 command: command.to_owned(),
94 stdin: None,
95 },
96 }
97 }
98
99 pub(crate) fn packed(label: &str, packed: PackedCommand) -> Self {
101 Self {
102 label: label.to_owned(),
103 packed,
104 }
105 }
106}
107
108pub(crate) fn step_labels(command: &str, steps: &[crate::domain::RemoteCommand]) -> Vec<String> {
110 let mut out = Vec::with_capacity(1 + steps.len());
111 out.push(command.to_owned());
112 out.extend(steps.iter().map(|s| s.as_str().to_owned()));
113 out
114}
115
116pub(crate) async fn run_prepared_steps(
129 vps: &VpsRecord,
130 steps: Vec<PreparedStep>,
131 mut client: Box<dyn SshClientTrait>,
132 format: OutputFormat,
133 json: bool,
134 target: &crate::json_wire::ExecTarget,
135) -> Result<()> {
136 if crate::signals::should_stop() {
137 return Err(cancelled_err());
138 }
139 let max_out = effective_limit(vps.max_output_chars.wire());
140 let as_json = format == OutputFormat::Json || json;
141 let multi = steps.len() > 1;
142 let mut last_output: Option<ExecutionOutput> = None;
143 let mut failed: Option<(i32, String)> = None;
144 for (i, mut step) in steps.into_iter().enumerate() {
145 if crate::signals::should_stop() {
146 let _ = client.disconnect().await;
147 return Err(cancelled_err());
148 }
149 tracing::debug!(step = i, "exec multi-cmd step");
150 let stdin = step.packed.take_stdin();
152 match client
153 .run_command(&step.packed.command, max_out, stdin)
154 .await
155 {
156 Ok(output) => {
157 if let Some(code) = output.exit_code {
158 if code != 0 && failed.is_none() {
159 failed = Some((code, output.stderr.clone()));
160 }
161 if i == 0 && code == 127 {
173 let _ = client.disconnect().await;
174 return Err(SshCliError::CommandFailed {
175 exit_code: code,
176 stderr: output.stderr,
177 }
178 .into());
179 }
180 }
181 if multi && as_json {
184 let mut v = serde_json::to_value(crate::json_wire::ExecutionJson::with_target(
185 &output, target,
186 ))
187 .unwrap_or_else(|_| serde_json::json!({}));
188 if let Some(obj) = v.as_object_mut() {
189 obj.insert("step".into(), serde_json::json!(i));
190 obj.insert("command".into(), serde_json::json!(step.label));
191 }
192 crate::output::print_json_value(&v)?;
193 } else if multi {
194 crate::output::write_line_fmt(format_args!(
195 "--- step {i}: {} ---",
196 step.label
197 ))?;
198 crate::output::print_execution_output(&output);
199 } else {
200 last_output = Some(output);
201 }
202 }
203 Err(e) => {
204 let _ = client.disconnect().await;
205 return Err(e.into());
206 }
207 }
208 }
209 let _ = client.disconnect().await;
210 if let Some(output) = last_output {
213 if as_json {
214 crate::output::print_execution_output_json(&output, target)?;
215 } else {
216 crate::output::print_execution_output(&output);
217 }
218 }
219 if let Some((code, stderr)) = failed {
220 return Err(SshCliError::CommandFailed {
221 exit_code: code,
222 stderr,
223 }
224 .into());
225 }
226 Ok(())
227}
228
229pub async fn run_exec_with_client_steps(
231 vps: &VpsRecord,
232 command: &str,
233 steps: &[crate::domain::RemoteCommand],
234 client: Box<dyn SshClientTrait>,
235 format: OutputFormat,
236 json: bool,
237 target: &crate::json_wire::ExecTarget,
238) -> Result<()> {
239 let prepared = step_labels(command, steps)
240 .iter()
241 .map(|c| PreparedStep::plain(c))
242 .collect();
243 run_prepared_steps(vps, prepared, client, format, json, target).await
244}