Skip to content

fix(input): pipe-based stdin cancellation for MSYS/mintty (v0.2.1)#12

Merged
kolkov merged 9 commits into
mainfrom
hotfix/v0.2.1
Feb 6, 2026
Merged

fix(input): pipe-based stdin cancellation for MSYS/mintty (v0.2.1)#12
kolkov merged 9 commits into
mainfrom
hotfix/v0.2.1

Conversation

@kolkov

@kolkov kolkov commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Hotfix release v0.2.1 resolving critical stdin race condition on MSYS2/mintty (Git Bash on Windows).

  • Pipe-based CancelableReader: Replace direct stdin reading with os.Pipe relay for instant cancellation on ALL platforms, including MSYS/mintty where WriteConsoleInputW is a no-op
  • Double-close protection: sync.Once guard on pipe writer prevents fd reuse bugs when Cancel() and relay defer race
  • Flaky test fix: Stabilized InputReaderRestartIdempotent test
  • Documentation: Updated CHANGELOG, README, ROADMAP, TTY_CONTROL_GUIDE for v0.2.1

Architecture

stdin -> [relayLoop] -> os.Pipe -> [readLoopPipe] -> readCh -> Read()

Cancellation is instant: Cancel() closes pipe writer -> EOF on read end. SetReadDeadline + WriteConsoleInputW as relay unblock fallbacks.

Files changed (12 files, +686 / -67)

Code (tea module):

  • cancelable_reader.go — Pipe relay + closePipeWriter() via sync.Once
  • cancelable_reader_test.go — 12 new tests (pipe relay + double-close)
  • reader.goWaitForShutdown() exposed through Reader
  • program.go — STEP 4 (WaitForShutdown), timeout 200ms
  • stdin_unblock_other.go / stdin_unblock_windows.go — Updated docs
  • go.modx/sys, x/term promoted to direct deps

Docs:

  • CHANGELOG.md — v0.2.1 hotfix entry
  • README.md — Status v0.2.1, "What's New" section
  • ROADMAP.md — Version, dates, key dates table
  • TTY_CONTROL_GUIDE.md — Version footer

Test plan

  • All tea tests pass (go test ./tea/... -count=1)
  • All input tests pass with 12 new tests (32 total)
  • go vet ./tea/... clean
  • go build ./... clean
  • CI green on Linux, macOS, Windows

Problem: MockTerminal returns EOF immediately, causing inputReader goroutine
to exit before the test can verify the running state. This created a race
condition especially visible on Windows.

Solution: Introduced blockingMockReader that blocks in Read() until Close()
is called. This ensures the inputReader goroutine stays alive for the duration
of the test, allowing proper verification of the idempotent restart behavior.

The test now reliably passes on all platforms (Linux, macOS, Windows) without
needing to skip it.
Replace direct stdin reading with os.Pipe relay to guarantee instant
cancellation on all platforms including MSYS/mintty where
UnblockStdinRead (WriteConsoleInputW) is a no-op.

- Add relayLoop() copying stdin to pipe, readLoopPipe() reading from pipe
- Cancel() closes pipe writer for immediate EOF on read end
- SetReadDeadline + UnblockStdinRead as relay unblock fallbacks
- WaitForShutdown() exposed through Reader for clean shutdown
- stopInputReader() calls WaitForShutdown(), timeout 100ms to 200ms
- Fallback to legacy direct-read if os.Pipe() fails
- Add sync.Once guard (closePipeWriter) preventing pipe fd double-close
  between Cancel() and relayLoop defer — eliminates fd reuse risk
- Add 6 double-close protection tests: CancelThenRelayExit,
  RelayExitsBeforeCancel, ConcurrentCancelAndRelayExit (50 iterations),
  RapidCreateCancelCycles (100 iterations), CancelWithoutRead,
  MultipleWaitForShutdown
- Update CHANGELOG.md, README.md, ROADMAP.md, TTY_CONTROL_GUIDE.md
  for v0.2.1 hotfix release
- golang.org/x/sys v0.37.0 -> v0.40.0
- golang.org/x/term v0.36.0 -> v0.39.0
- github.com/unilibs/uniwidth v0.1.0-beta -> v0.2.0
- github.com/spf13/cobra v1.8.0 -> v1.10.2
- github.com/spf13/pflag v1.0.5 -> v1.0.10
- Updated all 19 go.mod files across the monorepo
- style/examples/basic,complete: fix import path (style/api -> style)
- drag-scroll,hover-highlight,wheel-scroll: add missing testing replace
- cobra-cli: add replace directives for all Phoenix modules
- Remove GraphemeClusters() (internal-only, render uses uniseg directly)
- Simplify StringWidth/ClusterWidth to delegate to uniwidth.StringWidth
- Fix StringWidthWithConfig/ClusterWidthWithConfig: use grapheme-aware
  base width + per-rune EA Ambiguous delta (StringWidthWithOptions lacks
  grapheme awareness for emoji modifiers/ZWJ sequences)
- Remove related tests and benchmarks for removed GraphemeClusters
- core/go.mod now depends only on uniwidth v0.2.0
@kolkov kolkov merged commit 2c2db6e into main Feb 6, 2026
6 checks passed
@kolkov kolkov deleted the hotfix/v0.2.1 branch March 5, 2026 17:50
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