Skip to content

fix: on Windows, a reaped child closes the terminal - #277

Merged
vyncint merged 1 commit into
mainfrom
windows-exit-eof
Sep 8, 2026
Merged

fix: on Windows, a reaped child closes the terminal#277
vyncint merged 1 commit into
mainfrom
windows-exit-eof

Conversation

@vyncint

@vyncint vyncint commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Refs #149 — the first item of step 4, found by running the migrated suite (#276) on windows-latest.

The bug

Every liveness decision reads EOF off the master: ensure_deliverable (the "child is gone" refusals in send, resize, and the mouse path) and the fast Error::Eof every wait returns instead of burning its timeout. On Unix that is the right signal, for the reason the code already states — EOF means every slave descriptor is closed, while a reaped child says nothing about a grandchild still reading. On Windows it never arrives: a pseudoconsole's output pipe stays open until the console itself is closed, whoever has exited. Five tests fail there identically, with the child gone and the harness insisting someone is listening:

a_departed_child_is_refused_identically_on_every_platform · typed_input_to_a_departed_child_is_a_typed_error · a_mouse_click_at_a_departed_child_blames_the_child · resize_after_the_child_exits_is_refused_like_send · waits_fail_fast_on_eof_instead_of_burning_the_timeout

The fix

Where EOF cannot come, the reaped child is the signal — one named constant, EXIT_CLOSES_THE_TERMINAL = cfg!(windows), with the reasoning on it.

  • ExitWatch::tick runs inside each wait loop's poll (wait_until, wait_frame, wait_idle, wait_stable): a non-blocking try_wait; once the child is reaped and DRAIN_GRACE has passed with the terminal still open, state.eof = true and the wait returns exactly what it would have on Unix.
  • wait_exit already drained for DRAIN_GRACE waiting on EOF; when that expires on Windows it now marks the terminal closed, so the refusals that follow a wait_exit fire.
  • A no-op where EOF is real: Unix behaviour is byte-for-byte what it was, and the whole suite here says so.

Verification

Unix: full suite, three feature configurations, both clippy targets. Windows: windows.yml dispatched against this branch — the five tests above are the acceptance; result in a comment below.

@vyncint vyncint added this to the v0.10 milestone Sep 8, 2026
Every liveness decision in the harness reads EOF off the master: the
"child is gone" refusals in send, resize and the mouse path, and the
fast Error::Eof every wait returns instead of burning its timeout. On
Unix that is the right signal — every slave descriptor closed is exactly
"no one can read this", and a reaped child says nothing about a
grandchild still holding the terminal. On Windows it never arrives: a
pseudoconsole's output pipe stays open until the console itself is
closed, whoever has exited. Five tests failed there the same way, with
the child gone and the harness insisting someone was listening (#149).

So where EOF cannot come, the reaped child is the signal. Each wait
loop now ticks an ExitWatch alongside its poll: once try_wait reports
the child gone and DRAIN_GRACE has passed with the terminal still open,
the state is marked at EOF and the wait returns what it would have on
Unix. wait_exit does the same after its drain grace expires. Both are
gated on cfg!(windows) through one named constant, so Unix behaviour is
byte-for-byte what it was; the whole suite there says so.

Refs #149

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@vyncint

vyncint commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

windows.yml against this branch — run 34183062320

All five acceptance tests pass on windows-latest:

ok  a_departed_child_is_refused_identically_on_every_platform
ok  typed_input_to_a_departed_child_is_a_typed_error
ok  a_mouse_click_at_a_departed_child_blames_the_child
ok  resize_after_the_child_exits_is_refused_like_send
ok  waits_fail_fast_on_eof_instead_of_burning_the_timeout

Suite there: 351 passed / 86 failed (345 / 92 on main). The first version of this branch got four of the five: waits_fail_fast_on_eof… still burned its timeout, because wait_until skips a wake whose generation is unchanged before it ever looks at eof — and a synthesized EOF changes no byte. The watch now bumps the generation when it marks the stream closed, which is the honest description anyway: EOF is a fact about the stream even when no byte carried it.

The macOS leg's one failure on the first push was inspect_prints_its_usage_for_help_and_for_a_missing_program with NotFound on the example binary — inspect.rs rebuilds the example from every test with no serialization, unlike fixture_bin's BUILT mutex, so two tests can race a relink. Unrelated to this change (the file is untouched), green on the re-run, filed separately.

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.

1 participant