Merge remote-tracking branch 'origin/farm/58511219/fix-bash-process-substitution-hang'
This commit is contained in:
@@ -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()),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user