Watch
0
0
Fork
You've already forked hyperhive
0

hivectl agent watch: strip control chars, exit quietly on a broken pipe

This commit is contained in:
atlas 2026-11-07 19:24:22 +01:00 • committed by mara
commit 30a61d96dd

View file

@ -54,30 +54,10 @@ async fn follow(target: &Target, agent: &str) -> Result<()> {
Ok(())
}
/// Write one line and flush. `Ok(false)` means the reader went away (a closed
/// pipe, as in `watch x | head`), which ends the watch quietly; any other
/// write error is an error.
/// Write one line; `Ok(false)` means the reader went away (a closed pipe,
/// as in `watch x | head`), which ends the watch quietly.
fn emit(out: &mut impl std::io::Write, line: &str) -> Result<bool> {
match writeln!(out, "{line}").and_then(|()| out.flush()) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(false),
Err(e) => Err(e).context("writing to stdout"),
}
}
/// Replace every control character except tab with U+FFFD. Rows are
/// agent-authored, and an escape sequence printed as-is would act on the
/// operator's terminal.
fn sanitize(text: &str) -> String {
text.chars()
.map(|c| {
if c.is_control() && c != '\t' {
'\u{FFFD}'
} else {
c
}
})
.collect()
hive_types::term_out::emit(out, line).context("writing to stdout")
}
/// A response body read in pieces, whichever transport carried it.
@ -180,7 +160,7 @@ impl Frames {
/// no `data:` line (a keep-alive comment) prints nothing; a payload that is
/// not a row prints raw, so a shape this doesn't know stays visible.
fn render_frame(frame: &str) -> Option<String> {
render_raw(frame).map(|line| sanitize(&line))
render_raw(frame).map(|line| hive_types::term_out::sanitize(&line))
}
fn render_raw(frame: &str) -> Option<String> {
@ -261,29 +241,6 @@ mod tests {
assert_eq!(raw, "! \u{FFFD}[2J");
}
struct Failing(std::io::ErrorKind);
impl std::io::Write for Failing {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(self.0.into())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn a_closed_pipe_ends_the_watch_quietly_and_other_write_errors_fail() {
let mut ok = Vec::new();
assert!(super::emit(&mut ok, "x").expect("writes"));
assert_eq!(ok, b"x\n");
let closed = super::emit(&mut Failing(std::io::ErrorKind::BrokenPipe), "x");
assert!(!closed.expect("broken pipe is not an error"));
let other = super::emit(&mut Failing(std::io::ErrorKind::PermissionDenied), "x");
assert!(other.is_err());
}
/// Serves one canned response on a fresh unix socket and returns its path.
fn serve_once(name: &str, response: Vec<&'static [u8]>) -> std::path::PathBuf {
use std::io::{Read as _, Write as _};