diff --git a/src/cmds/system/read.rs b/src/cmds/system/read.rs index 2f56687ee3..a3b2c92310 100644 --- a/src/cmds/system/read.rs +++ b/src/cmds/system/read.rs @@ -4,6 +4,7 @@ use crate::core::filter::{self, FilterLevel, Language}; use crate::core::tracking; use anyhow::{Context, Result}; use std::fs; +use std::io::{self, Write}; use std::path::Path; pub fn run( @@ -70,13 +71,14 @@ pub fn run( } else { filtered.clone() }; - print!("{}", rtk_output); - timer.track( - &format!("cat {}", file.display()), - "rtk read", - &content, - &rtk_output, - ); + if write_stdout(&rtk_output)? { + timer.track( + &format!("cat {}", file.display()), + "rtk read", + &content, + &rtk_output, + ); + } Ok(()) } @@ -87,7 +89,7 @@ pub fn run_stdin( line_numbers: bool, verbose: u8, ) -> Result<()> { - use std::io::{self, Read as IoRead}; + use std::io::Read as IoRead; let timer = tracking::TimedExecution::start(); @@ -134,12 +136,23 @@ pub fn run_stdin( } else { filtered.clone() }; - print!("{}", rtk_output); + if !write_stdout(&rtk_output)? { + return Ok(()); + } timer.track("cat - (stdin)", "rtk read -", &content, &rtk_output); Ok(()) } +fn write_stdout(content: &str) -> Result { + let mut stdout = io::stdout().lock(); + match stdout.write_all(content.as_bytes()) { + Ok(()) => Ok(true), + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(false), + Err(e) => Err(e).context("failed writing read output"), + } +} + fn format_with_line_numbers(content: &str) -> String { let lines: Vec<&str> = content.lines().collect(); let width = lines.len().to_string().len(); diff --git a/tests/read_broken_pipe_test.rs b/tests/read_broken_pipe_test.rs new file mode 100644 index 0000000000..222d037c8b --- /dev/null +++ b/tests/read_broken_pipe_test.rs @@ -0,0 +1,42 @@ +use rusqlite::Connection; +use std::fs; +use std::process::Command; + +#[test] +fn read_piped_to_head_does_not_panic_or_record_partial_gain() { + let tmp = tempfile::tempdir().expect("tempdir"); + let file = tmp.path().join("large.txt"); + let db = tmp.path().join("history.db"); + fs::write(&file, "abcdef0123456789\n".repeat(20_000)).expect("write test file"); + + let output = Command::new("sh") + .arg("-c") + .arg("\"$RTK_BIN\" read \"$RTK_FILE\" | head -c 10 >/dev/null") + .env("RTK_BIN", env!("CARGO_BIN_EXE_rtk")) + .env("RTK_FILE", &file) + .env("RTK_DB_PATH", &db) + .env("RTK_TELEMETRY_DISABLED", "1") + .output() + .expect("run piped rtk read"); + + assert!( + output.status.success(), + "pipeline should succeed: status={:?}, stderr={}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("panicked") && !stderr.contains("failed printing to stdout"), + "broken pipe should be handled without panic, got stderr: {stderr}" + ); + + if db.exists() { + let conn = Connection::open(&db).expect("open history db"); + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM commands", [], |row| row.get(0)) + .expect("count commands"); + assert_eq!(count, 0, "partial read output must not be counted as gain"); + } +}