ConPTY's ClosePseudoConsole can deadlock when it tries to flush output
to a pipe that nobody is reading (microsoft/terminal#1810). This caused
the PTY Promise to never resolve on Windows, making bash commands with
pty:true hang indefinitely.
Root cause: portable-pty's drop(master) calls ClosePseudoConsole
synchronously. If ConPTY's internal render thread is blocked writing to
a full/undrained output pipe, ClosePseudoConsole waits forever.
Fix (three parts):
1. Rust (pty.rs): Reordered teardown to follow Microsoft's recommended
shutdown sequence:
- Drop writer first (close ConPTY input pipe)
- Drain reader thread with 500ms timeout (consume output pipe)
- Drop master in a background thread with recv_timeout(2s):
* Clean case: ClosePseudoConsole completes, thread reclaimed
* Hung case: timeout expires, main thread returns anyway
- Replace child.wait() with try_wait() polling on Windows
(WaitForSingleObject can also hang in ConPTY)
2. TypeScript (bash-pty-selection.ts): Remove the Windows blanket
disable that prevented PTY from ever being used on Windows.
3. Tests: Updated to verify PTY works on Windows with UI context.
Replaces the blanket PTY disable on Windows (PR #1105) with a targeted fix:
- TypeScript: PI_FORCE_PTY env var allows explicit Windows PTY opt-in.
PTY is still disabled by default on Windows to prevent hangs, but power
users who need interactive workflows can override.
- Rust: Adds ct.heartbeat() checks during PTY setup (openpty, spawn, reader
creation) so the existing timeout mechanism works even during setup.
- Rust (Windows): Wraps openpty() in a 5-second startup timeout thread.
If ConPTY hangs during pseudo-console creation, the Promise rejects with
a clear error instead of hanging forever.
Fixes#1103#1106