1#![forbid(unsafe_code)]
6
7use super::{is_quiet, report_json_serialize_error};
8use crate::domain::BatchRunId;
9use crate::json_wire::{
10 self, ExecBatchJson, ExecHostJson, HealthBatchJson, HealthHostJson, ScpBatchJson, ScpHostJson,
11 ScpTransferJson, SftpBatchJson, SftpFsOpJson, SftpListEntryJson, SftpListJson, SftpTransferJson,
12 TunnelListeningJson,
13};
14use crate::ssh::sftp_types::{SftpListEntry, SftpStat};
15use crate::sftp::batch::HostSftpResult;
16use crate::vps::{HostExecResult, HostHealthResult};
17use std::io::{self, Write};
18
19pub fn print_health_batch(
24 results: &[HostHealthResult],
25 max_concurrency: usize,
26 json: bool,
27) -> io::Result<()> {
28 if json {
29 let batch_run_id = BatchRunId::new().to_string_canonical();
31 let v = HealthBatchJson {
32 event: "health-check-batch".into(),
33 batch_run_id,
34 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
35 results: results
36 .iter()
37 .map(|h| HealthHostJson {
38 name: h.name.clone(),
39 status: if h.ok { "ok".into() } else { "error".into() },
40 latency_ms: h.latency_ms,
41 error: h.error.clone(),
42 })
43 .collect(),
44 };
45 return match json_wire::print_json_line(&v) {
46 Ok(()) => Ok(()),
47 Err(e) => {
48 report_json_serialize_error(&e);
49 Err(e)
50 }
51 };
52 }
53 if is_quiet() {
54 return Ok(());
55 }
56 let stdout = io::stdout();
57 let mut out = io::BufWriter::new(stdout.lock());
58 writeln!(
59 out,
60 "health-check --all (max_concurrency={max_concurrency}, hosts={})",
61 results.len()
62 )?;
63 for h in results {
64 match (h.ok, h.latency_ms) {
65 (true, Some(ms)) => writeln!(out, " ok {} {ms}ms", h.name)?,
66 (true, None) => writeln!(out, " ok {}", h.name)?,
67 (false, _) => {
68 let err = h.error.as_deref().unwrap_or("error");
69 writeln!(out, " ERR {} {err}", h.name)?;
70 }
71 }
72 }
73 out.flush()
74}
75
76pub fn print_exec_batch(
81 results: &[HostExecResult],
82 max_concurrency: usize,
83 json: bool,
84) -> io::Result<()> {
85 if json {
86 let batch_run_id = BatchRunId::new().to_string_canonical();
87 let v = ExecBatchJson {
88 event: "exec-batch".into(),
89 batch_run_id,
90 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
91 results: results
92 .iter()
93 .map(|h| ExecHostJson {
94 name: h.name.clone(),
95 ok: h.ok,
96 exit_code: h.exit_code,
97 stdout: h.stdout.clone(),
98 stderr: h.stderr.clone(),
99 duration_ms: h.duration_ms,
100 error: h.error.clone(),
101 })
102 .collect(),
103 };
104 return match json_wire::print_json_line(&v) {
105 Ok(()) => Ok(()),
106 Err(e) => {
107 report_json_serialize_error(&e);
108 Err(e)
109 }
110 };
111 }
112 if is_quiet() {
113 return Ok(());
114 }
115 let stdout = io::stdout();
116 let mut out = io::BufWriter::new(stdout.lock());
117 writeln!(
118 out,
119 "exec --all (max_concurrency={max_concurrency}, hosts={})",
120 results.len()
121 )?;
122 for h in results {
123 let status = if h.ok { "ok" } else { "ERR" };
124 writeln!(
125 out,
126 " {status} {} exit={:?} {}ms",
127 h.name, h.exit_code, h.duration_ms
128 )?;
129 if !h.stdout.is_empty() {
130 for line in h.stdout.lines() {
131 writeln!(out, " | {line}")?;
132 }
133 }
134 if !h.stderr.is_empty() {
135 for line in h.stderr.lines() {
136 writeln!(out, " ! {line}")?;
137 }
138 }
139 }
140 out.flush()
141}
142
143pub fn print_scp_batch(
148 direction: &str,
149 results: &[crate::scp::HostScpResult],
150 max_concurrency: usize,
151 json: bool,
152) -> io::Result<()> {
153 if json {
154 let batch_run_id = BatchRunId::new().to_string_canonical();
155 let v = ScpBatchJson {
156 event: "scp-batch".into(),
157 batch_run_id,
158 direction: direction.into(),
159 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
160 results: results
161 .iter()
162 .map(|h| ScpHostJson {
163 name: h.name.clone(),
164 ok: h.ok,
165 bytes: h.bytes,
166 duration_ms: h.duration_ms,
167 local: h.local.clone(),
168 error: h.error.clone(),
169 })
170 .collect(),
171 };
172 return match json_wire::print_json_line(&v) {
173 Ok(()) => Ok(()),
174 Err(e) => {
175 report_json_serialize_error(&e);
176 Err(e)
177 }
178 };
179 }
180 if is_quiet() {
181 return Ok(());
182 }
183 let stdout = io::stdout();
184 let mut out = io::BufWriter::new(stdout.lock());
185 writeln!(
186 out,
187 "scp {direction} --all (max_concurrency={max_concurrency}, hosts={})",
188 results.len()
189 )?;
190 for h in results {
191 if h.ok {
192 writeln!(
193 out,
194 " ok {} bytes={:?} {:?}ms",
195 h.name, h.bytes, h.duration_ms
196 )?;
197 } else {
198 let err = h.error.as_deref().unwrap_or("error");
199 writeln!(out, " ERR {} {err}", h.name)?;
200 }
201 }
202 out.flush()
203}
204
205pub fn print_transfer_json(
210 direction: &str,
211 vps: &str,
212 local: &str,
213 remote: &str,
214 bytes: u64,
215 duration_ms: u64,
216) -> io::Result<()> {
217 let v = ScpTransferJson {
219 ok: true,
220 event: "scp-transfer".into(),
221 direction: direction.to_string(),
222 vps: vps.to_string(),
223 local: local.to_string(),
224 remote: remote.to_string(),
225 bytes,
226 duration_ms,
227 };
228 match json_wire::print_json_line(&v) {
229 Ok(()) => Ok(()),
230 Err(e) => {
231 report_json_serialize_error(&e);
232 Err(e)
233 }
234 }
235}
236
237pub fn print_sftp_transfer_json(
242 direction: &str,
243 vps: &str,
244 local: &str,
245 remote: &str,
246 bytes: u64,
247 duration_ms: u64,
248 recursive: bool,
249) -> io::Result<()> {
250 let v = SftpTransferJson {
251 ok: true,
252 event: "sftp-transfer".into(),
253 direction: direction.to_string(),
254 vps: vps.to_string(),
255 local: local.to_string(),
256 remote: remote.to_string(),
257 bytes,
258 duration_ms,
259 recursive,
260 };
261 match json_wire::print_json_line(&v) {
262 Ok(()) => Ok(()),
263 Err(e) => {
264 report_json_serialize_error(&e);
265 Err(e)
266 }
267 }
268}
269
270pub fn print_sftp_list_json(vps: &str, path: &str, entries: &[SftpListEntry]) -> io::Result<()> {
275 let v = SftpListJson {
276 ok: true,
277 event: "sftp-list".into(),
278 vps: vps.to_string(),
279 path: path.to_string(),
280 entries: entries
281 .iter()
282 .map(|e| SftpListEntryJson {
283 name: e.name.clone(),
284 path: e.path.clone(),
285 kind: e.kind.clone(),
286 size: e.size,
287 mode: e.mode,
288 })
289 .collect(),
290 };
291 match json_wire::print_json_line(&v) {
292 Ok(()) => Ok(()),
293 Err(e) => {
294 report_json_serialize_error(&e);
295 Err(e)
296 }
297 }
298}
299
300pub fn print_sftp_fs_op_json(
305 op: &str,
306 vps: &str,
307 path: &str,
308 to: Option<&str>,
309 duration_ms: u64,
310) -> io::Result<()> {
311 let v = SftpFsOpJson {
312 ok: true,
313 event: "sftp-fs-op".into(),
314 op: op.to_string(),
315 vps: vps.to_string(),
316 path: path.to_string(),
317 to: to.map(str::to_owned),
318 duration_ms,
319 kind: None,
320 size: None,
321 mode: None,
322 mtime: None,
323 };
324 match json_wire::print_json_line(&v) {
325 Ok(()) => Ok(()),
326 Err(e) => {
327 report_json_serialize_error(&e);
328 Err(e)
329 }
330 }
331}
332
333pub fn print_sftp_stat_json(vps: &str, st: &SftpStat) -> io::Result<()> {
338 let v = SftpFsOpJson {
339 ok: true,
340 event: "sftp-fs-op".into(),
341 op: "stat".into(),
342 vps: vps.to_string(),
343 path: st.path.clone(),
344 to: None,
345 duration_ms: 0,
346 kind: Some(st.kind.clone()),
347 size: st.size,
348 mode: st.mode,
349 mtime: st.mtime,
350 };
351 match json_wire::print_json_line(&v) {
352 Ok(()) => Ok(()),
353 Err(e) => {
354 report_json_serialize_error(&e);
355 Err(e)
356 }
357 }
358}
359
360pub fn print_sftp_batch(
365 direction: &str,
366 results: &[HostSftpResult],
367 max_concurrency: usize,
368 json: bool,
369) -> io::Result<()> {
370 if json {
371 let batch_run_id = BatchRunId::new().to_string_canonical();
372 let v = SftpBatchJson {
373 event: "sftp-batch".into(),
374 batch_run_id,
375 direction: direction.to_string(),
376 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
377 results: results
378 .iter()
379 .map(|h| ScpHostJson {
380 name: h.name.clone(),
381 ok: h.ok,
382 bytes: h.bytes,
383 duration_ms: h.duration_ms,
384 local: h.local.clone(),
385 error: h.error.clone(),
386 })
387 .collect(),
388 };
389 return match json_wire::print_json_line(&v) {
390 Ok(()) => Ok(()),
391 Err(e) => {
392 report_json_serialize_error(&e);
393 Err(e)
394 }
395 };
396 }
397 if is_quiet() {
398 return Ok(());
399 }
400 let stdout = io::stdout();
401 let mut out = io::BufWriter::new(stdout.lock());
402 writeln!(
403 out,
404 "sftp {direction} --all (max_concurrency={max_concurrency}, hosts={})",
405 results.len()
406 )?;
407 for h in results {
408 if h.ok {
409 writeln!(
410 out,
411 " ok {} bytes={:?} ms={:?}",
412 h.name, h.bytes, h.duration_ms
413 )?;
414 } else {
415 let err = h.error.as_deref().unwrap_or("error");
416 writeln!(out, " ERR {} {err}", h.name)?;
417 }
418 }
419 out.flush()
420}
421
422pub fn print_tunnel_listening_json(
427 vps: &str,
428 local_port: u16,
429 remote_host: &str,
430 remote_port: u16,
431 timeout_ms: u64,
432) -> io::Result<()> {
433 let v = TunnelListeningJson {
434 ok: true,
435 event: "tunnel_listening".into(),
436 vps: vps.to_string(),
437 local_port,
438 remote_host: remote_host.to_string(),
439 remote_port,
440 timeout_ms,
441 };
442 match json_wire::print_json_line(&v) {
443 Ok(()) => Ok(()),
444 Err(e) => {
445 report_json_serialize_error(&e);
446 Err(e)
447 }
448 }
449}
450