WSL Terminals Under the Hood: ConPTY, Linux PTYs, and Interactive Semantics
Trace interactive WSL input and output across Windows console hosting and Linux PTYs, then test TTY, resize, signal, and pipe behavior.
An interactive Linux shell in Windows crosses several interfaces that are easy to conflate. A terminal emulator paints characters and sends key input. Windows console plumbing may expose a pseudoconsole stream to a client. WSL starts a Linux process, which can itself be connected to a Linux pseudoterminal. The shell and its children then make decisions based on whether their file descriptors are terminals. A command that behaves differently in a Windows Terminal tab, a PowerShell pipeline, and an SSH session may be reacting to different terminal semantics rather than a WSL kernel defect.
The reliable method is to identify which layer owns the behavior. Do not infer that every invocation of wsl.exe uses an identical console path: launchers, terminal hosts, Windows builds, and redirected handles differ. Instead, inspect the Linux process’s file descriptors and terminal state, then compare an interactive terminal with an intentionally redirected invocation.
Separate the visible terminal from the Linux PTY
Windows Terminal is a terminal emulator. On Windows, a console application can use a pseudoconsole session, exposed through the ConPTY APIs, to exchange input and output through pipes while the terminal host renders the screen. Microsoft’s ConPTY sample creates input and output pipes, attaches them to a pseudoconsole, starts a child process, and reads the resulting virtual-terminal stream.
Inside Linux, a pseudoterminal has a master and a slave side. A terminal emulator or intermediary drives the master; the shell and interactive programs use the slave as a terminal device. The kernel’s line discipline, terminal attributes, and foreground process group explain many behaviors users attribute to a GUI terminal. ConPTY and a Linux PTY are not the same API or device. They are distinct layers that can participate in a WSL terminal experience.
This distinction matters when writing wrappers. A Windows program that captures output into a pipe may launch a Linux process without a terminal even if the same command appears interactive in a Terminal tab. Conversely, an SSH client can allocate a remote PTY inside a local terminal, creating another terminal boundary. Document the actual launcher and handle redirection in bug reports.
Ask the process whether it has a terminal
Start with facts from the Linux side:
printf 'stdin TTY: '; test -t 0 && echo yes || echo no
printf 'stdout TTY: '; test -t 1 && echo yes || echo no
printf 'stderr TTY: '; test -t 2 && echo yes || echo no
tty
stty -a
ps -o pid,ppid,pgid,sid,tty,stat,cmd -p "$$"
A successful tty command and a nonempty terminal field in ps show that the shell is attached to a terminal device. The file descriptor tests are more precise than asking whether the parent executable is wsl.exe. A process can have terminal input but redirected output, or the reverse. A shell’s own TTY state also does not prove that every child inherited all three descriptors unchanged.
Now compare with an explicitly piped launch:
wsl.exe --distribution Ubuntu --exec sh -c 'test -t 0; echo "stdin test exit=$?"'
wsl.exe --distribution Ubuntu --exec sh -c 'printf "plain output
"' | Out-String
The first test deliberately asks the child to inspect stdin; it is not a recommendation to force a terminal. A Windows PowerShell pipeline can impose its own text conversion and capture rules after wsl.exe writes bytes. For byte-sensitive protocols, save output to a file and inspect it in both environments instead of relying on what a terminal renders.
Understand input, line discipline, and Ctrl-C
A terminal application may send control characters rather than Windows keyboard events directly to a Linux process. In canonical mode, the Linux terminal line discipline interprets configured control characters. The conventional interrupt character, often Ctrl-C, causes a signal to the terminal’s foreground process group when the terminal settings enable that behavior. A program can change those settings, disable canonical input, or handle signals itself.
This is why Ctrl-C can stop a foreground command in an ordinary shell but be consumed by a full-screen editor, debugger, or terminal multiplexer. Inspect stty output for intr, isig, icanon, and echo rather than assuming every key maps to the same signal. A raw-mode application commonly changes terminal attributes temporarily and is responsible for restoring them on normal exit; a crash can leave the terminal looking broken until reset or a new terminal is opened.
Job control is also a Linux-side property. Shells place pipelines into process groups and designate a foreground group for the terminal. Ctrl-Z, fg, and bg depend on that relationship. A Windows terminal close is not equivalent to typing Ctrl-C, and forcefully stopping a Windows host process is not equivalent to asking the Linux foreground group to exit gracefully.
Resize is a protocol event, not just a window repaint
When a terminal window changes dimensions, the terminal host and console layer can report a new row and column size. Linux applications that use a terminal can then query the dimensions or react to SIGWINCH. Editors, pagers, and REPLs often redraw after that notification. If text wraps incorrectly only after a resize, test whether the shell’s terminal reports the new size:
stty size
tput cols
tput lines
Run these before and after resizing. Different answers suggest a problem in the terminal-host path or a stale terminal setting; matching answers with a broken application point toward that application’s redraw logic. Avoid hard-coding a screen size in a dotfile as a first fix because it can hide a resize integration failure and make remote sessions worse.
Distinguish terminal capability from encoding
A terminal can transport UTF-8 bytes and virtual-terminal escape sequences while a Linux program still believes it is attached to a dumb terminal. The TERM environment variable describes a terminal capability profile; it does not identify Windows Terminal itself and should not be set to a value unsupported by the active terminal. Check TERM, locale, and the program’s own output separately.
Likewise, output redirected to a file is not a screen recording. Some programs change color, buffering, prompts, progress bars, and password handling when stdout or stdin is not a TTY. Preserve pipes and exit codes intentionally in automation. If a command needs an interactive terminal for a particular function, make that requirement explicit rather than inserting a pseudo-terminal tool into every pipeline.
Process-tree teardown and timeout behavior
When a Windows terminal tab closes, the hosting application can close its pseudoconsole session and the attached console process tree can be terminated. That is not the same event as a user sending Ctrl-C to a foreground Linux process. A long-running write, editor buffer, or build can therefore lose work if the operator uses tab closure as a cancellation strategy. Applications should save state and implement their own graceful stop path where data integrity matters.
When a scripted launcher needs a timeout, record whether it sends an interrupt-like input, terminates the Windows process, or shuts down a WSL distro. These actions have different scopes. A process killed at the host side may leave application-level cleanup incomplete, while a WSL shutdown stops every running WSL 2 distro. Test the exact timeout path with disposable data and observe the child exit status before using it to protect production state.
A bounded acceptance test for terminal integrations
For each supported entry point, record the command used to launch the distro, the terminal host, Windows build, WSL version, and whether stdin, stdout, or stderr is redirected. Run the descriptor test, a resize test, and a short foreground-process test. Confirm that Ctrl-C reaches an interruptible foreground process, Ctrl-Z and fg behave in an interactive shell, and noninteractive scripts finish without prompts or color-dependent parsing.
For automation, test input with a known byte sequence and compare output bytes, not just visible glyphs. For user interfaces, verify resize and focus behavior in the actual terminal host. Repeat after opening a fresh tab and after a WSL shutdown only when the symptom involves initialization. This narrow test matrix separates terminal integration from shell startup, program logic, and host-side text capture.
Diagnose by layer before changing configuration
If the process has no TTY, inspect the Windows caller’s redirection and terminal allocation. If a TTY exists but Ctrl-C or job control is wrong, inspect stty state and foreground process groups. If dimensions are stale, compare stty size before and after a resize. If only glyphs or colors differ, investigate encoding, locale, and TERM independently. Capture raw output when possible so rendering does not become the evidence.
Do not assume that installing another terminal, changing the distro, or adding an undocumented WSL option will repair an application that was launched with a pipe. ConPTY is a documented Windows interface for console applications; Linux PTYs are documented kernel interfaces; neither guarantees that an arbitrary parent program will allocate a terminal. Fix the boundary that the evidence identifies.
Related:
- Calling WSL from Windows Scripts: Commands, Working Directories, Streams, and Exit Codes
- How WSL Lets Linux and Windows Executables Call Each Other
Sources:
- Microsoft: Creating a pseudoconsole session
- Microsoft: Console virtual terminal sequences
- Microsoft: Basic commands for WSL
- Linux man-pages: pseudoterminal interfaces (
pty(7)) - Linux man-pages: terminal window-size ioctls (
ioctl_tty(2)) - Linux man-pages: termios
- Linux man-pages: tty
- Linux man-pages: terminal capability database (
terminfo(5))