Merge remote-tracking branch 'origin/farm/58511219/fix-bash-process-substitution-hang'

This commit is contained in:
can1357
2026-07-15 12:38:47 +02:00
3 changed files with 51 additions and 2 deletions
+36 -2
View File
@@ -9,7 +9,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
ffi::OsString, ffi::{OsStr, OsString},
io::{self, Read, Write}, io::{self, Read, Write},
panic::catch_unwind, panic::catch_unwind,
sync::{ sync::{
@@ -18,6 +18,8 @@ use std::{
}, },
}; };
#[cfg(unix)]
use brush_core::ShellFd;
use brush_core::{ use brush_core::{
Error, Error,
builtins::{BoxFuture, ContentOptions, ContentType, Registration}, builtins::{BoxFuture, ContentOptions, ContentType, Registration},
@@ -31,6 +33,34 @@ use brush_core::{
/// command name at index 0) and returns a process-style exit code. /// command name at index 0) and returns a process-style exit code.
type UutilRun = fn(Vec<OsString>) -> i32; type UutilRun = fn(Vec<OsString>) -> i32;
#[cfg(unix)]
fn process_substitution_fd(arg: &OsStr) -> Option<ShellFd> {
let fd = arg.to_str()?.strip_prefix("/dev/fd/")?.parse().ok()?;
(fd > OpenFiles::STDERR_FD).then_some(fd)
}
#[cfg(unix)]
fn materialize_process_substitution_fds<SE: ShellExtensions>(
context: &ExecutionContext<'_, SE>,
argv: &mut [OsString],
) -> Result<Vec<std::os::fd::OwnedFd>, Error> {
use std::os::fd::AsRawFd as _;
let mut fds = Vec::new();
for arg in argv {
let Some(shell_fd) = process_substitution_fd(arg) else {
continue;
};
let Some(file) = context.try_fd(shell_fd) else {
continue;
};
let fd = file.try_borrow_as_fd()?.try_clone_to_owned()?;
*arg = OsString::from(format!("/dev/fd/{}", fd.as_raw_fd()));
fds.push(fd);
}
Ok(fds)
}
/// Drives a patched uutils utility to completion under a [`pi_uutils_ctx`] /// Drives a patched uutils utility to completion under a [`pi_uutils_ctx`]
/// scope derived from the command execution context. /// scope derived from the command execution context.
async fn run_uutil<SE: ShellExtensions>( async fn run_uutil<SE: ShellExtensions>(
@@ -76,14 +106,18 @@ async fn run_uutil<SE: ShellExtensions>(
// brush passes the command name as the first `CommandArg`, which is exactly // brush passes the command name as the first `CommandArg`, which is exactly
// the argv[0] uutils' argument parsing expects. // the argv[0] uutils' argument parsing expects.
let argv: Vec<OsString> = args let mut argv: Vec<OsString> = args
.iter() .iter()
.map(|arg| OsString::from(arg.to_string())) .map(|arg| OsString::from(arg.to_string()))
.collect(); .collect();
#[cfg(unix)]
let process_substitution_fds = materialize_process_substitution_fds(&context, &mut argv)?;
drop(context); drop(context);
let mut handle = tokio::task::spawn_blocking(move || { let mut handle = tokio::task::spawn_blocking(move || {
#[cfg(unix)]
let _process_substitution_fds = process_substitution_fds;
let stdin: Box<dyn Read + Send> = match stdin { let stdin: Box<dyn Read + Send> = match stdin {
Some(file) => Box::new(file), Some(file) => Box::new(file),
None => Box::new(io::empty()), None => Box::new(io::empty()),
+14
View File
@@ -3215,6 +3215,20 @@ mod tests {
path path
} }
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread")]
async fn uutils_diff_reads_process_substitution_fds() {
let (result, output) = time::timeout(
Duration::from_secs(5),
run_command_capture("diff <(echo a) <(echo b)", None, None, CancelToken::default()),
)
.await
.expect("process substitution should not hang");
assert_eq!(result.exit_code, Some(1));
assert!(output.contains("-a\n+b\n"), "diff output missing changed lines: {output:?}");
}
#[cfg(unix)] #[cfg(unix)]
fn printf_minimizer( fn printf_minimizer(
settings_path: &std::path::Path, settings_path: &std::path::Path,
+1
View File
@@ -16,6 +16,7 @@
- Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)). - Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)).
- Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd. - Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd.
- Fixed prewalk repeatedly continuing after a bash-only task such as `commit` had already completed ([#5551](https://github.com/can1357/oh-my-pi/issues/5551)). - Fixed prewalk repeatedly continuing after a bash-only task such as `commit` had already completed ([#5551](https://github.com/can1357/oh-my-pi/issues/5551)).
- Fixed the Bash tool hanging when in-process commands read process substitution operands such as `<(cmd)` ([#5557](https://github.com/can1357/oh-my-pi/issues/5557)).
## [16.5.2] - 2026-07-14 ## [16.5.2] - 2026-07-14