Skip to content

fix: launch external background jobs before shell exit#1139

Draft
cataggar wants to merge 2 commits into
reubeno:mainfrom
cataggar:fix/background-jobs-launch-before-exit
Draft

fix: launch external background jobs before shell exit#1139
cataggar wants to merge 2 commits into
reubeno:mainfrom
cataggar:fix/background-jobs-launch-before-exit

Conversation

@cataggar

@cataggar cataggar commented May 5, 2026

Copy link
Copy Markdown
Contributor

Made by GitHub Copilot.

Summary

  • Spawn eligible simple external background commands before returning from the async separator path so short-lived -c/stdin shell invocations do not drop them when brush exits.
  • Register those spawned children as external job tasks, preserving wait/job-manager behavior without adding an implicit wait on shell exit.
  • Unskip the simple external background job compatibility coverage and add guards that long-running background jobs do not delay shell exit.
  • Leave compound background async lists skipped with a clearer note because they need durable background subshell execution as a follow-up.

Manual testing

Before the fix:

  • Built the current debug binary with cargo build --package brush-shell --quiet.
  • In a temp directory, ran target/debug/brush --norc --noprofile --no-config --disable-color -c 'touch bg_done &', slept briefly, and confirmed bg_done was missing.
  • Ran the bash control bash --norc --noprofile -c 'touch bg_done &', slept briefly, and confirmed bg_done was present.
  • Timed bash --norc --noprofile -c 'sleep 1 &' and confirmed bash returned immediately (~0.008s).

After the fix:

  • Re-ran the direct brush temp-directory repro and confirmed bg_done is present.
  • Timed target/debug/brush --norc --noprofile --no-config --disable-color -c 'sleep 1 &' and confirmed brush returns immediately (~0.023s).

Automated testing

  • cargo test --package brush-shell --test brush-compat-tests -- "without explicit wait" --nocapture
  • cargo test --package brush-shell --test brush-compat-tests -- "does not delay shell exit" --nocapture
  • cargo test --package brush-shell --test brush-compat-tests -- "Background jobs::" --nocapture
  • cargo xtask ci quick
    • Format, build, and clippy passed.
    • The unit-test phase initially failed because cargo-nextest was not installed in this environment.
  • Installed cargo-nextest 0.9.133.
  • cargo nextest run --workspace --no-fail-fast -E 'not binary(brush-compat-tests) and not binary(brush-interactive-tests) and not binary(brush-completion-tests)'
    • 352 tests run: 352 passed, 1 skipped.
  • cargo test --workspace --exclude brush-fuzz --lib --bins --examples --no-fail-fast

Manual verification steps

  1. Build brush: cargo build --package brush-shell --quiet.
  2. Verify a background command launched via -c survives shell exit:
    repo=$(pwd); tmp=$(mktemp -d); (cd "$tmp" && "$repo/target/debug/brush" --norc --noprofile --no-config --disable-color -c 'touch bg_done &'); sleep 0.2; test -f "$tmp/bg_done".
  3. Verify the stdin path similarly:
    repo=$(pwd); tmp=$(mktemp -d); (cd "$tmp" && printf 'touch bg_done &\n' | "$repo/target/debug/brush" --norc --noprofile --no-config --disable-color); sleep 0.2; test -f "$tmp/bg_done".
  4. Verify shell exit is not delayed by a long-running background job:
    time target/debug/brush --norc --noprofile --no-config --disable-color -c 'sleep 5 >/dev/null 2>&1 &'.
  5. Run the targeted compatibility case set:
    cargo test --package brush-shell --test brush-compat-tests -- "Background jobs::" --nocapture.

Addresses #1079.

Spawn simple external background commands before returning from the async separator path so short-lived shells do not drop them when the runtime exits. Keep compound background subshells documented as a follow-up.

Assisted-by: GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Performance Benchmark Report

