mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
CLI: a closed stdout ends vectorcraft-cli quietly
`vectorcraft-cli commands | head -1` panicked and exited with status 101: thread 'main' panicked at library/std/src/io/stdio.rs:1168:9: failed printing to stdout: Broken pipe (os error 32) println! and print! panic when stdout is closed. Output now goes through outln! and out!, which end the program with status 0 on a broken pipe (the reader has what it wanted) and report any other write error on stderr with status 1. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2c02502f43
commit
32f73e9cce
3 files changed
+57
-13
No files matched your search
@@ -14,11 +14,41 @@
|
||||
use std::io::Write;
|
||||
use std::process::ExitCode;
|
||||
|
||||
/// `println!` / `print!` that end the program quietly when stdout is closed
|
||||
/// (`vectorcraft-cli commands | head`) instead of panicking with "failed printing to stdout:
|
||||
/// Broken pipe (os error 32)".
|
||||
macro_rules! outln {
|
||||
($($arg:tt)*) => {{
|
||||
use std::io::Write as _;
|
||||
if let Err(e) = writeln!(std::io::stdout(), $($arg)*) {
|
||||
$crate::stdout_failed(e);
|
||||
}
|
||||
}};
|
||||
}
|
||||
macro_rules! out {
|
||||
($($arg:tt)*) => {{
|
||||
use std::io::Write as _;
|
||||
if let Err(e) = write!(std::io::stdout(), $($arg)*) {
|
||||
$crate::stdout_failed(e);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
mod perf;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_mcp::{Backend, DEFAULT_ADDR, Headless, Remote, Server};
|
||||
|
||||
/// stdout went away. A reader that stopped early (a closed pipe) ends the program quietly, as
|
||||
/// ripgrep does; any other write error is reported.
|
||||
fn stdout_failed(e: std::io::Error) -> ! {
|
||||
if e.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
std::process::exit(0);
|
||||
}
|
||||
eprintln!("vectorcraft-cli: can't write to stdout: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
const USAGE: &str = "\
|
||||
vectorcraft-cli — VectorCraft automation
|
||||
|
||||
@@ -68,11 +98,11 @@ fn main() -> ExitCode {
|
||||
Some("bench") => bench(&args[1..]),
|
||||
Some("perf") => perf::run(&args[1..]),
|
||||
Some("-h" | "--help" | "help") | None => {
|
||||
print!("{}", usage());
|
||||
out!("{}", usage());
|
||||
Ok(())
|
||||
}
|
||||
Some("-V" | "--version") => {
|
||||
println!("vectorcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
outln!("vectorcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
}
|
||||
Some(other) => Err(format!("unknown subcommand `{other}`\n\n{}", usage())),
|
||||
@@ -120,7 +150,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
|
||||
fn commands() -> Result<(), String> {
|
||||
let mut h = Headless::new();
|
||||
let v = h.call("engine.commands", json!({}))?;
|
||||
println!("{}", serde_json::to_string_pretty(&v).unwrap_or_default());
|
||||
outln!("{}", serde_json::to_string_pretty(&v).unwrap_or_default());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -146,7 +176,7 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
json!({"command": "document.export", "params": {"path": output, "scale": scale, "artboard": artboard, "range": range, "outlineText": outline_text}}),
|
||||
)
|
||||
.map_err(|e| format!("export {output}: {e}"))?;
|
||||
println!("{r}");
|
||||
outln!("{r}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -162,7 +192,7 @@ fn info(args: &[String]) -> Result<(), String> {
|
||||
let artboards: Vec<Value> =
|
||||
doc.artboards.iter().map(|a| json!({"name": a.name, "rect": [a.rect.x0, a.rect.y0, a.rect.width(), a.rect.height()]})).collect();
|
||||
let v = json!({"file": file, "info": base, "artboards": artboards, "kinds": kinds, "fonts": fonts});
|
||||
println!("{}", serde_json::to_string_pretty(&v).unwrap_or_default());
|
||||
outln!("{}", serde_json::to_string_pretty(&v).unwrap_or_default());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -252,7 +282,7 @@ fn bench(args: &[String]) -> Result<(), String> {
|
||||
* vectorcraft_geom::Affine::scale(z)
|
||||
* vectorcraft_geom::Affine::translate(-b.center().to_vec2());
|
||||
let opts = vectorcraft_render::RenderOptions::default();
|
||||
println!("{file}: {} nodes, {w}x{h}", doc.layers.iter().map(|l| l.count()).sum::<usize>());
|
||||
outln!("{file}: {} nodes, {w}x{h}", doc.layers.iter().map(|l| l.count()).sum::<usize>());
|
||||
for threads in [vectorcraft_render::default_threads(), 0] {
|
||||
let mut r = vectorcraft_render::Renderer::new();
|
||||
r.threads = threads;
|
||||
@@ -261,7 +291,7 @@ fn bench(args: &[String]) -> Result<(), String> {
|
||||
for _ in 0..iters {
|
||||
r.render(&doc, w, h, view, &opts);
|
||||
}
|
||||
println!(
|
||||
outln!(
|
||||
" threads {threads}: {:.1} ms/frame (drawn {}, culled {})",
|
||||
t.elapsed().as_secs_f64() * 1000.0 / iters as f64,
|
||||
r.stats.drawn,
|
||||
|
||||
@@ -81,14 +81,14 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let cores = std::thread::available_parallelism().map_or(1, |c| c.get());
|
||||
let load = load_average();
|
||||
println!("VectorCraft performance budgets — {n} paths, {cores} cores, load average {}", load.map_or("?".into(), |l| format!("{l:.1}")));
|
||||
outln!("VectorCraft performance budgets — {n} paths, {cores} cores, load average {}", load.map_or("?".into(), |l| format!("{l:.1}")));
|
||||
let noisy = load.is_some_and(|l| l > cores as f64 * 0.75);
|
||||
if noisy {
|
||||
println!("WARNING: the machine is busy; wall-clock timings below are not trustworthy.");
|
||||
outln!("WARNING: the machine is busy; wall-clock timings below are not trustworthy.");
|
||||
}
|
||||
let t = Instant::now();
|
||||
let doc = synthetic(n)?;
|
||||
println!(" (built in {:.0} ms)", t.elapsed().as_secs_f64() * 1000.0);
|
||||
outln!(" (built in {:.0} ms)", t.elapsed().as_secs_f64() * 1000.0);
|
||||
|
||||
let mut rows: Vec<(&str, f64, f64)> = vec![];
|
||||
let opts = RenderOptions::default();
|
||||
@@ -150,16 +150,16 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let mut over = 0;
|
||||
println!(" {:<40} {:>10} {:>10}", "", "measured", "budget");
|
||||
outln!(" {:<40} {:>10} {:>10}", "", "measured", "budget");
|
||||
for (name, ms, budget) in &rows {
|
||||
let ok = ms <= budget;
|
||||
over += usize::from(!ok);
|
||||
println!(" {name:<40} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" });
|
||||
outln!(" {name:<40} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" });
|
||||
}
|
||||
match (over, noisy) {
|
||||
(0, _) => Ok(()),
|
||||
(_, true) => {
|
||||
println!("{over} over budget, but the machine is busy: re-run when idle.");
|
||||
outln!("{over} over budget, but the machine is busy: re-run when idle.");
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!("{over} budget(s) exceeded")),
|
||||
|
||||
@@ -82,3 +82,17 @@ fn mcp_headless_over_stdio() {
|
||||
assert_eq!(replies[2]["result"]["isError"], false);
|
||||
assert_eq!(replies[3]["result"]["content"][0]["type"], "image");
|
||||
}
|
||||
|
||||
/// `vectorcraft-cli commands | head -1` panicked with "failed printing to
|
||||
/// stdout: Broken pipe (os error 32)" and exit status 101.
|
||||
#[test]
|
||||
fn closed_stdout_ends_quietly() {
|
||||
use std::io::Read;
|
||||
let mut child = Command::new(BIN).arg("commands").stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
||||
let mut first = [0u8; 16];
|
||||
child.stdout.as_mut().unwrap().read_exact(&mut first).unwrap();
|
||||
drop(child.stdout.take());
|
||||
let out = child.wait_with_output().unwrap();
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(out.status.success() && !err.contains("panicked"), "{:?}: {err}", out.status);
|
||||
}
|
||||
Reference in new issue
Block a user