Skip to content

Broken pipe when using head in various dz commands #3387

@martinsander00

Description

@martinsander00

This has been an issue in some monorepo user commands but also in our new doublezero-solana commands in the offchain repo. Rust's runtime ignores SIGPIPE at startup, which causes println! to panic instead of the process dying silently.

Example:

$ doublezero-solana shreds price | head -n 5
95 device(s) found:
...
thread 'main' panicked at library/std/src/io/stdio.rs:1165:9:
failed printing to stdout: Broken pipe (os error 32)

Below are three ways to restore sane broken-pipe behavior. Want to gather feedback because this requires changes in how we log stuff (println!writeln! + error handling) or in the main files of a few components that use the CLI.


Option 1 — Reset SIGPIPE to default (libc::signal)

Rust's runtime sets SIGPIPE to SIG_IGN before fn main(). This restores the default Unix behavior by calling libc::signal at the start of main, making the process behave like a C program, it gets killed silently when a pipe reader closes. This is the standard approach used by tools like ripgrep, bat, and fd.

fn reset_sigpipe() {
    #[cfg(unix)]
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }
}

Requires unsafe and libc as a direct dependency (which we already have). Fixes all println! calls globally.


Option 2 — Custom panic hook

Since println! panics on write errors rather than returning a Result, we can intercept the panic before it prints the error. This replaces the default panic handler with one that detects broken pipe panics and exits the process cleanly with code 0. The hook is set at the start of main.

let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
    let is_broken_pipe = info
        .payload()
        .downcast_ref::<String>()
        .map(|s| s.contains("Broken pipe"))
        .unwrap_or(false);
    if is_broken_pipe {
        std::process::exit(0);
    }
    default_hook(info);
}));

No unsafe, no new dependencies, fixes all println! calls globally. Relies on string-matching the panic message, which could break if Rust changes the wording.


Option 3 — Replace println! with writeln! + catch in main

Instead of using println! which panics on errors, use writeln! which returns a Result that can be propagated with ?. The broken pipe error bubbles up to main, where we catch it and exit silently instead of printing an error trace.

// In commands:
let mut stdout = std::io::stdout().lock();
writeln!(stdout, "{table}")?;

// In main:
Err(err)
    if err
        .downcast_ref::<std::io::Error>()
        .is_some_and(|e| e.kind() == std::io::ErrorKind::BrokenPipe) =>
{
    Ok(())
}

No unsafe, no new dependencies, explicit error handling. Requires changing every println! to writeln! in affected commands — only protects the commands you change.


Affected components

  • offchain repodoublezero-solana binary (crates/solana-cli/src/main.rs)
  • monorepodoublezero_cli library consumed by client/doublezero, activator, controlplane/doublezero-admin, client/doublezero-geolocation-cli

For the monorepo, options 1 and 2 would need to be added to each binary's main.rs. Option 3 could be handled inside the library itself (so all binaries get the fix for free).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions