Skip to content

fix(rewrite): process wrapper prefixes - #3937

Merged
aeppling merged 5 commits into
developfrom
feat/process-wrapper-prefixes
Sep 10, 2026
Merged

aeppling merged 5 commits into
developfrom
feat/process-wrapper-prefixes

Conversation

@aeppling

@aeppling aeppling commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2375. Supersedes #2568, whose wrapper set and peel-recurse-reprepend shape this builds on (@YOMXXX).

What changes

timeout 300 cargo test now rewrites to timeout 300 rtk cargo test. Same for time, nice, nohup, and path-qualified forms like /usr/bin/timeout.

PROCESS_WRAPPERS in registry.rs describes each wrapper's own arguments (value options, flag options, attached values like -k5s and -n10, positionals such as timeout's duration). One generic scanner walks them in order. Adding a wrapper is a table entry.

Two rules keep it safe:

  • An unknown option drops the rewrite, since it may consume the next word and shift which token looks like the command.
  • Shell syntax before the command (redirect, subshell, glob, command substitution) drops the rewrite, since the argv can no longer be read from the token list.

stdbuf is excluded on purpose. It exists to get incremental output, and rtk buffers until the child exits, so rewriting it removes the reason to type it.

Signal handling

Wrapping changes who gets signalled: timeout 300 rtk cargo test signals rtk, not cargo. Before this PR rtk died on the default disposition and every captured line was lost, which is worse than running unwrapped.

core::stream now relays SIGINT/SIGTERM to the child and lets the normal filter-and-print path finish. If the child does not exit, it escalates to SIGKILL after 1.5s, then restores the default disposition and re-raises after another 1.5s, so rtk can never be held open.

How this was tested

Shim-based execution tests, with a fake cargo on PATH that records its argv:

  • argv and exit codes are identical between the raw and rewritten forms for all four wrappers, for success, exit 101 and exit 7.
  • output under a kill, shim prints 200 lines then hangs, killed at 2s:
exit lines seen
timeout 2 cargo clippy 124 200
timeout 2 rtk cargo clippy before 124 0
timeout 2 rtk cargo clippy after 124 9 filtered, diagnostic kept
  • timeout --foreground no longer orphans the child.
  • stubborn child that defers SIGTERM: bounded at 3s instead of hanging 300s, output still flushed.

Differential fuzz against develop, 6,567 generated command lines through both binaries:

bucket count
identical 4,555
newly filtered 1,534
changed (compound, wrapped half now rewrites) 478
lost rewrite 0
crash or hang 0
shape violation 0

Shape check asserts a rewrite only ever inserts rtk tokens, never reorders or drops shell text. Rewriting is idempotent, so the hook can re-run over its own output.

cargo fmt --all, cargo clippy --all-targets, cargo test --all: clean, 3341 unit tests plus all integration targets. tests/signal_flush_test.rs fails without the relay.

Needs a decision

test_toml_filter_rewrites_bare_command_but_not_wrapped_invocations asserted that a wrapped TOML-filtered command is never rewritten. That pinned the absence of wrapper peeling, which is what this PR adds, so it now asserts the wrapper-preserving rewrite and is renamed. What keeps a filter off the wrapper itself is the is_fully_anchored gate on match_command, unchanged and still tested in core::toml_filter.

aeppling and others added 3 commits September 8, 2026 13:41
Table-driven timeout/time/nice/nohup; unknown options and shell syntax before the command fall through to passthrough.

Co-authored-by: YOMXXX <liguanchen@xiaomi.com>
Relay SIGINT/SIGTERM to the child, escalating to SIGKILL then re-raise so a stubborn child cannot hold rtk open.
@aeppling
aeppling force-pushed the feat/process-wrapper-prefixes branch from a8c63c3 to ea67c7e Compare September 8, 2026 11:42

@pszymkowiak pszymkowiak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice work on this one. The wrapper peeling is fail-closed where it counts: an option the table doesn't describe drops the rewrite rather than guessing which token is the command, and the option-form coverage (-k 5s, -k5s, --kill-after=5s, --, nice -10/+5) is the part that usually breaks on the first real user. Leaving stdbuf out is the right call and the README says why.

The signal relay is the half I looked at hardest, and it holds up. The handler only touches kill, signal, raise and atomics, so it stays async-signal-safe, and the drop order means the handlers come off before the child is reaped. signal_flush_test.rs tests the behaviour rather than the wiring, which is what makes it worth having.

Approving. Three things I'd like noted rather than fixed here, unless you disagree:

  1. The relay installs on every run_streaming, not just wrapped invocations, so Ctrl-C on any rtk command now behaves differently (child gets SIGINT twice, rtk survives to print). That's probably what we want, but it deserves a line in the CHANGELOG since the PR title doesn't hint at it.

  2. CHILD_PID / RELAYED / FINISHED are process-wide, so this quietly assumes one streaming run at a time. True today, but nothing in the code says it has to stay true. A short comment would save someone a bad afternoon later.

  3. test_toml_filter_rewrites_bare_and_wrapped_invocations widens the blast radius to user TOML filters, with is_fully_anchored as the only thing keeping a filter off the wrapper itself. You documented it in the test comment, which is the right instinct - I'm just flagging it so it's visible in the review history.

Re-raise SIGINT/SIGTERM once output is printed so callers still see signal death, not a plain exit code.
@aeppling
aeppling merged commit 52ed9fe into develop Sep 10, 2026
17 checks passed
@rtk-release-bot rtk-release-bot Bot mentioned this pull request Sep 10, 2026
@KuSh
KuSh deleted the feat/process-wrapper-prefixes branch September 10, 2026 18:26
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.

Hook rewrite ignores process-wrapper prefixes (timeout, time) so wrapped commands run raw/unfiltered

2 participants