use std::{collections::HashMap, io::Read, sync::Arc}; use bstr::{BStr, BString}; use crate::{ Driver, driver, driver::{Operation, Process, State, process, process::client::invoke},}; /// What to do if delay is supported by a process filter.#[derive(Default, Debug, Copy, Clone)]pub enum Delay { /// Use delayed processing for this entry. /// /// Note that it's up to the filter to determine whether or not the processing should be delayed. #[default] Allow, /// Do not delay the processing, and force it to happen immediately. In this case, no delayed processing will occur /// even if the filter supports it. /// /// This is the default as it requires no special precautions to be taken by the caller as /// outputs will be produced immediately. Forbid,} /// The error returned by [State::apply()][super::State::apply()].#[derive(Debug, thiserror::Error)]#[expect(missing_docs)]pub enum Error { #[error(transparent)] Init(#[from] driver::init::Error), #[error("Could not write entire object to driver")] WriteSource(#[from] std::io::Error), #[error("Filter process delayed an entry even though that was not requested")] DelayNotAllowed, #[error("Failed to invoke '{command}' command")] ProcessInvoke { source: process::client::invoke::Error, command: String, }, #[error("The invoked command '{command}' in process indicated an error: {status:?}")] ProcessStatus { status: driver::process::Status, command: String, },} /// Additional information for use in the [`State::apply()`] method.#[derive(Debug, Copy, Clone)]pub struct Context<'a, 'b> { /// The repo-relative using slashes as separator of the entry currently being processed. pub rela_path: &'a BStr, /// The name of the reference that `HEAD` is pointing to. It's passed to `process` filters if present. pub ref_name: Option<&'b BStr>, /// The root-level tree that contains the current entry directly or indirectly, or the commit owning the tree (if available). /// /// This is passed to `process` filters if present. pub treeish: Option<gix_hash::ObjectId>, /// The actual blob-hash of the data we are processing. It's passed to `process` filters if present. /// /// Note that this hash might be different from the `$Id$` of the respective `ident` filter, as the latter generates the hash itself. pub blob: Option<gix_hash::ObjectId>,} /// Apply operations to filter programs.impl State { /// Apply `operation` of `driver` to the bytes read from `src` and return a reader to immediately consume the output /// produced by the filter. `rela_path` is the repo-relative path of the entry to handle. /// It's possible that the filter stays inactive, in which case the `src` isn't consumed and has to be used by the caller. /// /// Each call to this method will cause the corresponding filter to be invoked unless `driver` indicates a `process` filter, /// which is only launched once and maintained using this state. /// /// Note that it's not an error if there is no filter process for `operation` or if a long-running process doesn't support /// the desired capability. /// /// ### Deviation /// /// If a long-running process returns the 'abort' status after receiving the data, it will be removed similarly to how `git` does it. /// However, if it returns an unsuccessful error status later, it will not be removed, but reports the error only. /// If any other non-'error' status is received, the process will be stopped. But that doesn't happen if such a status is received /// after reading the filtered result. pub fn apply<'a>( &'a mut self, driver: &Driver, src: &mut impl std::io::Read, operation: Operation, ctx: Context<'_, '_>, ) -> Result<Option<Box<dyn std::io::Read + 'a>>, Error> { match self.apply_delayed(driver, src, operation, Delay::Forbid, ctx)? { Some(MaybeDelayed::Delayed(_)) => { unreachable!("we forbid delaying the entry") } Some(MaybeDelayed::Immediate(read)) => Ok(Some(read)), None => Ok(None), } } /// Like [`apply()`](Self::apply()), but use `delay` to determine if the filter result may be delayed or not. /// /// Poll [`list_delayed_paths()`](Self::list_delayed_paths()) until it is empty and query the available paths again. /// Note that even though it's possible, the API assumes that commands aren't mixed when delays are allowed. pub fn apply_delayed<'a>( &'a mut self, driver: &Driver, src: &mut impl std::io::Read, operation: Operation, delay: Delay, ctx: Context<'_, '_>, ) -> Result<Option<MaybeDelayed<'a>>, Error> { match self.maybe_launch_process(driver, operation, ctx.rela_path)? { Some(Process::SingleFile { mut child, command }) => { // To avoid deadlock when the filter immediately echoes input to output (like `cat`), // we need to write to stdin and read from stdout concurrently. If we write all data // to stdin before reading from stdout, and the pipe buffer fills up, both processes // will block: the filter blocks writing to stdout (buffer full), and we block writing // to stdin (waiting for the filter to consume data). // // Solution: Read all data into a buffer, then spawn a thread to write it to stdin // while we can immediately read from stdout. // // TODO(perf): This keeps the entire input in memory until the writer is done. For // required drivers, output remains streamed; for non-required drivers, the input is // retained as a fallback while the entire output is buffered, making peak storage // approximately input plus output (the `Arc` clone does not copy the input). Git's // `apply_single_file_filter()` can instead have its async worker copy directly from // an input file descriptor while the main thread reads output // (`convert.c::filter_buffer_or_fd()`), avoiding an input-sized allocation in that // case. Find a way to similarly pump the borrowed reader concurrently. let mut input_data = Vec::new(); std::io::copy(src, &mut input_data)?; let stdin = child.stdin.take().expect("configured"); let input_data: Arc<[u8]> = input_data.into(); let fallback = (!driver.required).then(|| Arc::clone(&input_data)); let write_thread = WriterThread::write_all_in_background(input_data, stdin)?; Ok(Some(MaybeDelayed::Immediate(Box::new(ReadFilterOutput { inner: child.stdout.take(), child: Some((child, command)), write_thread: Some(write_thread), fallback, buffered: None, })))) } Some(Process::MultiFile { client, key }) => { let command = operation.as_str(); if !client.capabilities().contains(command) { return Ok(None); } let invoke_result = client.invoke( command, &mut [ ("pathname", Some(ctx.rela_path.to_owned())), ("ref", ctx.ref_name.map(ToOwned::to_owned)), ("treeish", ctx.treeish.map(|id| id.to_hex().to_string().into())), ("blob", ctx.blob.map(|id| id.to_hex().to_string().into())), ( "can-delay", match delay { Delay::Allow if client.capabilities().contains("delay") => Some("1".into()), Delay::Forbid | Delay::Allow => None, }, ), ] .into_iter() .filter_map(|(key, value)| value.map(|v| (key, v))), src, ); let status = match invoke_result { Ok(status) => status, Err(err) => { let invoke::Error::Io(io_err) = &err; handle_io_err(io_err, &mut self.running, key.0.as_ref()); return Err(Error::ProcessInvoke { command: command.into(), source: err, }); } }; if status.is_delayed() { if matches!(delay, Delay::Forbid) { return Err(Error::DelayNotAllowed); } Ok(Some(MaybeDelayed::Delayed(key))) } else if status.is_success() { // TODO: find a way to not have to do the 'borrow-dance'. let client = self.running.remove(&key.0).expect("present for borrowcheck dance"); self.running.insert(key.0.clone(), client); let client = self.running.get_mut(&key.0).expect("just inserted"); Ok(Some(MaybeDelayed::Immediate(Box::new(client.as_read())))) } else { let message = status.message().unwrap_or_default(); match message { "abort" => { client.capabilities_mut().remove(command); } "error" => {} _strange => { let client = self.running.remove(&key.0).expect("we definitely have it"); client.into_child().kill().ok(); } } Err(Error::ProcessStatus { command: command.into(), status, }) } } None => Ok(None), } }} /// A type to represent delayed or immediate apply-filter results.pub enum MaybeDelayed<'a> { /// Using the delayed protocol, this entry has been sent to a long-running process and needs to be /// checked for again, later, using the [`driver::Key`] to refer to the filter who owes a response. /// /// Note that the path to the entry is also needed to obtain the filtered result later. Delayed(driver::Key), /// The filtered result can be read from the contained reader right away. /// /// Note that it must be consumed in full or till a read error occurs. Immediate(Box<dyn std::io::Read + 'a>),} /// A helper to manage writing to stdin on a separate thread to avoid deadlock.struct WriterThread { handle: Option<std::thread::JoinHandle<std::io::Result<()>>>,} impl WriterThread { /// Spawn a thread that will write all data from `data` to `stdin`. fn write_all_in_background(data: Arc<[u8]>, mut stdin: std::process::ChildStdin) -> std::io::Result<Self> { let handle = std::thread::Builder::new() .name("gix-filter-stdin-writer".into()) .stack_size(128 * 1024) .spawn(move || { use std::io::Write; stdin.write_all(&data)?; // Explicitly drop stdin to close the pipe and signal EOF to the child drop(stdin); Ok(()) })?; Ok(Self { handle: Some(handle) }) } /// Wait for the writer thread to complete and return any error that occurred. fn join(&mut self) -> std::io::Result<()> { let Some(handle) = self.handle.take() else { return Ok(()); }; handle.join().map_err(|panic_info| { let msg = if let Some(s) = panic_info.downcast_ref::<String>() { format!("Writer thread panicked: {s}") } else if let Some(s) = panic_info.downcast_ref::<&str>() { format!("Writer thread panicked: {s}") } else { "Writer thread panicked while writing to filter stdin".to_string() }; std::io::Error::other(msg) })? }} impl Drop for WriterThread { fn drop(&mut self) { // Best effort join on drop. if let Err(_err) = self.join() { gix_trace::debug!(err = %_err, "Failed to join writer thread during drop"); } }} /// A utility type to facilitate streaming the output of a filter process.struct ReadFilterOutput { inner: Option<std::process::ChildStdout>, /// Present until the process is waited on, then taken so subsequent reads don't wait again. child: Option<(std::process::Child, std::process::Command)>, /// The thread writing to stdin, if any. Must be joined when reading is done. write_thread: Option<WriterThread>, /// Original input to return if a non-required driver fails. fallback: Option<Arc<[u8]>>, /// Fully buffered output of a non-required driver, or its original input after failure. buffered: Option<std::io::Cursor<BufferedOutput>>,} enum BufferedOutput { Filtered(Vec<u8>), Original(Arc<[u8]>),} impl AsRef<[u8]> for BufferedOutput { fn as_ref(&self) -> &[u8] { match self { BufferedOutput::Filtered(data) => data, BufferedOutput::Original(data) => data, } }} impl ReadFilterOutput { /// Buffer all output to verify that the non-required driver succeeded before exposing it, /// falling back to the original input if reading, writing, or the process itself fails. fn buffer_non_required_driver(&mut self, fallback: Arc<[u8]>) -> &mut std::io::Cursor<BufferedOutput> { let mut output = Vec::new(); let read_result = self.inner.take().expect("configured").read_to_end(&mut output); let write_result = self.write_thread.take().map_or(Ok(()), |mut thread| thread.join()); let (mut child, _command) = self.child.take().expect("configured"); let status = child.wait(); // A filter may deliberately close stdin before consuming all input, which makes the writer // see a broken pipe even though the filter produced valid output and exited successfully. // Other write failures invalidate the filtered output; read and exit status are checked below. let write_succeeded = match &write_result { Ok(()) => true, Err(err) => err.kind() == std::io::ErrorKind::BrokenPipe, }; let succeeded = read_result.is_ok() && write_succeeded && status.as_ref().is_ok_and(std::process::ExitStatus::success); if !succeeded { gix_trace::debug!( ?_command, ?read_result, ?write_result, ?status, "Non-required filter driver failed; using original input" ); } self.buffered = Some(std::io::Cursor::new(if succeeded { BufferedOutput::Filtered(output) } else { BufferedOutput::Original(fallback) })); self.buffered.as_mut().expect("just initialized") }} pub(crate) fn handle_io_err(err: &std::io::Error, running: &mut HashMap<BString, process::Client>, process: &BStr) { if matches!( err.kind(), std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::UnexpectedEof ) { running.remove(process).expect("present or we wouldn't be here"); }} impl std::io::Read for ReadFilterOutput { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { if let Some(buffered) = self.buffered.as_mut() { return buffered.read(buf); } if let Some(fallback) = self.fallback.take() { return self.buffer_non_required_driver(fallback).read(buf); } match self.inner.as_mut() { Some(inner) => { let num_read = match inner.read(buf) { Ok(n) => n, Err(e) => { // On read error, ensure we join the writer thread before propagating the error. // This is expected to finish with failure as well as it should fail to write // to the process which now fails to produce output (that we try to read). if let Some(mut write_thread) = self.write_thread.take() { // Try to join but prioritize the original read error if let Err(_thread_err) = write_thread.join() { gix_trace::debug!(thread_err = %_thread_err, read_err = %e, "write to stdin error during failed read"); } } return Err(e); } }; if num_read == 0 { self.inner.take(); // Join the writer thread first to ensure all data has been written // and that resources are freed now. let write_result = self.write_thread.take().map_or(Ok(()), |mut thread| thread.join()); if let Some((mut child, cmd)) = self.child.take() { let status = child.wait()?; if !status.success() { return Err(std::io::Error::other(format!("Driver process {cmd:?} failed"))); } } write_result?; } Ok(num_read) } None => Ok(0), } }}
use std::{collections::HashMap, io::Read, sync::Arc};
use bstr::{BStr, BString};
use crate::{
Driver, driver,
driver::{Operation, Process, State, process, process::client::invoke},
};
/// What to do if delay is supported by a process filter.
#[derive(Default, Debug, Copy, Clone)]
pub enum Delay {
/// Use delayed processing for this entry.
///
/// Note that it's up to the filter to determine whether or not the processing should be delayed.
#[default]
Allow,
/// Do not delay the processing, and force it to happen immediately. In this case, no delayed processing will occur
/// even if the filter supports it.
///
/// This is the default as it requires no special precautions to be taken by the caller as
/// outputs will be produced immediately.
Forbid,
}
/// The error returned by [State::apply()][super::State::apply()].
#[derive(Debug, thiserror::Error)]
#[expect(missing_docs)]
pub enum Error {
#[error(transparent)]
Init(#[from] driver::init::Error),
#[error("Could not write entire object to driver")]
WriteSource(#[from] std::io::Error),
#[error("Filter process delayed an entry even though that was not requested")]
DelayNotAllowed,
#[error("Failed to invoke '{command}' command")]
ProcessInvoke {
source: process::client::invoke::Error,
command: String,
},
#[error("The invoked command '{command}' in process indicated an error: {status:?}")]
ProcessStatus {
status: driver::process::Status,
command: String,
},
}
/// Additional information for use in the [`State::apply()`] method.
#[derive(Debug, Copy, Clone)]
pub struct Context<'a, 'b> {
/// The repo-relative using slashes as separator of the entry currently being processed.
pub rela_path: &'a BStr,
/// The name of the reference that `HEAD` is pointing to. It's passed to `process` filters if present.
pub ref_name: Option<&'b BStr>,
/// The root-level tree that contains the current entry directly or indirectly, or the commit owning the tree (if available).
///
/// This is passed to `process` filters if present.
pub treeish: Option<gix_hash::ObjectId>,
/// The actual blob-hash of the data we are processing. It's passed to `process` filters if present.
///
/// Note that this hash might be different from the `$Id$` of the respective `ident` filter, as the latter generates the hash itself.
pub blob: Option<gix_hash::ObjectId>,
}
/// Apply operations to filter programs.
impl State {
/// Apply `operation` of `driver` to the bytes read from `src` and return a reader to immediately consume the output
/// produced by the filter. `rela_path` is the repo-relative path of the entry to handle.
/// It's possible that the filter stays inactive, in which case the `src` isn't consumed and has to be used by the caller.
///
/// Each call to this method will cause the corresponding filter to be invoked unless `driver` indicates a `process` filter,
/// which is only launched once and maintained using this state.
///
/// Note that it's not an error if there is no filter process for `operation` or if a long-running process doesn't support
/// the desired capability.
///
/// ### Deviation
///
/// If a long-running process returns the 'abort' status after receiving the data, it will be removed similarly to how `git` does it.
/// However, if it returns an unsuccessful error status later, it will not be removed, but reports the error only.
/// If any other non-'error' status is received, the process will be stopped. But that doesn't happen if such a status is received
/// after reading the filtered result.
pub fn apply<'a>(
&'a mut self,
driver: &Driver,
src: &mut impl std::io::Read,
operation: Operation,
ctx: Context<'_, '_>,
) -> Result<Option<Box<dyn std::io::Read + 'a>>, Error> {
match self.apply_delayed(driver, src, operation, Delay::Forbid, ctx)? {
Some(MaybeDelayed::Delayed(_)) => {
unreachable!("we forbid delaying the entry")
}
Some(MaybeDelayed::Immediate(read)) => Ok(Some(read)),
None => Ok(None),
}
}
/// Like [`apply()`](Self::apply()), but use `delay` to determine if the filter result may be delayed or not.
///
/// Poll [`list_delayed_paths()`](Self::list_delayed_paths()) until it is empty and query the available paths again.
/// Note that even though it's possible, the API assumes that commands aren't mixed when delays are allowed.
pub fn apply_delayed<'a>(
&'a mut self,
driver: &Driver,
src: &mut impl std::io::Read,
operation: Operation,
delay: Delay,
ctx: Context<'_, '_>,
) -> Result<Option<MaybeDelayed<'a>>, Error> {
match self.maybe_launch_process(driver, operation, ctx.rela_path)? {
Some(Process::SingleFile { mut child, command }) => {
// To avoid deadlock when the filter immediately echoes input to output (like `cat`),
// we need to write to stdin and read from stdout concurrently. If we write all data
// to stdin before reading from stdout, and the pipe buffer fills up, both processes
// will block: the filter blocks writing to stdout (buffer full), and we block writing
// to stdin (waiting for the filter to consume data).
//
// Solution: Read all data into a buffer, then spawn a thread to write it to stdin
// while we can immediately read from stdout.
//
// TODO(perf): This keeps the entire input in memory until the writer is done. For
// required drivers, output remains streamed; for non-required drivers, the input is
// retained as a fallback while the entire output is buffered, making peak storage
// approximately input plus output (the `Arc` clone does not copy the input). Git's
// `apply_single_file_filter()` can instead have its async worker copy directly from
// an input file descriptor while the main thread reads output
// (`convert.c::filter_buffer_or_fd()`), avoiding an input-sized allocation in that
// case. Find a way to similarly pump the borrowed reader concurrently.
let mut input_data = Vec::new();
std::io::copy(src, &mut input_data)?;
let stdin = child.stdin.take().expect("configured");
let input_data: Arc<[u8]> = input_data.into();
let fallback = (!driver.required).then(|| Arc::clone(&input_data));
let write_thread = WriterThread::write_all_in_background(input_data, stdin)?;
Ok(Some(MaybeDelayed::Immediate(Box::new(ReadFilterOutput {
inner: child.stdout.take(),
child: Some((child, command)),
write_thread: Some(write_thread),
fallback,
buffered: None,
}))))
}
Some(Process::MultiFile { client, key }) => {
let command = operation.as_str();
if !client.capabilities().contains(command) {
return Ok(None);
}
let invoke_result = client.invoke(
command,
&mut [
("pathname", Some(ctx.rela_path.to_owned())),
("ref", ctx.ref_name.map(ToOwned::to_owned)),
("treeish", ctx.treeish.map(|id| id.to_hex().to_string().into())),
("blob", ctx.blob.map(|id| id.to_hex().to_string().into())),
(
"can-delay",
match delay {
Delay::Allow if client.capabilities().contains("delay") => Some("1".into()),
Delay::Forbid | Delay::Allow => None,
},
),
]
.into_iter()
.filter_map(|(key, value)| value.map(|v| (key, v))),
src,
);
let status = match invoke_result {
Ok(status) => status,
Err(err) => {
let invoke::Error::Io(io_err) = &err;
handle_io_err(io_err, &mut self.running, key.0.as_ref());
return Err(Error::ProcessInvoke {
command: command.into(),
source: err,
});
}
};
if status.is_delayed() {
if matches!(delay, Delay::Forbid) {
return Err(Error::DelayNotAllowed);
}
Ok(Some(MaybeDelayed::Delayed(key)))
} else if status.is_success() {
// TODO: find a way to not have to do the 'borrow-dance'.
let client = self.running.remove(&key.0).expect("present for borrowcheck dance");
self.running.insert(key.0.clone(), client);
let client = self.running.get_mut(&key.0).expect("just inserted");
Ok(Some(MaybeDelayed::Immediate(Box::new(client.as_read()))))
} else {
let message = status.message().unwrap_or_default();
match message {
"abort" => {
client.capabilities_mut().remove(command);
}
"error" => {}
_strange => {
let client = self.running.remove(&key.0).expect("we definitely have it");
client.into_child().kill().ok();
}
}
Err(Error::ProcessStatus {
command: command.into(),
status,
})
}
}
None => Ok(None),
}
}
}
/// A type to represent delayed or immediate apply-filter results.
pub enum MaybeDelayed<'a> {
/// Using the delayed protocol, this entry has been sent to a long-running process and needs to be
/// checked for again, later, using the [`driver::Key`] to refer to the filter who owes a response.
///
/// Note that the path to the entry is also needed to obtain the filtered result later.
Delayed(driver::Key),
/// The filtered result can be read from the contained reader right away.
///
/// Note that it must be consumed in full or till a read error occurs.
Immediate(Box<dyn std::io::Read + 'a>),
}
/// A helper to manage writing to stdin on a separate thread to avoid deadlock.
struct WriterThread {
handle: Option<std::thread::JoinHandle<std::io::Result<()>>>,
}
impl WriterThread {
/// Spawn a thread that will write all data from `data` to `stdin`.
fn write_all_in_background(data: Arc<[u8]>, mut stdin: std::process::ChildStdin) -> std::io::Result<Self> {
let handle = std::thread::Builder::new()
.name("gix-filter-stdin-writer".into())
.stack_size(128 * 1024)
.spawn(move || {
use std::io::Write;
stdin.write_all(&data)?;
// Explicitly drop stdin to close the pipe and signal EOF to the child
drop(stdin);
Ok(())
})?;
Ok(Self { handle: Some(handle) })
}
/// Wait for the writer thread to complete and return any error that occurred.
fn join(&mut self) -> std::io::Result<()> {
let Some(handle) = self.handle.take() else {
return Ok(());
};
handle.join().map_err(|panic_info| {
let msg = if let Some(s) = panic_info.downcast_ref::<String>() {
format!("Writer thread panicked: {s}")
} else if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("Writer thread panicked: {s}")
} else {
"Writer thread panicked while writing to filter stdin".to_string()
};
std::io::Error::other(msg)
})?
}
}
impl Drop for WriterThread {
fn drop(&mut self) {
// Best effort join on drop.
if let Err(_err) = self.join() {
gix_trace::debug!(err = %_err, "Failed to join writer thread during drop");
}
}
}
/// A utility type to facilitate streaming the output of a filter process.
struct ReadFilterOutput {
inner: Option<std::process::ChildStdout>,
/// Present until the process is waited on, then taken so subsequent reads don't wait again.
child: Option<(std::process::Child, std::process::Command)>,
/// The thread writing to stdin, if any. Must be joined when reading is done.
write_thread: Option<WriterThread>,
/// Original input to return if a non-required driver fails.
fallback: Option<Arc<[u8]>>,
/// Fully buffered output of a non-required driver, or its original input after failure.
buffered: Option<std::io::Cursor<BufferedOutput>>,
}
enum BufferedOutput {
Filtered(Vec<u8>),
Original(Arc<[u8]>),
}
impl AsRef<[u8]> for BufferedOutput {
fn as_ref(&self) -> &[u8] {
match self {
BufferedOutput::Filtered(data) => data,
BufferedOutput::Original(data) => data,
}
}
}
impl ReadFilterOutput {
/// Buffer all output to verify that the non-required driver succeeded before exposing it,
/// falling back to the original input if reading, writing, or the process itself fails.
fn buffer_non_required_driver(&mut self, fallback: Arc<[u8]>) -> &mut std::io::Cursor<BufferedOutput> {
let mut output = Vec::new();
let read_result = self.inner.take().expect("configured").read_to_end(&mut output);
let write_result = self.write_thread.take().map_or(Ok(()), |mut thread| thread.join());
let (mut child, _command) = self.child.take().expect("configured");
let status = child.wait();
// A filter may deliberately close stdin before consuming all input, which makes the writer
// see a broken pipe even though the filter produced valid output and exited successfully.
// Other write failures invalidate the filtered output; read and exit status are checked below.
let write_succeeded = match &write_result {
Ok(()) => true,
Err(err) => err.kind() == std::io::ErrorKind::BrokenPipe,
};
let succeeded =
read_result.is_ok() && write_succeeded && status.as_ref().is_ok_and(std::process::ExitStatus::success);
if !succeeded {
gix_trace::debug!(
?_command,
?read_result,
?write_result,
?status,
"Non-required filter driver failed; using original input"
);
}
self.buffered = Some(std::io::Cursor::new(if succeeded {
BufferedOutput::Filtered(output)
} else {
BufferedOutput::Original(fallback)
}));
self.buffered.as_mut().expect("just initialized")
}
}
pub(crate) fn handle_io_err(err: &std::io::Error, running: &mut HashMap<BString, process::Client>, process: &BStr) {
if matches!(
err.kind(),
std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::UnexpectedEof
) {
running.remove(process).expect("present or we wouldn't be here");
}
}
impl std::io::Read for ReadFilterOutput {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if let Some(buffered) = self.buffered.as_mut() {
return buffered.read(buf);
}
if let Some(fallback) = self.fallback.take() {
return self.buffer_non_required_driver(fallback).read(buf);
}
match self.inner.as_mut() {
Some(inner) => {
let num_read = match inner.read(buf) {
Ok(n) => n,
Err(e) => {
// On read error, ensure we join the writer thread before propagating the error.
// This is expected to finish with failure as well as it should fail to write
// to the process which now fails to produce output (that we try to read).
if let Some(mut write_thread) = self.write_thread.take() {
// Try to join but prioritize the original read error
if let Err(_thread_err) = write_thread.join() {
gix_trace::debug!(thread_err = %_thread_err, read_err = %e, "write to stdin error during failed read");
}
}
return Err(e);
}
};
if num_read == 0 {
self.inner.take();
// Join the writer thread first to ensure all data has been written
// and that resources are freed now.
let write_result = self.write_thread.take().map_or(Ok(()), |mut thread| thread.join());
if let Some((mut child, cmd)) = self.child.take() {
let status = child.wait()?;
if !status.success() {
return Err(std::io::Error::other(format!("Driver process {cmd:?} failed")));
}
}
write_result?;
}
Ok(num_read)
}
None => Ok(0),
}
}
}