hivectl agent watch: strip control chars, exit quietly on a broken pipe
This commit is contained in:
parent
9457f49684
commit
30a61d96dd
2 changed files with 160 additions and 59 deletions
|
|
@ -1,5 +1,5 @@
|
|||
//! Foundational shared types for the hyperhive workspace: [`Ident`], [`Permission`], [`NixBuilds`],
|
||||
//! the [`term_msg`] row markers and the [`review`] rule.
|
||||
//! the [`term_msg`] row markers, the [`term_out`] terminal printing helpers and the [`review`] rule.
|
||||
//!
|
||||
//! A zero-dependency (bar `serde`) leaf crate so every wire-type crate
|
||||
//! (`hive-sh4re`, `hive-host-sock`, `hive-core-agent-sock`) and both binaries
|
||||
|
|
@ -15,6 +15,7 @@ pub use permission::{Permission, deserialize_known};
|
|||
|
||||
pub mod review;
|
||||
pub mod term_msg;
|
||||
pub mod term_out;
|
||||
|
||||
/// The environment variable nix uses to hand this process the blacklist of
|
||||
/// names that already mean something to the message layer.
|
||||
|
|
|
|||
67
hive-types/src/term_out.rs
Normal file
67
hive-types/src/term_out.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
//! Printing agent-authored text to an operator's terminal: `hivectl agent
|
||||
//! watch` and `swarmctl agent watch` share these two helpers.
|
||||
|
||||
use std::io::{ErrorKind, Write};
|
||||
|
||||
/// 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.
|
||||
#[must_use]
|
||||
pub fn sanitize(text: &str) -> String {
|
||||
text.chars()
|
||||
.map(|c| {
|
||||
if c.is_control() && c != '\t' {
|
||||
'\u{FFFD}'
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Write one line and flush. `Ok(false)` means the reader went away (a closed
|
||||
/// pipe, as in `watch x | head`), which ends a watch quietly.
|
||||
///
|
||||
/// # Errors
|
||||
/// Any write or flush error other than `BrokenPipe`.
|
||||
pub fn emit(out: &mut impl Write, line: &str) -> std::io::Result<bool> {
|
||||
match writeln!(out, "{line}").and_then(|()| out.flush()) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) if e.kind() == ErrorKind::BrokenPipe => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct Failing(ErrorKind);
|
||||
|
||||
impl 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 sanitize_keeps_tab_and_text_and_replaces_other_controls() {
|
||||
assert_eq!(
|
||||
sanitize("a\u{1b}[2Jb\tc\r\u{9b}d\u{7f}é"),
|
||||
"a\u{FFFD}[2Jb\tc\u{FFFD}\u{FFFD}d\u{FFFD}é"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_pipe_ends_quietly_and_other_write_errors_fail() {
|
||||
let mut ok = Vec::new();
|
||||
assert!(emit(&mut ok, "x").expect("writes"));
|
||||
assert_eq!(ok, b"x\n");
|
||||
assert!(!emit(&mut Failing(ErrorKind::BrokenPipe), "x").expect("not an error"));
|
||||
assert!(emit(&mut Failing(ErrorKind::PermissionDenied), "x").is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue