hivectl agent watch: strip control chars, exit quietly on a broken pipe
This commit is contained in:
parent
9457f49684
commit
30a61d96dd
1 changed files with 160 additions and 59 deletions
|
|
@ -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 _};
|
||||
|
|
|
|||
Loading…
Reference in a new issue