Skip to content

Commit 0f93cd8

Browse files
committed
Handle EPIPE error gracefully
1 parent 0e7b029 commit 0f93cd8

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

candumpr/src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ extern "C" fn signal_handler(_sig: libc::c_int) {
2121
STOP.store(true, Ordering::Relaxed);
2222
}
2323

24+
/// True if any error in the chain is an EPIPE.
25+
fn is_broken_pipe(err: &eyre::Report) -> bool {
26+
err.chain()
27+
.filter_map(|e| e.downcast_ref::<std::io::Error>())
28+
.any(|io| io.kind() == std::io::ErrorKind::BrokenPipe)
29+
}
30+
2431
/// Log a link-state edge to stderr, ignoring repeats of the last observed state.
2532
fn handle_link_event(event: LinkEvent, link_up: &mut [Option<bool>], names: &[String]) {
2633
let (sock_id, up) = match event {
@@ -201,6 +208,10 @@ fn main() -> ExitCode {
201208
Ok(mut batch) => {
202209
log_error_frames(&batch, &mut bus_state, &names);
203210
if let Err(e) = pipeline.write_batch(&batch) {
211+
if is_broken_pipe(&e) {
212+
tracing::debug!("output closed; shutting down");
213+
break;
214+
}
204215
tracing::error!(error = ?e, "failed to write batch");
205216
failed = true;
206217
}
@@ -255,6 +266,9 @@ fn main() -> ExitCode {
255266
// Drain everything the receiver queued before it exited.
256267
while let Ok(mut batch) = full_rx.try_recv() {
257268
if let Err(e) = pipeline.write_batch(&batch) {
269+
if is_broken_pipe(&e) {
270+
break;
271+
}
258272
tracing::error!(error = ?e, "failed to write batch during drain");
259273
failed = true;
260274
}

candumpr/tests/epipe.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::io::{BufRead, BufReader};
2+
use std::os::unix::io::AsFd;
3+
use std::process::{Command, Stdio};
4+
use std::time::{Duration, Instant};
5+
6+
use candumpr::can::{self, LinuxCanFrame};
7+
use vcan_fixture::VcanHarness;
8+
9+
#[ctor::ctor]
10+
fn setup() {
11+
tracing_subscriber::fmt().with_test_writer().init();
12+
vcan_fixture::enter_namespace();
13+
}
14+
15+
/// When the stdout consumer goes away (`candumpr can0 | head`), candumpr must notice the broken
16+
/// pipe on its next write and exit cleanly on its own, without being signalled.
17+
#[test]
18+
#[cfg_attr(feature = "ci", ignore = "requires vcan")]
19+
fn exits_cleanly_when_stdout_closes() {
20+
let vcans = VcanHarness::new(1).unwrap();
21+
let iface = &vcans.names()[0];
22+
23+
let mut child = Command::new(env!("CARGO_BIN_EXE_candumpr"))
24+
.arg(iface)
25+
.arg("--log-level=INFO") // TRACE level fills up the stderr buf and prevents EPIPE error
26+
.stdout(Stdio::piped())
27+
.stderr(Stdio::piped())
28+
.spawn()
29+
.unwrap();
30+
31+
// Give the process time to set up io_uring and start receiving.
32+
std::thread::sleep(Duration::from_millis(200));
33+
34+
let tx = can::open_can_raw_blocking(iface).unwrap();
35+
let frame = LinuxCanFrame::new(0x18FECA00 | libc::CAN_EFF_FLAG, &[0xAA, 0xBB]);
36+
37+
// Prove the pipe is live: send one frame and read the line it produces.
38+
let mut stdout = BufReader::new(child.stdout.take().unwrap());
39+
can::send_frame(tx.as_fd(), &frame).unwrap();
40+
let mut line = String::new();
41+
stdout.read_line(&mut line).unwrap();
42+
assert!(
43+
line.ends_with('\n'),
44+
"expected a full output line: {line:?}"
45+
);
46+
47+
// Close the read end, like `head` exiting. The next batch write hits EPIPE.
48+
drop(stdout);
49+
can::send_frame(tx.as_fd(), &frame).unwrap();
50+
51+
let deadline = Instant::now() + Duration::from_secs(5);
52+
let status = loop {
53+
if let Some(status) = child.try_wait().unwrap() {
54+
break status;
55+
}
56+
if Instant::now() >= deadline {
57+
child.kill().unwrap();
58+
let output = child.wait_with_output().unwrap();
59+
panic!(
60+
"candumpr did not exit after stdout closed. stderr:\n{}",
61+
String::from_utf8_lossy(&output.stderr)
62+
);
63+
}
64+
std::thread::sleep(Duration::from_millis(50));
65+
};
66+
67+
let output = child.wait_with_output().unwrap();
68+
eprint!("{}", String::from_utf8_lossy(&output.stderr));
69+
assert!(
70+
status.success(),
71+
"expected clean exit after stdout closed, got {status}"
72+
);
73+
}

0 commit comments

Comments
 (0)