Skip to content

Commit f472bce

Browse files
authored
Ignore EPIPE in CLI (#746)
Piping command output to a process that performs a partial read before closing the pipe, such as `head`, will cause an `EPIPE` to be raised on the next write attempt. The standard `(e)print(ln)!` macros will panic on any errors when writing, including `EPIPE`. One option to handle this is to switch to `write(ln)!`, but this will inject new requirements to handle `Result` where used, which can be onerous. Create wrappers around the print macros to ignore `EPIPE`, and replace calls to the originals with them. Update `main` to ignore any `EPIPE`s returned from `writeln!` calls in subcommands. We deliberately do not make this change to the `auth login/logout` subcommands as these are mutating the config. Failure to notify the user of the changes is fatal. Before: $ oxide system networking switch-port-settings show | head -1 switch0/qsfp0 thread 'tokio-runtime-worker' panicked at library/std/src/io/stdio.rs:1021:9: failed printing to stdout: Broken pipe (os error 32) note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'main' panicked at cli/src/main.rs:102:10: called `Result::unwrap()` on an `Err` value: JoinError::Panic(Id(15), ...) After: $ oxide system networking switch-port-settings show | head -1 switch0/qsfp0
1 parent 040f1b0 commit f472bce

File tree

6 files changed

+183
-54
lines changed

6 files changed

+183
-54
lines changed

cli/src/cmd_api.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use futures::{StreamExt, TryStreamExt};
1616
use oxide::Client;
1717
use serde::Deserialize;
1818

19+
use crate::{print_nopipe, println_nopipe};
20+
1921
/// Makes an authenticated HTTP request to the Oxide API and prints the response.
2022
///
2123
/// The endpoint argument should be a path of a Oxide API endpoint.
@@ -183,18 +185,18 @@ impl crate::AuthenticatedCmd for CmdApi {
183185
if !resp.status().is_success() {
184186
let status = resp.status();
185187
let v = resp.json::<serde_json::Value>().await?;
186-
println!(
188+
println_nopipe!(
187189
"error; status code: {} {}",
188190
status.as_u16(),
189191
status.canonical_reason().unwrap_or_default()
190192
);
191-
println!("{}", serde_json::to_string_pretty(&v).unwrap());
193+
println_nopipe!("{}", serde_json::to_string_pretty(&v).unwrap());
192194
return Err(anyhow!("error"));
193195
}
194196

195197
// Print the response headers if requested.
196198
if self.include {
197-
println!("{:?} {}", resp.version(), resp.status());
199+
println_nopipe!("{:?} {}", resp.version(), resp.status());
198200
print_headers(resp.headers())?;
199201
}
200202

@@ -205,7 +207,7 @@ impl crate::AuthenticatedCmd for CmdApi {
205207
if !self.paginate {
206208
// Print the response body.
207209
let result = resp.json::<serde_json::Value>().await?;
208-
println!("{}", serde_json::to_string_pretty(&result)?);
210+
println_nopipe!("{}", serde_json::to_string_pretty(&result)?);
209211

210212
Ok(())
211213
} else {
@@ -239,7 +241,7 @@ impl crate::AuthenticatedCmd for CmdApi {
239241
Result::Ok(Some((items, next_page)))
240242
});
241243

242-
print!("[");
244+
print_nopipe!("[");
243245

244246
let result = first
245247
.chain(rest)
@@ -252,19 +254,19 @@ impl crate::AuthenticatedCmd for CmdApi {
252254
let items_core = &value_out[2..len - 2];
253255

254256
if comma_needed {
255-
print!(",");
257+
print_nopipe!(",");
256258
}
257-
println!();
258-
print!("{}", items_core);
259+
println_nopipe!();
260+
print_nopipe!("{}", items_core);
259261
}
260262
Ok(true)
261263
})
262264
.await;
263-
println!();
264-
println!("]");
265+
println_nopipe!();
266+
println_nopipe!("]");
265267

266268
if let Err(e) = &result {
267-
println!("An error occurred during a paginated query:\n{}", e);
269+
println_nopipe!("An error occurred during a paginated query:\n{}", e);
268270
}
269271
result.map(|_| ())
270272
}
@@ -387,7 +389,7 @@ fn print_headers(headers: &reqwest::header::HeaderMap) -> Result<()> {
387389
tw.flush()?;
388390

389391
let table = String::from_utf8(tw.into_inner()?)?;
390-
println!("{}", table);
392+
println_nopipe!("{}", table);
391393

392394
Ok(())
393395
}

cli/src/cmd_auth.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use toml_edit::{Item, Table};
1919
use uuid::Uuid;
2020

2121
use crate::context::Context;
22-
use crate::{AsHost, RunnableCmd};
22+
use crate::{println_nopipe, AsHost, RunnableCmd};
2323

2424
/// Login, logout, and get the status of your authentication.
2525
///
@@ -441,11 +441,11 @@ impl CmdAuthStatus {
441441
match client.current_user_view().send().await {
442442
Ok(user) => {
443443
log::debug!("success response for {} (env): {:?}", host_env, user);
444-
println!("Logged in to {} as {}", host_env, user.id)
444+
println_nopipe!("Logged in to {} as {}", host_env, user.id)
445445
}
446446
Err(e) => {
447447
log::debug!("error response for {} (env): {}", host_env, e);
448-
println!("{}: {}", host_env, Self::error_msg(&e))
448+
println_nopipe!("{}: {}", host_env, Self::error_msg(&e))
449449
}
450450
};
451451
} else {
@@ -467,9 +467,11 @@ impl CmdAuthStatus {
467467
}
468468
};
469469

470-
println!(
470+
println_nopipe!(
471471
"Profile \"{}\" ({}) status: {}",
472-
profile_name, profile_info.host, status
472+
profile_name,
473+
profile_info.host,
474+
status
473475
);
474476
}
475477
}

