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, TunnelCloseReason, TunnelClosedJson, TunnelListeningJson,
12};
13#[cfg(feature = "ssh-real")]
14use crate::json_wire::{
15 SftpBatchJson, SftpFsOpJson, SftpListEntryJson, SftpListJson, SftpTransferJson,
16};
17#[cfg(feature = "ssh-real")]
20use crate::sftp::batch::HostSftpResult;
21#[cfg(feature = "ssh-real")]
22use crate::ssh::sftp_types::{SftpListEntry, SftpStat};
23use crate::vps::{HostExecResult, HostHealthResult};
24use std::io::{self, Write};
25
26pub fn print_health_batch(
31 results: &[HostHealthResult],
32 max_concurrency: usize,
33 json: bool,
34) -> io::Result<()> {
35 if json {
36 let batch_run_id = BatchRunId::new().to_string_canonical();
38 let v = HealthBatchJson {
39 event: "health-check-batch".into(),
40 batch_run_id,
41 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
42 results: results
43 .iter()
44 .map(|h| HealthHostJson {
45 name: h.name.clone(),
46 status: if h.ok { "ok".into() } else { "error".into() },
47 latency_ms: h.latency_ms,
48 error: h.error.clone(),
49 })
50 .collect(),
51 };
52 return match json_wire::print_json_line(&v) {
53 Ok(()) => Ok(()),
54 Err(e) => {
55 report_json_serialize_error(&e);
56 Err(e)
57 }
58 };
59 }
60 if is_quiet() {
61 return Ok(());
62 }
63 let stdout = io::stdout();
64 let mut out = io::BufWriter::new(stdout.lock());
65 writeln!(
66 out,
67 "health-check --all (max_concurrency={max_concurrency}, hosts={})",
68 results.len()
69 )?;
70 for h in results {
71 match (h.ok, h.latency_ms) {
72 (true, Some(ms)) => writeln!(out, " ok {} {ms}ms", h.name)?,
73 (true, None) => writeln!(out, " ok {}", h.name)?,
74 (false, _) => {
75 let err = h.error.as_deref().unwrap_or("error");
76 writeln!(out, " ERR {} {err}", h.name)?;
77 }
78 }
79 }
80 out.flush()
81}
82
83pub fn print_exec_batch(
88 results: &[HostExecResult],
89 max_concurrency: usize,
90 json: bool,
91) -> io::Result<()> {
92 if json {
93 let batch_run_id = BatchRunId::new().to_string_canonical();
94 let v = ExecBatchJson {
95 event: "exec-batch".into(),
96 batch_run_id,
97 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
98 results: results
99 .iter()
100 .map(|h| ExecHostJson {
101 name: h.name.clone(),
102 ok: h.ok,
103 exit_code: h.exit_code,
104 stdout: h.stdout.clone(),
105 stderr: h.stderr.clone(),
106 duration_ms: h.duration_ms,
107 error: h.error.clone(),
108 })
109 .collect(),
110 };
111 return match json_wire::print_json_line(&v) {
112 Ok(()) => Ok(()),
113 Err(e) => {
114 report_json_serialize_error(&e);
115 Err(e)
116 }
117 };
118 }
119 if is_quiet() {
120 return Ok(());
121 }
122 let stdout = io::stdout();
123 let mut out = io::BufWriter::new(stdout.lock());
124 writeln!(
125 out,
126 "exec --all (max_concurrency={max_concurrency}, hosts={})",
127 results.len()
128 )?;
129 for h in results {
130 let status = if h.ok { "ok" } else { "ERR" };
131 writeln!(
132 out,
133 " {status} {} exit={:?} {}ms",
134 h.name, h.exit_code, h.duration_ms
135 )?;
136 if !h.stdout.is_empty() {
137 for line in h.stdout.lines() {
138 writeln!(out, " | {line}")?;
139 }
140 }
141 if !h.stderr.is_empty() {
142 for line in h.stderr.lines() {
143 writeln!(out, " ! {line}")?;
144 }
145 }
146 }
147 out.flush()
148}
149
150pub fn print_scp_batch(
160 direction: &str,
161 results: &[crate::scp::HostScpResult],
162 max_concurrency: usize,
163 json: bool,
164 source: json_wire::TargetSource,
165) -> io::Result<()> {
166 if json {
167 let batch_run_id = BatchRunId::new().to_string_canonical();
168 let v = ScpBatchJson {
169 event: "scp-batch".into(),
170 target_source: source,
171 host_source: source,
172 batch_run_id,
173 direction: direction.into(),
174 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
175 results: results
176 .iter()
177 .map(|h| ScpHostJson {
178 name: h.name.clone(),
179 ok: h.ok,
180 bytes: h.bytes,
181 duration_ms: h.duration_ms,
182 local: h.local.clone(),
183 error: h.error.clone(),
184 })
185 .collect(),
186 };
187 return match json_wire::print_json_line(&v) {
188 Ok(()) => Ok(()),
189 Err(e) => {
190 report_json_serialize_error(&e);
191 Err(e)
192 }
193 };
194 }
195 if is_quiet() {
196 return Ok(());
197 }
198 let stdout = io::stdout();
199 let mut out = io::BufWriter::new(stdout.lock());
200 writeln!(
201 out,
202 "scp {direction} --all (max_concurrency={max_concurrency}, hosts={})",
203 results.len()
204 )?;
205 for h in results {
206 if h.ok {
207 writeln!(
208 out,
209 " ok {} bytes={:?} {:?}ms",
210 h.name, h.bytes, h.duration_ms
211 )?;
212 } else {
213 let err = h.error.as_deref().unwrap_or("error");
214 writeln!(out, " ERR {} {err}", h.name)?;
215 }
216 }
217 out.flush()
218}
219
220pub fn print_transfer_json(
230 direction: &str,
231 target: &json_wire::ExecTarget,
232 local: &str,
233 remote: &str,
234 result: &crate::ssh::client::TransferResult,
235) -> io::Result<()> {
236 let v = ScpTransferJson {
238 ok: true,
239 event: "scp-transfer".into(),
240 target: json_wire::TargetEcho::new(target),
241 direction: direction.to_string(),
242 vps: target.host.clone(),
243 local: local.to_string(),
244 remote: remote.to_string(),
245 bytes: result.bytes_transferred,
246 duration_ms: result.duration_ms,
247 mtime_preserved: result.mtime_preserved,
248 durable: result.durable,
249 };
250 match json_wire::print_json_line(&v) {
251 Ok(()) => Ok(()),
252 Err(e) => {
253 report_json_serialize_error(&e);
254 Err(e)
255 }
256 }
257}
258
259#[cfg(feature = "ssh-real")]
264pub fn print_sftp_transfer_json(
265 direction: &str,
266 target: &json_wire::ExecTarget,
267 local: &str,
268 remote: &str,
269 bytes: u64,
270 duration_ms: u64,
271 recursive: bool,
272) -> io::Result<()> {
273 let v = SftpTransferJson {
274 ok: true,
275 event: "sftp-transfer".into(),
276 target: json_wire::TargetEcho::new(target),
277 direction: direction.to_string(),
278 vps: target.host.clone(),
279 local: local.to_string(),
280 remote: remote.to_string(),
281 bytes,
282 duration_ms,
283 recursive,
284 };
285 match json_wire::print_json_line(&v) {
286 Ok(()) => Ok(()),
287 Err(e) => {
288 report_json_serialize_error(&e);
289 Err(e)
290 }
291 }
292}
293
294#[cfg(feature = "ssh-real")]
299pub fn print_sftp_list_json(vps: &str, path: &str, entries: &[SftpListEntry]) -> io::Result<()> {
300 let v = SftpListJson {
301 ok: true,
302 event: "sftp-list".into(),
303 vps: vps.to_string(),
304 path: path.to_string(),
305 entries: entries
306 .iter()
307 .map(|e| SftpListEntryJson {
308 name: e.name.clone(),
309 path: e.path.clone(),
310 kind: e.kind.clone(),
311 size: e.size,
312 mode: e.mode,
313 })
314 .collect(),
315 };
316 match json_wire::print_json_line(&v) {
317 Ok(()) => Ok(()),
318 Err(e) => {
319 report_json_serialize_error(&e);
320 Err(e)
321 }
322 }
323}
324
325#[cfg(feature = "ssh-real")]
330pub fn print_sftp_fs_op_json(
331 op: &str,
332 vps: &str,
333 path: &str,
334 to: Option<&str>,
335 duration_ms: u64,
336) -> io::Result<()> {
337 let v = SftpFsOpJson {
338 ok: true,
339 event: "sftp-fs-op".into(),
340 op: op.to_string(),
341 vps: vps.to_string(),
342 path: path.to_string(),
343 to: to.map(str::to_owned),
344 duration_ms,
345 kind: None,
346 size: None,
347 mode: None,
348 mtime: None,
349 };
350 match json_wire::print_json_line(&v) {
351 Ok(()) => Ok(()),
352 Err(e) => {
353 report_json_serialize_error(&e);
354 Err(e)
355 }
356 }
357}
358
359#[cfg(feature = "ssh-real")]
364pub fn print_sftp_stat_json(vps: &str, st: &SftpStat) -> io::Result<()> {
365 let v = SftpFsOpJson {
366 ok: true,
367 event: "sftp-fs-op".into(),
368 op: "stat".into(),
369 vps: vps.to_string(),
370 path: st.path.clone(),
371 to: None,
372 duration_ms: 0,
373 kind: Some(st.kind.clone()),
374 size: st.size,
375 mode: st.mode,
376 mtime: st.mtime,
377 };
378 match json_wire::print_json_line(&v) {
379 Ok(()) => Ok(()),
380 Err(e) => {
381 report_json_serialize_error(&e);
382 Err(e)
383 }
384 }
385}
386
387#[cfg(feature = "ssh-real")]
392pub fn print_sftp_batch(
393 direction: &str,
394 results: &[HostSftpResult],
395 max_concurrency: usize,
396 json: bool,
397) -> io::Result<()> {
398 if json {
399 let batch_run_id = BatchRunId::new().to_string_canonical();
400 let v = SftpBatchJson {
401 event: "sftp-batch".into(),
402 target_source: json_wire::TargetSource::Selector,
404 host_source: json_wire::TargetSource::Selector,
405 batch_run_id,
406 direction: direction.to_string(),
407 max_concurrency: u32::try_from(max_concurrency).unwrap_or(u32::MAX),
408 results: results
409 .iter()
410 .map(|h| ScpHostJson {
411 name: h.name.clone(),
412 ok: h.ok,
413 bytes: h.bytes,
414 duration_ms: h.duration_ms,
415 local: h.local.clone(),
416 error: h.error.clone(),
417 })
418 .collect(),
419 };
420 return match json_wire::print_json_line(&v) {
421 Ok(()) => Ok(()),
422 Err(e) => {
423 report_json_serialize_error(&e);
424 Err(e)
425 }
426 };
427 }
428 if is_quiet() {
429 return Ok(());
430 }
431 let stdout = io::stdout();
432 let mut out = io::BufWriter::new(stdout.lock());
433 writeln!(
434 out,
435 "sftp {direction} --all (max_concurrency={max_concurrency}, hosts={})",
436 results.len()
437 )?;
438 for h in results {
439 if h.ok {
440 writeln!(
441 out,
442 " ok {} bytes={:?} ms={:?}",
443 h.name, h.bytes, h.duration_ms
444 )?;
445 } else {
446 let err = h.error.as_deref().unwrap_or("error");
447 writeln!(out, " ERR {} {err}", h.name)?;
448 }
449 }
450 out.flush()
451}
452
453#[must_use]
461pub fn build_tunnel_listening(
462 vps: &str,
463 local_port: u16,
464 remote_host: &str,
465 remote_port: u16,
466 timeout_ms: u64,
467 bind: &str,
468 mode: &str,
469) -> TunnelListeningJson {
470 TunnelListeningJson {
471 ok: true,
472 event: "tunnel_listening".into(),
473 vps: vps.to_string(),
474 local_port,
475 remote_host: remote_host.to_string(),
476 remote_port,
477 timeout_ms,
478 bind: bind.to_string(),
479 mode: mode.to_string(),
480 }
481}
482
483pub fn print_tunnel_listening_json(
488 vps: &str,
489 local_port: u16,
490 remote_host: &str,
491 remote_port: u16,
492 timeout_ms: u64,
493 bind: &str,
494 mode: &str,
495) -> io::Result<()> {
496 let v = build_tunnel_listening(
497 vps,
498 local_port,
499 remote_host,
500 remote_port,
501 timeout_ms,
502 bind,
503 mode,
504 );
505 match json_wire::print_json_line(&v) {
506 Ok(()) => Ok(()),
507 Err(e) => {
508 report_json_serialize_error(&e);
509 Err(e)
510 }
511 }
512}
513
514#[must_use]
523pub fn build_tunnel_closed(input: TunnelClosedInput<'_>) -> TunnelClosedJson {
524 TunnelClosedJson {
525 ok: !matches!(input.reason, TunnelCloseReason::AcceptError),
528 event: "tunnel_closed".into(),
529 vps: input.vps.to_string(),
530 reason: input.reason,
531 bind: input.bind.to_string(),
532 local_port: input.local_port,
533 forwards_served: input.forwards_served,
534 capacity_waits: input.capacity_waits,
535 duration_ms: input.duration_ms,
536 mode: input.mode.to_string(),
537 }
538}
539
540pub struct TunnelClosedInput<'a> {
548 pub vps: &'a str,
550 pub reason: TunnelCloseReason,
552 pub bind: &'a str,
554 pub local_port: u16,
556 pub forwards_served: u64,
558 pub capacity_waits: u64,
560 pub duration_ms: u64,
562 pub mode: &'a str,
564}
565
566pub fn print_tunnel_closed_json(event: &TunnelClosedJson) -> io::Result<()> {
574 match json_wire::print_json_line(event) {
575 Ok(()) => Ok(()),
576 Err(e) => {
577 report_json_serialize_error(&e);
578 Err(e)
579 }
580 }
581}