Skip to content

fix(read): skip terminal settings for non-terminal input - #1259

Open
Jpei1994 wants to merge 1 commit into
reubeno:mainfrom
Jpei1994:fix/read-os-error-87
Open

Jpei1994 wants to merge 1 commit into
reubeno:mainfrom
Jpei1994:fix/read-os-error-87

Conversation

@Jpei1994

@Jpei1994 Jpei1994 commented Aug 5, 2026

Copy link
Copy Markdown

Problem

On Windows, read fails with os error 87 (ERROR_INVALID_PARAMETER) whenever the input is a pipe, file, or redirect AND the host process's standard handles are a console. This is the common case for an agent shell hosted in a terminal (e.g. a coding agent running printf 'x\n' | while read -r line; ...), where the shell runs in-process in the TUI host.

Root cause

setup_terminal_settings always calls AutoModeGuard::new(file) and applies settings. AutoModeGuard::new's from_term keys off the process's standard handles (GetStdHandle) rather than the file argument — so in a terminal-hosted process it succeeds even when the read input is a pipe/file/redirect. apply_settings then calls SetConsoleMode on the non-console input, which fails with ERROR_INVALID_PARAMETER, and the ? propagates the failure, failing the whole read.

Subprocesses never hit this because their stdin is a pipe, so AutoModeGuard::new fails and the .ok() degrades to no-op.

Fix

Skip terminal-mode setup entirely for non-terminal input (if !file.is_terminal() { return Ok(None); }), matching bash's termios behavior (bash only applies terminal settings to terminal input).

Tests

Two contract tests: pipe input and file input both return Ok(None) (no mode guard installed, no error).

Verified on Windows: after this change printf 'ok\n' | while IFS= read -r x; do echo "$x"; done works in the terminal-hosted agent shell.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Performance Benchmark Report

Benchmark name Baseline (μs) Test/PR (μs) Delta (μs) Delta %
clone_shell_object 17.52 μs 17.88 μs 0.36 μs 🟠 +2.08%
eval_arithmetic 0.15 μs 0.15 μs -0.00 μs ⚪ Unchanged
expand_one_string 1.74 μs 1.68 μs -0.06 μs ⚪ Unchanged
for_loop 32.60 μs 32.79 μs 0.19 μs ⚪ Unchanged
full_peg_complex 55.56 μs 56.39 μs 0.83 μs ⚪ Unchanged
full_peg_for_loop 6.07 μs 6.11 μs 0.04 μs ⚪ Unchanged
full_peg_nested_expansions 16.31 μs 16.10 μs -0.21 μs 🟢 -1.26%
full_peg_pipeline 4.07 μs 4.16 μs 0.08 μs ⚪ Unchanged
full_peg_simple 1.76 μs 1.77 μs 0.01 μs ⚪ Unchanged
function_call 3.49 μs 3.54 μs 0.05 μs ⚪ Unchanged
instantiate_shell 57.12 μs 57.62 μs 0.51 μs ⚪ Unchanged
instantiate_shell_with_init_scripts 28530.83 μs 29032.47 μs 501.64 μs ⚪ Unchanged
parse_peg_bash_completion 2073.88 μs 2091.64 μs 17.76 μs ⚪ Unchanged
parse_peg_complex 18.53 μs 18.70 μs 0.16 μs ⚪ Unchanged
parse_peg_for_loop 1.79 μs 1.79 μs 0.01 μs ⚪ Unchanged
parse_peg_pipeline 1.89 μs 1.95 μs 0.06 μs ⚪ Unchanged
parse_peg_simple 1.02 μs 1.01 μs -0.00 μs ⚪ Unchanged
run_echo_builtin_command 16.00 μs 16.02 μs 0.02 μs ⚪ Unchanged
tokenize_sample_script 3.62 μs 3.53 μs -0.09 μs ⚪ Unchanged

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
brush-builtins/src/read.rs 🟢 91.38% 🟢 90.72% 🔴 -0.66%
brush-test-harness/src/comparison.rs 🟢 83.97% 🟢 76.92% 🔴 -7.05%
brush-test-harness/src/execution.rs 🟢 83.17% 🟢 80.2% 🔴 -2.97%
brush-test-harness/src/reporting.rs 🔴 46.48% 🔴 11.62% 🔴 -34.86%
brush-test-harness/src/runner.rs 🟢 84.77% 🟢 84.17% 🔴 -0.6%
brush-test-harness/src/util.rs 🟠 71.64% 🔴 47.76% 🔴 -23.88%
Overall Coverage 🟢 75.75% 🟢 75.18% 🔴 -0.57%

Minimum allowed coverage is 70%, this run produced 75.18%
Maximum allowed coverage difference is -5%, this run produced -0.57%

Test Summary: bash-completion test suite

Outcome Count Percentage
✅ Pass 1581 74.96
❗️ Error 17 0.81
❌ Fail 157 7.44
⏩ Skip 339 16.07
❎ Expected Fail 13 0.62
✔️ Unexpected Pass 2 0.09
📊 Total 2109 100.00

@reubeno reubeno left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together! I've got a few questions/requests. My biggest question is what the actual call stack of the failing API looks like so I can better understand whether this is the best place for the check.


#[test]
fn test_setup_terminal_settings_skips_file_input() {
let mut tmp = std::env::temp_dir();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of manually creating a file in the temp dir and cleaning it up, could you please use tempfile::tempfile? We use that crate elsewhere in testing to simplify auto-cleanup.


use super::*;

// ==================== setup_terminal_settings tests ====================

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these tests!

// Terminal settings only make sense for terminal input. `AutoModeGuard::new`
// keys off the process's standard handles, which may be a console even when
// the read input is a pipe/file/redirect (e.g. an agent shell hosted in a
// terminal): in that case `apply_settings` would hit `SetConsoleMode` on the

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify where the call to SetConsoleMode happens -- or what the caller is? I don't see any direct calls to it in brush, nor do we have any Windows-specific console paths right now. (Perhaps I've overlooked something, though?)

&self,
file: &brush_core::openfiles::OpenFile,
) -> Result<Option<brush_core::terminal::AutoModeGuard>, brush_core::Error> {
// Terminal settings only make sense for terminal input. `AutoModeGuard::new`

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta-note: this comment is Windows-centric; could you simplify it and clarify which parts are platform specific?

@reubeno reubeno added the state: updates requested Pull requests with updates requested label Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state: updates requested Pull requests with updates requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants