diff --git a/src/reporter.rs b/src/reporter.rs index e4b0623..b38a45e 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -506,6 +506,22 @@ pub fn run( ds: Arc>, args: &cli::commands::scan::ScanArgs, audit_context: Option, +) -> Result<()> { + let writer = args.output_args.get_writer()?; + run_with_writer(global_args, ds, args, audit_context, writer) +} + +/// Same as [`run`], but writes into a caller-provided `Write` instead of +/// constructing one from `args.output_args`. Useful when the caller wants +/// to render into an in-memory buffer first (e.g. so a stdout lock can be +/// held only around the final atomic emit, not around the report's CPU +/// work). +pub fn run_with_writer( + global_args: &GlobalArgs, + ds: Arc>, + args: &cli::commands::scan::ScanArgs, + audit_context: Option, + writer: W, ) -> Result<()> { global_args.use_color(std::io::stdout()); let stdout_is_tty = std::io::stdout().is_terminal(); @@ -513,11 +529,8 @@ pub fn run( let styles = Styles::new(use_color); let ds_clone = Arc::clone(&ds); - // Initialize the reporter let reporter = DetailsReporter { datastore: ds_clone, styles, only_valid: args.only_valid, audit_context }; - let writer = args.output_args.get_writer()?; - // Generate and write the report in the specified format reporter.report(args.output_args.format, writer, args) } pub struct DetailsReporter { diff --git a/src/scanner/runner.rs b/src/scanner/runner.rs index 5ae9b2c..fbfe8d1 100644 --- a/src/scanner/runner.rs +++ b/src/scanner/runner.rs @@ -942,20 +942,27 @@ async fn run_parallel_scan( if !output_to_file { // Per-repo emit goes to stdout from many rayon - // threads in parallel. Hold stdout's reentrant - // lock for the duration of `reporter::run` so - // the report's writes (and the eventual - // `BufWriter::flush` on drop) can't - // interleave with another thread's report, - // which would otherwise corrupt JSONL output. - let _stdout_lock = std::io::stdout().lock(); - crate::reporter::run( + // threads in parallel. Render the report into + // an in-memory buffer first (CPU work, no + // contention), then take the stdout lock only + // around the final atomic write+flush so two + // threads' envelopes can't interleave and + // corrupt JSONL output. + let mut buf: Vec = Vec::with_capacity(8 * 1024); + crate::reporter::run_with_writer( global_args, Arc::clone(&repo_datastore), &args, None, + &mut buf, ) .context("Failed to run report command")?; + if !buf.is_empty() { + use std::io::Write; + let mut stdout = std::io::stdout().lock(); + stdout.write_all(&buf)?; + stdout.flush()?; + } } {