cli/src/cmd_disk.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
// Copyright 2024 Oxide Computer Company
66

7+
use crate::{eprintln_nopipe, println_nopipe};
8+
79
use anyhow::bail;
810
use anyhow::Result;
911
use async_trait::async_trait;
@@ -190,9 +192,10 @@ impl CmdDiskImport {
190192
.await;
191193

192194
if let Err(e) = response {
193-
eprintln!(
195+
eprintln_nopipe!(
194196
"trying to unwind, deleting {:?} failed with {:?}",
195-
self.disk, e
197+
self.disk,
198+
e
196199
);
197200
return Err(e.into());
198201
}
@@ -210,9 +213,10 @@ impl CmdDiskImport {
210213

211214
// If this fails, then the disk will remain in state "import-ready"
212215
if let Err(e) = response {
213-
eprintln!(
216+
eprintln_nopipe!(
214217
"trying to unwind, finalizing {:?} failed with {:?}",
215-
self.disk, e
218+
self.disk,
219+
e
216220
);
217221
return Err(e.into());
218222
}
@@ -231,9 +235,10 @@ impl CmdDiskImport {
231235
// If this fails, then the disk will remain in state
232236
// "importing-from-bulk-writes"
233237
if let Err(e) = response {
234-
eprintln!(
238+
eprintln_nopipe!(
235239
"trying to unwind, stopping the bulk write process for {:?} failed with {:?}",
236-
self.disk, e
240+
self.disk,
241+
e
237242
);
238243
return Err(e.into());
239244
}
@@ -334,7 +339,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
334339
.await;
335340

336341
if let Err(e) = start_bulk_write_response {
337-
eprintln!("starting the bulk write process failed with {:?}", e);
342+
eprintln_nopipe!("starting the bulk write process failed with {:?}", e);
338343

339344
// If this fails, the disk is in state import-ready. Finalize it so
340345
// it can be deleted.
@@ -402,7 +407,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
402407
let n = match file.by_ref().take(CHUNK_SIZE).read_to_end(&mut chunk) {
403408
Ok(n) => n,
404409
Err(e) => {
405-
eprintln!(
410+
eprintln_nopipe!(
406411
"reading from {} failed with {:?}",
407412
self.path.to_string_lossy(),
408413
e,
@@ -420,7 +425,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
420425
let encoded = base64::engine::general_purpose::STANDARD.encode(&chunk[0..n]);
421426

422427
if let Err(e) = senders[i % UPLOAD_TASKS].send((offset, encoded, n)).await {
423-
eprintln!("sending chunk to thread failed with {:?}", e);
428+
eprintln_nopipe!("sending chunk to thread failed with {:?}", e);
424429
break Err(e.into());
425430
}
426431
} else {
@@ -456,7 +461,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
456461

457462
if results.iter().any(|x| x.is_err()) {
458463
// If any of the upload threads returned an error, unwind the disk.
459-
eprintln!("one of the upload threads failed");
464+
eprintln_nopipe!("one of the upload threads failed");
460465
self.unwind_disk_bulk_write_stop(client).await?;
461466
self.unwind_disk_finalize(client).await?;
462467
self.unwind_disk_delete(client).await?;
@@ -475,7 +480,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
475480
.await;
476481

477482
if let Err(e) = stop_bulk_write_response {
478-
eprintln!("stopping the bulk write process failed with {:?}", e);
483+
eprintln_nopipe!("stopping the bulk write process failed with {:?}", e);
479484

480485
// Attempt to unwind the disk, although it will probably fail - the
481486
// first step is to stop the bulk write process!
@@ -498,7 +503,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
498503
let finalize_response = request.send().await;
499504

500505
if let Err(e) = finalize_response {
501-
eprintln!("finalizing the disk failed with {:?}", e);
506+
eprintln_nopipe!("finalizing the disk failed with {:?}", e);
502507

503508
// Attempt to unwind the disk, although it will probably fail - the
504509
// first step is to finalize the disk!
@@ -541,7 +546,7 @@ impl crate::AuthenticatedCmd for CmdDiskImport {
541546
.await?;
542547
}
543548

544-
println!("done!");
549+
println_nopipe!("done!");
545550

546551
Ok(())
547552
}

cli/src/cmd_net.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ use std::io::Write;
2828
use tabwriter::TabWriter;
2929
use uuid::Uuid;
3030

31+
use crate::println_nopipe;
32+
3133
// We do not yet support port breakouts, but the API is phrased in terms of
3234
// ports that can be broken out. The constant phy0 represents the first port
3335
// in a breakout.
@@ -875,14 +877,14 @@ impl AuthenticatedCmd for CmdPortConfig {
875877
.map(|x| (x.id, x.name))
876878
.collect();
877879

878-
println!(
880+
println_nopipe!(
879881
"{}{}{}",
880882
p.switch_location.to_string().blue(),
881883
"/".dimmed(),
882884
p.port_name.blue(),
883885
);
884886

885-
println!(
887+
println_nopipe!(
886888
"{}",
887889
"=".repeat(p.port_name.len() + p.switch_location.to_string().len() + 1)
888890
.dimmed()
@@ -900,7 +902,7 @@ impl AuthenticatedCmd for CmdPortConfig {
900902
writeln!(&mut tw, "{:?}\t{:?}\t{:?}", l.autoneg, l.fec, l.speed,)?;
901903
}
902904
tw.flush()?;
903-
println!();
905+
println_nopipe!();
904906

905907
writeln!(
906908
&mut tw,
@@ -923,7 +925,7 @@ impl AuthenticatedCmd for CmdPortConfig {
923925
writeln!(&mut tw, "{}\t{}\t{:?}", addr, *alb.0.name, a.vlan_id)?;
924926
}
925927
tw.flush()?;
926-
println!();
928+
println_nopipe!();
927929

928930
writeln!(
929931
&mut tw,
@@ -987,7 +989,7 @@ impl AuthenticatedCmd for CmdPortConfig {
987989
)?;
988990
}
989991
tw.flush()?;
990-
println!();
992+
println_nopipe!();
991993

992994
// Uncomment to see full payload
993995
//println!("");
@@ -1017,13 +1019,13 @@ impl AuthenticatedCmd for CmdBgpStatus {
10171019
.iter()
10181020
.partition(|x| x.switch == SwitchLocation::Switch0);
10191021

1020-
println!("{}", "switch0".dimmed());
1021-
println!("{}", "=======".dimmed());
1022+
println_nopipe!("{}", "switch0".dimmed());
1023+
println_nopipe!("{}", "=======".dimmed());
10221024
show_status(&sw0)?;
1023-
println!();
1025+
println_nopipe!();
10241026

1025-
println!("{}", "switch1".dimmed());
1026-
println!("{}", "=======".dimmed());
1027+
println_nopipe!("{}", "switch1".dimmed());
1028+
println_nopipe!("{}", "=======".dimmed());
10271029
show_status(&sw1)?;
10281030

10291031
Ok(())
@@ -1078,12 +1080,12 @@ impl AuthenticatedCmd for CmdPortStatus {
10781080
sw0.sort_by_key(|x| x.port_name.as_str());
10791081
sw1.sort_by_key(|x| x.port_name.as_str());
10801082

1081-
println!("{}", "switch0".dimmed());
1082-
println!("{}", "=======".dimmed());
1083+
println_nopipe!("{}", "switch0".dimmed());
1084+
println_nopipe!("{}", "=======".dimmed());
10831085
self.show_switch(client, "switch0", &sw0).await?;
10841086

1085-
println!("{}", "switch1".dimmed());
1086-
println!("{}", "=======".dimmed());
1087+
println_nopipe!("{}", "switch1".dimmed());
1088+
println_nopipe!("{}", "=======".dimmed());
10871089
self.show_switch(client, "switch1", &sw1).await?;
10881090

10891091
Ok(())
@@ -1228,9 +1230,9 @@ impl CmdPortStatus {
12281230
}
12291231

12301232
ltw.flush()?;
1231-
println!();
1233+
println_nopipe!();
12321234
mtw.flush()?;
1233-
println!();
1235+
println_nopipe!();
12341236

12351237
Ok(())
12361238
}

0 commit comments

Comments
 (0)