Repository navigation
scripts: stop executor close waiting on inherited stdout - #532
Jay (jay23606) wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
The focused fix addresses the deadlock scenario and includes appropriate regression coverage.
0 open findings
What changed in this PR
Prevents executor cleanup from blocking on stdout inherited by child processes.
Changes:
- Moves stdout closure to the reader thread after reading finishes.
- Adds regression coverage ensuring
close()remains bounded.
| File | Description |
|---|---|
scripts/nvx_tools/adversarial.py |
Avoids racing stdout closure with a blocked reader. |
scripts/test_adversarial.py |
Tests inherited-stdout cleanup behavior. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open findings
🧠 Review effort: Lite
|
Jay (@jay23606) please rebase. no merge commits. |
The reader kept queuing lines from stdout after close() had returned, so a child that outlived the join could grow the queue without bound. Once close() has started, the reader keeps draining the pipe but retains nothing. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The grandchild must outlive the five-second bound, or a blocking close() would still pass. Also bound close() time in the noisy-child regression test. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ys open A descendant that keeps the stdout pipe open kept the reader thread and its descriptor alive after close(). Start every executor in its own session on POSIX, and kill its group when the reader is still blocked after the join. On Windows, taskkill /T covers the tree while the root is still running. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
| if self._process.poll() is None: | ||
| with contextlib.suppress(OSError, subprocess.TimeoutExpired): | ||
| subprocess.run( | ||
| ["taskkill.exe", "/PID", str(self._process.pid), "/T", "/F"], |
|
Jay (@jay23606) please remove all merge commits and resolve all copilot comments. |


Bug
ExecutorClient.close()closed the executor's stdout stream from the calling thread while the reader thread was still blocked inreadline()on that same stream.BufferedReader.close()waits for the blocked read, so when an external wrapper exits but a child it started keeps the inherited stdout pipe open,close()waits until that child exits. The two-second reader join never ran, because the stream close blocked first. A timed-out executor therefore delayed failure classification and summary emission by however long the child kept the pipe open.Change
scripts/nvx_tools/adversarial.py:close()no longer closes stdout. The reader thread closes it in afinallyafter its last read, so the close never races a blocked read. stdin and stderr are closed as before, and the reader join stays bounded at two seconds.scripts/test_adversarial.py: addstest_external_executor_close_ignores_inherited_stdout. The wrapper exits at once and leaves a 10-second grandchild holding stdout. The test assertsclose()returns in under 5 seconds.Validation
Before the fix, the new test fails with
close() blocked 10.04s waiting on inherited stdout. After the fix it passes.cd scripts && python -m unittest test_adversarial.CopilotContainmentTests.test_external_executor_close_ignores_inherited_stdout: OKcd scripts && python -m unittest test_adversarial: OK (skipped=6)ruff format --check scripts/nvx_tools/adversarial.py scripts/test_adversarial.py: 2 files already formattedruff check scripts/nvx_tools/adversarial.py scripts/test_adversarial.py: All checks passed!Not run here: Pyright strict over
scripts/, and the Windows CI jobs. The test uses# type: ignore[arg-type]on thePopencall, which Pyright may flag.Regression test: fails on base twice (/close() blocked \d+.\d+s waiting on inherited stdout/), passes on fix twice (
cd scripts && python -m unittest test_adversarial.CopilotContainmentTests.test_external_executor_close_ignores_inherited_stdout)Fixes #503