Skip to content

Fixes #5349. Ignore non-readable poll events during ANSI shutdown#5353

Merged
harder merged 1 commit into
gui-cs:developfrom
harder:fix-5349-ansi-dispose-hang
May 21, 2026
Merged

Fixes #5349. Ignore non-readable poll events during ANSI shutdown#5353
harder merged 1 commit into
gui-cs:developfrom
harder:fix-5349-ansi-dispose-hang

Conversation

@harder
Copy link
Copy Markdown
Collaborator

@harder harder commented May 20, 2026

Fixes

Proposed Changes

  • Change UnixIOHelper.IsInputAvailable () to return true only when poll() reports POLLIN
  • Add UnixIOHelperTests to cover both a readable poll event and a non-readable poll event on Unix-like platforms
  • Keep the fix at the shared helper so all ANSI input paths stop treating POLLHUP/POLLERR/POLLNVAL-only wakeups as readable input

Root Cause

AnsiInput.FlushInput () uses UnixIOHelper.IsInputAvailable () to decide whether to keep draining input during shutdown.

On Unix/macOS, poll() can wake up with non-readable events such as POLLHUP or POLLNVAL without POLLIN. The old helper treated any poll() > 0 result as readable input, so shutdown could attempt another read even though no readable data was available. If that happened while MainLoopCoordinator.Stop () was waiting for the input task to exit, app.Dispose () could hang.

OSS Comparison

This fix matches the defensive pattern used by other mature terminal codebases:

  • prompt_toolkit fixed the same class of closed/unpollable-stdin bugs by treating EOF/unpollable stdin as terminal closure and unregistering the reader
  • kitty currently has an active bug in the same family where non-readable poll events are effectively treated as readable
  • lnav, vim, zsh ZLE, and arcan all gate reads on POLLIN and handle hangup/error events separately

That makes this change consistent with established terminal I/O practice: only read when POLLIN is set.

Verification

  • dotnet build --no-restore
  • dotnet test --project Tests/UnitTestsParallelizable --no-build --filter-class "*UnixIOHelperTests"
  • dotnet test --project Tests/UnitTestsParallelizable --no-build --filter-class "*AllDriverTests"
  • dotnet test --project Tests/UnitTestsParallelizable --no-build --filter-class "*RunTests"

No new warnings were introduced in the touched files. The build still reports an existing unrelated IntegrationTests analyzer warning (AD0001).

To pull down this PR locally:

git remote add copilot https://github.com/harder/Terminal.Gui.git
git fetch copilot fix-5349-ansi-dispose-hang
git checkout copilot/fix-5349-ansi-dispose-hang

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@harder harder requested a review from tig as a code owner May 20, 2026 18:47
Copy link
Copy Markdown
Member

@tig tig left a comment

Choose a reason for hiding this comment

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

The fix is clean and well-targeted. Here's my review:

Summary

Good PR. The fix correctly addresses a hang during ANSI shutdown by checking revents for POLLIN instead of treating any poll wake-up (e.g., POLLHUP, POLLERR, POLLNVAL) as readable data. Previously, poll() returning > 0 was enough to signal "input available," which could cause a read attempt on a closed/errored fd — leading to a hang.

Findings

The logic change is correct and the tests exercise both the happy path and the error-event path using real pipes + P/Invoke. Two minor observations:

  1. Test: double-close risk — In IsInputAvailable_ReturnsFalse_WhenPollReportsNonReadableEvent, pipeFds[0] is closed inside the try block (the test's setup step), but the finally block doesn't guard against closing it again. Currently fine because finally only closes pipeFds[1], but if someone refactors this, it's fragile. Minor — not blocking.

  2. Test: platform skip pattern — The early-return skip pattern works but isn't self-documenting in test output (xUnit won't distinguish "skipped" from "passed"). A Skip attribute or Assert.Skip() would make CI results clearer. Minor — not blocking.

No bugs, no security issues, no logic errors. The change is minimal, correct, and well-tested.

@harder harder merged commit e956484 into gui-cs:develop May 21, 2026
13 checks passed
@harder harder deleted the fix-5349-ansi-dispose-hang branch May 21, 2026 01:43
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.

ANSI app can hang in IApplication.Dispose() after RunAsync returns

2 participants