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 path = resolve_config_path(config_override.as_deref())?;
36 let mut file = load(&path)?;
37 let mut vps = file
39 .hosts
40 .remove(&vps_name)
41 .ok_or(SshCliError::VpsNotFound(vps_name))?;
42
43 apply_overrides(&mut vps, opts.take_auth_overrides());
44 let cmd = append_description(command, opts.description.as_deref());
45 validate_command_length(&cmd, vps.max_command_chars.wire())?;
46 for s in &opts.steps {
47 validate_command_length(s.as_str(), vps.max_command_chars.wire())?;
48 }
49 let cfg = build_connection_config(&vps, Some(&path), opts.replace_host_key);
50 let client: Box<dyn SshClientTrait> = <SshClient as SshClientTrait>::connect(cfg).await?;
51 run_exec_with_client_steps(&vps, &cmd, &opts.steps, client, format, json).await
52}
53
54pub async fn run_exec_with_client(
56 vps: &VpsRecord,
57 command: &str,
58 client: Box<dyn SshClientTrait>,
59 format: OutputFormat,
60 json: bool,
61) -> Result<()> {
62 run_exec_with_client_steps(vps, command, &[], client, format, json).await
63}
64
65pub(crate) struct PreparedStep {
72 label: String,
74 packed: PackedCommand,
76}
77
78impl PreparedStep {
79 pub(crate) fn plain(command: &str) -> Self {
81 Self {
82 label: command.to_owned(),
83 packed: PackedCommand {
84 command: command.to_owned(),
85 stdin: None,
86 },
87 }
88 }
89
90 pub(crate) fn packed(label: &str, packed: PackedCommand) -> Self {
92 Self {
93 label: label.to_owned(),
94 packed,
95 }
96 }
97}
98
99pub(crate) fn step_labels(command: &str, steps: &[crate::domain::RemoteCommand]) -> Vec<String> {
101 let mut out = Vec::with_capacity(1 + steps.len());
102 out.push(command.to_owned());
103 out.extend(steps.iter().map(|s| s.as_str().to_owned()));
104 out
105}
106
107pub(crate) async fn run_prepared_steps(
114 vps: &VpsRecord,
115 steps: Vec<PreparedStep>,
116 mut client: Box<dyn SshClientTrait>,
117 format: OutputFormat,
118 json: bool,
119) -> Result<()> {
120 if crate::signals::should_stop() {
121 return Err(cancelled_err());
122 }
123 let max_out = effective_limit(vps.max_output_chars.wire());
124 let as_json = format == OutputFormat::Json || json;
125 let multi = steps.len() > 1;
126 let mut last_output: Option<ExecutionOutput> = None;
127 let mut failed: Option<(i32, String)> = None;
128 for (i, mut step) in steps.into_iter().enumerate() {
129 if crate::signals::should_stop() {
130 let _ = client.disconnect().await;
131 return Err(cancelled_err());
132 }
133 tracing::debug!(step = i, "exec multi-cmd step");
134 let stdin = step.packed.take_stdin();
136 match client
137 .run_command(&step.packed.command, max_out, stdin)
138 .await
139 {
140 Ok(output) => {
141 if let Some(code) = output.exit_code {
142 if code != 0 && failed.is_none() {
143 failed = Some((code, output.stderr.clone()));
144 }
145 }
146 if multi && as_json {
149 let mut v =
150 serde_json::to_value(crate::json_wire::ExecutionJson::from(&output))
151 .unwrap_or_else(|_| serde_json::json!({}));
152 if let Some(obj) = v.as_object_mut() {
153 obj.insert("step".into(), serde_json::json!(i));
154 obj.insert("command".into(), serde_json::json!(step.label));
155 }
156 crate::output::print_json_value(&v)?;
157 } else if multi {
158 crate::output::write_line_fmt(format_args!(
159 "--- step {i}: {} ---",
160 step.label
161 ))?;
162 crate::output::print_execution_output(&output);
163 } else {
164 last_output = Some(output);
165 }
166 }
167 Err(e) => {
168 let _ = client.disconnect().await;
169 return Err(e.into());
170 }
171 }
172 }
173 let _ = client.disconnect().await;
174 if let Some(output) = last_output {
177 if as_json {
178 crate::output::print_execution_output_json(&output)?;
179 } else {
180 crate::output::print_execution_output(&output);
181 }
182 }
183 if let Some((code, stderr)) = failed {
184 return Err(SshCliError::CommandFailed {
185 exit_code: code,
186 stderr,
187 }
188 .into());
189 }
190 Ok(())
191}
192
193pub async fn run_exec_with_client_steps(
195 vps: &VpsRecord,
196 command: &str,
197 steps: &[crate::domain::RemoteCommand],
198 client: Box<dyn SshClientTrait>,
199 format: OutputFormat,
200 json: bool,
201) -> Result<()> {
202 let prepared = step_labels(command, steps)
203 .iter()
204 .map(|c| PreparedStep::plain(c))
205 .collect();
206 run_prepared_steps(vps, prepared, client, format, json).await
207}