Benchmark name Baseline (μs) Test/PR (μs) Delta (μs) Delta %
clone_shell_object 17.95 μs 18.27 μs 0.33 μs 🟠 +1.83%
eval_arithmetic 0.13 μs 0.13 μs 0.00 μs ⚪ Unchanged
expand_one_string 1.62 μs 1.64 μs 0.02 μs ⚪ Unchanged
for_loop 28.43 μs 29.35 μs 0.93 μs 🟠 +3.26%
full_peg_complex 55.64 μs 55.62 μs -0.02 μs ⚪ Unchanged
full_peg_for_loop 6.31 μs 6.48 μs 0.17 μs 🟠 +2.69%
full_peg_nested_expansions 16.22 μs 16.33 μs 0.11 μs ⚪ Unchanged
full_peg_pipeline 4.21 μs 4.21 μs 0.01 μs ⚪ Unchanged
full_peg_simple 1.74 μs 1.74 μs -0.00 μs ⚪ Unchanged
function_call 3.54 μs 4.19 μs 0.66 μs 🟠 +18.59%
instantiate_shell 53.40 μs 53.26 μs -0.14 μs ⚪ Unchanged
instantiate_shell_with_init_scripts 25731.71 μs 25169.81 μs -561.89 μs ⚪ Unchanged
parse_peg_bash_completion 2262.30 μs 2271.34 μs 9.04 μs ⚪ Unchanged
parse_peg_complex 19.48 μs 19.29 μs -0.19 μs 🟢 -0.98%
parse_peg_for_loop 1.94 μs 1.93 μs -0.01 μs ⚪ Unchanged
parse_peg_pipeline 2.03 μs 2.04 μs 0.00 μs ⚪ Unchanged
parse_peg_simple 1.06 μs 1.07 μs 0.01 μs ⚪ Unchanged
run_echo_builtin_command 13.63 μs 13.78 μs 0.15 μs ⚪ Unchanged
tokenize_sample_script 3.54 μs 3.53 μs -0.01 μs ⚪ Unchanged

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
brush-core/src/interp.rs 🟢 93% 🟢 91.03% 🔴 -1.97%
brush-core/src/jobs.rs 🟠 50.91% 🟠 56.36% 🟢 5.45%
brush-core/src/processes.rs 🟠 63.41% 🟢 85.37% 🟢 21.96%
brush-parser/src/ast.rs 🔴 46.26% 🔴 48.67% 🟢 2.41%
brush-test-harness/src/execution.rs 🟢 78.71% 🟢 80.2% 🟢 1.49%
Overall Coverage 🟢 75.38% 🟢 75.64% 🟢 0.26%

Minimum allowed coverage is 70%, this run produced 75.64%

Test Summary: bash-completion test suite

Outcome Count Percentage
✅ Pass 1582 75.01
❗️ Error 19 0.90
❌ Fail 154 7.30
⏩ Skip 339 16.07
❎ Expected Fail 13 0.62
✔️ Unexpected Pass 2 0.09
📊 Total 2109 100.00

@cataggar cataggar closed this May 6, 2026
@cataggar cataggar reopened this May 6, 2026
Self-review follow-ups for the launch-before-exit fix.

Replace the hand-curated metacharacter scan in is_plain_command_name
with parser-backed checks that delegate to the single source of truth:

  * brush_parser::word::parse for quoting / parameter / command /
    arithmetic / tilde expansion and escape sequences;
  * brush_parser::word::parse_brace_expansions, inspecting for an
    actual BraceExpressionOrText::Expr piece (the parser wraps any
    non-empty input in Some, so a naive Some(_) check would reject
    every literal name);
  * brush_parser::pattern::pattern_has_glob_metacharacters for
    pathname expansion, honoring the live extglob option.

Also reject names containing any character that the current IFS would
split on, so a custom IFS like '/' can't make the fast path execute
'/bin/echo' verbatim while the regular path would have split it into
multiple fields.

Add rustdoc to all of the new helpers introduced in reubeno#1139
(start_background_ao_list, background_job_params,
is_simple_external_background_candidate, is_plain_command_name, the
*_has_deferred_expansion family) so the rationale for each step of the
fast-path eligibility check is documented.

Add unit tests covering literal names, quoting, expansions, brace
expressions vs single-element {…}, glob metacharacters,
extglob-sensitivity, and the IFS-aware splitting check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants