Skip to content

fix(incremental): treat NUL stdin on Windows as non-interactive - #1166

Merged
1 commit merged into
tirth8205:stagingfrom
xOscarCodes:fix/windows-nul-stdin-console-check
Nov 9, 2026
Merged

1 commit merged into
tirth8205:stagingfrom
xOscarCodes:fix/windows-nul-stdin-console-check

Conversation

@xOscarCodes

Copy link
Copy Markdown
Contributor

Linked issue

Closes #1163

What & why

On Windows, isatty() is True for every character device, including the NUL device. A parent gives a child NUL as stdin through subprocess.DEVNULL or Node's stdio: "ignore". In that case the Windows non-TTY fallback in _select_executor_kind() (#46, #136) did not apply, and parsing used a ProcessPoolExecutor. When the parent then ended the run, for example a pre-commit hook with a timeout, Windows did not end the pool workers. Each run could leave up to 8 orphaned spawn_main processes. On one development machine about 1,400 of them collected and held about 7.6 GB.

This PR adds _stdin_is_interactive(). On Windows it returns True only when the stdin handle accepts GetConsoleMode. A console accepts that call, and NUL, files and pipes refuse it. On other platforms the result is the same as isatty(), so the Linux and macOS executor choice does not change. CRG_PARSE_EXECUTOR still overrides everything.

How it was tested

On Windows 11, CPython 3.12 (uv):

uv run pytest tests/test_incremental.py -k ParseExecutorSelection -q
# 6 passed. Without the change in incremental.py, the 3 new tests fail.
uv run --with pytest-xdist pytest tests/test_incremental.py tests/test_main.py -n 4 -q
# 211 passed
uvx ruff check code_review_graph/ tests/test_incremental.py
# All checks passed
uvx mypy code_review_graph/incremental.py --ignore-missing-imports --no-strict-optional --follow-imports=silent
uvx mypy code_review_graph/incremental.py --ignore-missing-imports --no-strict-optional --follow-imports=silent --platform linux
# no issues in both
uvx mypy code_review_graph/ --ignore-missing-imports --no-strict-optional
# 9 errors in 3 other files on Windows, the same 9 without this change (for example SIGKILL in multi_worktree.py)

New tests:

  • test_windows_nul_stdin_uses_threads: Windows, isatty() True, no console handle, so the result is thread.
  • test_windows_console_stdin_keeps_process_default: Windows, a console handle, so the result is process.
  • test_real_nul_stdin_is_not_interactive_on_windows (Windows only): a real child with stdin=DEVNULL reports isatty() True, _stdin_is_interactive() False and executor thread.

Measured effect on a repository with about 650 files: a full build with the old selection reached 8 pool workers in 58 s. With the thread executor it reached 0 workers in 60 s.

Limits:

  • The full suite did not finish locally. Two Windows runs stopped at about 74%, one at a time limit and one when the session restarted, so I rely on CI for the rest of the suite. Those partial Windows runs showed some failures in tests that this change does not touch. I did not compare them against staging.
  • A real interactive console was not tested end to end. That case is covered by the unit test with a stand-in for the console check.

🤖 Generated with Claude Code

On Windows, isatty() is True for every character device, including the NUL
device that subprocess.DEVNULL and Node's stdio "ignore" provide. The
Windows non-TTY fallback from tirth8205#46 and tirth8205#136 therefore did not apply, and
the parse step started a ProcessPoolExecutor. When the caller then ended
the run, Windows left the pool workers behind as orphans.

Stdin now counts as interactive on Windows only when its handle accepts
GetConsoleMode, which a console accepts and NUL, files and pipes refuse.

Closes tirth8205#1163

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tirth8205 tirth8205 closed this pull request by merging all changes into tirth8205:staging in d01c1fd Nov 9, 2026
HengYangDS pushed a commit to HengYangDS/code-review-graph that referenced this pull request Nov 9, 2026
…ld (review of tirth8205#1166)

The Windows console check could raise ImportError (a Python without
ctypes) or ctypes.ArgumentError (a handle too large to pass as a C int),
and both went straight through _select_executor_kind into the build. The
old isatty() check could not fail that way. Both now mean "not a console",
which picks the thread executor. Adds tests that run the real check with
stand-ins for msvcrt and kernel32 on every platform, and one for a missing
sys.stdin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HengYangDS pushed a commit to HengYangDS/code-review-graph that referenced this pull request Nov 9, 2026
)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

Windows: NUL stdin counts as a TTY, so the process pool is used and its workers are orphaned when the run is stopped

2 participants