Skip to content

Fix Console.ReadLine() returning each line one Enter late with TreatControlCAsInput on Windows - #133896

Open
caraioniurie47 wants to merge 1 commit into
dotnet:mainfrom
caraioniurie47:issue-133814
Open

caraioniurie47 wants to merge 1 commit into
dotnet:mainfrom
caraioniurie47:issue-133814

Conversation

@caraioniurie47

Copy link
Copy Markdown

Fixes #133814

With Console.TreatControlCAsInput = true, which clears ENABLE_PROCESSED_INPUT, the Windows console host ends each line with a lone \r instead of \r\n. Console.In wraps a StreamReader, and StreamReader.ReadLine() reads ahead after a \r to check for a \n. On a console that read waits for the next line, so each ReadLine() returned only after the following Enter.

Change. A new internal ConsoleStreamReader, compiled for Windows only, ends a line at \r without reading ahead, and skips a \n that pairs with that \r at the start of the next ReadLine, Peek, Read or ReadToEnd. A zero-length read leaves the skip pending, so it still returns without waiting. ConsolePal.Windows.GetOrCreateReader uses the new reader only when Console.IsInputRedirected is false, the same condition under which ConsolePal.Unix uses StdInReader; redirected input keeps the plain StreamReader.

Behaviour change. On a Windows console, a line ending in a lone \r is now returned as soon as the \r arrives, not after the next line. In the default mode the console sends \r\n, and ReadLine() returns the same lines at the same Enter as before. Whether this needs the breaking-change label is left to the area owners.

Why not in StreamReader or the console stream. Neither was built. The same change in StreamReader.ReadLine() would apply to every stream, not only a console. Translating \r to \r\n in the console stream would also change the bytes Console.OpenStandardInput() returns. The new reader reads a character at a time, because StreamReader's buffer is not accessible from System.Console.

Performance

Reading a character at a time makes ReadLine slower than StreamReader.ReadLine, which searches its buffer for the line end. BenchmarkDotNet 0.15.2, .NET 10.0.12, Windows 11 x64. The type is internal and the benchmark process's input is redirected, so this branch's ConsoleStreamReader.cs is compiled into the benchmark and compared with StreamReader. Each operation reads just over 40,000 chars of lines ending \r\n from a MemoryStream to the end (UTF-8, bufferSize: 4096 as Console.ReadBufferSize):

Operation Line length StreamReader ConsoleStreamReader Allocated (both)
ReadLine() 40 27.9 µs 186.2 µs 109.05 KB
ReadLine() 4000 10.2 µs 160.8 µs 90.63 KB
Read() 40 63.5 µs 75.3 µs 12.27 KB
Read() 4000 76.4 µs 75.9 µs 12.27 KB
Read(char[], 0, 4096) 40 3.93 µs 3.99 µs 12.27 KB
Read(char[], 0, 4096) 4000 4.26 µs 4.09 µs 12.27 KB

That is about 4 ns per character in ReadLine, with the same allocation. The reader is used only for console input that is not redirected.

Tests

  • ConsoleStreamReaderTests, new, 23 cases, compiling the reader's source in as KeyParserTests does. They cover lone \r, \n and \r\n endings; a \r and its \n arriving in separate reads; a line longer than the buffers; a surrogate pair and a two-byte UTF-8 character split across reads; Peek, Read, ReadBlock, ReadToEnd and span reads after a \r; and zero-length reads returning, and invalid arguments throwing, without a read from the stream. With the ReadLine override disabled, 3 of the 23 fail, ReadLine_LineEndingInCarriageReturn_DoesNotReadPastIt, Read_OfNoCharacters_DoesNotWaitForInput and Read_WithInvalidArguments_ThrowsWithoutWaitingForInput, each with the test stream throwing because it was read past the \r; the others pass either way, since their streams never make a read wait.
  • ReadLineWithTreatControlCAsInput, a new manual test, reads two lines with the property set. With the reader disabled in GetOrCreateReader it failed at the second Assert.Equal (Expected: "two", Actual: "") in a driven run; with it, it passes driven and by hand.
  • System.Console.Tests: 4341 total, 0 failed.
  • The Console.ReadLine() returns each line one Enter late when Console.TreatControlCAsInput is true on Windows #133814 repro, run on a local build under inbox conhost: each ReadLine() returns its line right after its own Enter; on .NET 10.0.12 it still lags one line. The local build does the same under OpenConsole.exe 1.0.200517002, run by hand.
  • Redirected input, from a file and from a pipe, with \r-only and \r\n line endings: output identical to .NET 10.0.12.

Not run: Linux and macOS, where Console.In is StdInReader, which this change does not touch; the manual test is Windows-only. Release configuration was not run either.

Note

AI-generated, written at my direction and reviewed by me before posting. Everything ran on Windows 11 x64 (build 26200). The tests and console runs used a local build of this branch, Debug libraries on a Checked runtime; the benchmark ran as described above. Console runs used inbox conhost unless named. The manual test was run both with keystrokes written into the console from another process (AttachConsole + WriteConsoleInput) and typed by hand; the repro was driven that way under conhost and typed by hand under OpenConsole.

With Console.TreatControlCAsInput set, ENABLE_PROCESSED_INPUT is clear
and the Windows console host ends a line with a lone '\r'. Console.In
wrapped a plain StreamReader, whose ReadLine reads ahead after '\r' to
look for a '\n'. On a console that read waits for the next line, so
each ReadLine returned only after the following Enter.

Add ConsoleStreamReader, used on Windows when input is not redirected,
the same condition under which Unix uses StdInReader. Its ReadLine ends
the line at '\r' without reading ahead, and the next read skips a '\n'
that pairs with it. Zero-length reads and reads with invalid arguments
leave that skip pending, so they return or throw without waiting.
Redirected input keeps the plain StreamReader.

Add unit tests for the reader and a Windows manual test.

Fix dotnet#133814

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 14, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-console
See info in area-owners.md if you want to be subscribed.

@caraioniurie47

Copy link
Copy Markdown
Author

/ba-g all failures are known System.Net issues: #122522, #132004, #133373, #133012, #133778

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Console community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console.ReadLine() returns each line one Enter late when Console.TreatControlCAsInput is true on Windows

1 participant