Repository navigation
fix(core): deactivate idle TTY write handles - #35886
Merged
Merged
Conversation
nathanwhit
marked this pull request as ready for review
July 8, 2026 20:18
bartlomieju
pushed a commit
that referenced
this pull request
Jul 15, 2026
## Summary Allow Node-compatible TTY stdio writes to drain and then stop keeping Deno alive, without changing the observable ref state of the underlying stdio handle where that handle is exposed. Adds a PTY integration regression for a large `process.stderr.write()` that verifies the write callback runs and the process exits. On platforms that expose `process.stderr._handle.hasRef()`, it also verifies the handle remains ref'ed before and inside the callback. ## Root Cause `uv_tty_init()` creates TTY handles with `UV_HANDLE_REF`, matching Node's observable `process.stderr._handle.hasRef() === true` behavior for stdio TTYs where `_handle` is exposed. Deno's `uv_compat` write path also marks TTY handles `UV_HANDLE_ACTIVE` via `ensure_tty_registered()`. Plain TTY writes drained their callbacks, but the normal write-drain path did not clear `UV_HANDLE_ACTIVE`; only the shutdown path deactivated the handle. Since loop liveness checks count handles that are both active and ref'ed, an idle stdio TTY write handle could keep Deno alive forever after all JS and children had finished. The fix deactivates TTY handles once there is no pending read, write, or shutdown work. It leaves `UV_HANDLE_REF` untouched, so `_handle.hasRef()` remains Node-compatible while idle inactive handles no longer keep the loop alive. Redirected output avoids the TTY stream path, which is why the same script exited with stdout/stderr redirected. ## Validation - `./tools/format.js ext/node/polyfills/process.ts libs/core/uv_compat/tty.rs tests/integration/run_tests.rs tests/testdata/run/process_stderr_tty_write_exit.js` - `cargo test --test integration process_stderr_tty_write_exit` - `cargo test -p deno_core uv_compat` - `./tools/lint.js` - Manual PTY reduction: large `process.stderr.write()` exits and reports `_handle.hasRef()` as `true` before and inside the callback on macOS - Manual PTY validation in `../v8x`: `target/debug/deno run -A tests/harness/run.mjs rusty_v8 quickjs --rescue` exits cleanly with `pass=267 fail=0 skip=0` - CI: `test integration (1/2) debug windows-x86_64` passed on head `3a9f366bf34b16ad3b0c12117233460c5320e2a0`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Allow Node-compatible TTY stdio writes to drain and then stop keeping Deno alive, without changing the observable ref state of the underlying stdio handle where that handle is exposed.
Adds a PTY integration regression for a large
process.stderr.write()that verifies the write callback runs and the process exits. On platforms that exposeprocess.stderr._handle.hasRef(), it also verifies the handle remains ref'ed before and inside the callback.Root Cause
uv_tty_init()creates TTY handles withUV_HANDLE_REF, matching Node's observableprocess.stderr._handle.hasRef() === truebehavior for stdio TTYs where_handleis exposed.Deno's
uv_compatwrite path also marks TTY handlesUV_HANDLE_ACTIVEviaensure_tty_registered(). Plain TTY writes drained their callbacks, but the normal write-drain path did not clearUV_HANDLE_ACTIVE; only the shutdown path deactivated the handle. Since loop liveness checks count handles that are both active and ref'ed, an idle stdio TTY write handle could keep Deno alive forever after all JS and children had finished.The fix deactivates TTY handles once there is no pending read, write, or shutdown work. It leaves
UV_HANDLE_REFuntouched, so_handle.hasRef()remains Node-compatible while idle inactive handles no longer keep the loop alive.Redirected output avoids the TTY stream path, which is why the same script exited with stdout/stderr redirected.
Validation
./tools/format.js ext/node/polyfills/process.ts libs/core/uv_compat/tty.rs tests/integration/run_tests.rs tests/testdata/run/process_stderr_tty_write_exit.jscargo test --test integration process_stderr_tty_write_exitcargo test -p deno_core uv_compat./tools/lint.jsprocess.stderr.write()exits and reports_handle.hasRef()astruebefore and inside the callback on macOS../v8x:target/debug/deno run -A tests/harness/run.mjs rusty_v8 quickjs --rescueexits cleanly withpass=267 fail=0 skip=0test integration (1/2) debug windows-x86_64passed on head3a9f366bf34b16ad3b0c12117233460c5320e2a0