Skip to content

Commit ec0251b

Browse files
authored
Console.Unix: for echoing, write back to the terminal instead of writing to Console.Out. (#94414)
* Console.Unix: for echoing, write back to the terminal instead of writing to Console.Out. On Unix .NET implements its own echo to make Console behave similar to Windows. The echo implementation was writing to standard output, which has the unintended effect of writing the echo characters into a redirected standard output. This changes to write the echo to the stdin terminal where they were read from.
1 parent 115199d commit ec0251b

File tree

10 files changed

+144
-119
lines changed

10 files changed

+144
-119
lines changed

src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetWindowWidth.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Runtime.InteropServices;
6+
using Microsoft.Win32.SafeHandles;
67

78
internal static partial class Interop
89
{
@@ -18,7 +19,7 @@ internal struct WinSize
1819
};
1920

2021
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetWindowSize", SetLastError = true)]
21-
internal static partial int GetWindowSize(out WinSize winSize);
22+
internal static partial int GetWindowSize(SafeFileHandle terminalHandle, out WinSize winSize);
2223

2324
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetWindowSize", SetLastError = true)]
2425
internal static partial int SetWindowSize(in WinSize winSize);

src/libraries/Common/src/Interop/Unix/System.Native/Interop.InitializeTerminalAndSignalHandling.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Runtime.InteropServices;
5+
using Microsoft.Win32.SafeHandles;
56

67
internal static partial class Interop
78
{
@@ -12,6 +13,6 @@ internal static partial class Sys
1213
internal static partial bool InitializeTerminalAndSignalHandling();
1314

1415
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetKeypadXmit", StringMarshalling = StringMarshalling.Utf8)]
15-
internal static partial void SetKeypadXmit(string terminfoString);
16+
internal static partial void SetKeypadXmit(SafeFileHandle terminalHandle, string terminfoString);
1617
}
1718
}

src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public override int Read(Span<byte> buffer) =>
4747
ConsolePal.Read(_handle, buffer);
4848

4949
public override void Write(ReadOnlySpan<byte> buffer) =>
50-
ConsolePal.Write(_handle, buffer);
50+
ConsolePal.WriteFromConsoleStream(_handle, buffer);
5151

5252
public override void Flush()
5353
{

src/libraries/System.Console/src/System/ConsolePal.Unix.cs

Lines changed: 65 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ internal static partial class ConsolePal
3737
private static int s_windowWidth; // Cached WindowWidth, -1 when invalid.
3838
private static int s_windowHeight; // Cached WindowHeight, invalid when s_windowWidth == -1.
3939
private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings.
40+
private static SafeFileHandle? s_terminalHandle; // Tracks the handle used for writing to the terminal.
4041

4142
/// <summary>Gets the lazily-initialized terminal information for the terminal.</summary>
4243
public static TerminalFormatStrings TerminalFormatStringsInstance { get { return s_terminalFormatStringsInstance.Value; } }
@@ -127,13 +128,7 @@ public static ConsoleKeyInfo ReadKey(bool intercept)
127128
throw new InvalidOperationException(SR.InvalidOperation_ConsoleReadKeyOnFile);
128129
}
129130

130-
bool previouslyProcessed;
131-
ConsoleKeyInfo keyInfo = StdInReader.ReadKey(out previouslyProcessed);
132-
133-
if (!intercept && !previouslyProcessed && keyInfo.KeyChar != '\0')
134-
{
135-
Console.Write(keyInfo.KeyChar);
136-
}
131+
ConsoleKeyInfo keyInfo = StdInReader.ReadKey(intercept);
137132
return keyInfo;
138133
}
139134

@@ -205,7 +200,7 @@ public static string Title
205200
if (!string.IsNullOrEmpty(titleFormat))
206201
{
207202
string ansiStr = TermInfo.ParameterizedStrings.Evaluate(titleFormat, value);
208-
WriteStdoutAnsiString(ansiStr, mayChangeCursorPosition: false);
203+
WriteTerminalAnsiString(ansiStr, mayChangeCursorPosition: false);
209204
}
210205
}
211206
}
@@ -214,15 +209,15 @@ public static void Beep()
214209
{
215210
if (!Console.IsOutputRedirected)
216211
{
217-
WriteStdoutAnsiString(TerminalFormatStringsInstance.Bell, mayChangeCursorPosition: false);
212+
WriteTerminalAnsiString(TerminalFormatStringsInstance.Bell, mayChangeCursorPosition: false);
218213
}
219214
}
220215

221216
public static void Clear()
222217
{
223218
if (!Console.IsOutputRedirected)
224219
{
225-
WriteStdoutAnsiString(TerminalFormatStringsInstance.Clear);
220+
WriteTerminalAnsiString(TerminalFormatStringsInstance.Clear);
226221
}
227222
}
228223

@@ -231,6 +226,11 @@ public static void SetCursorPosition(int left, int top)
231226
if (Console.IsOutputRedirected)
232227
return;
233228

229+
SetTerminalCursorPosition(left, top);
230+
}
231+
232+
public static void SetTerminalCursorPosition(int left, int top)
233+
{
234234
lock (Console.Out)
235235
{
236236
if (TryGetCachedCursorPosition(out int leftCurrent, out int topCurrent) &&
@@ -244,7 +244,7 @@ public static void SetCursorPosition(int left, int top)
244244
if (!string.IsNullOrEmpty(cursorAddressFormat))
245245
{
246246
string ansiStr = TermInfo.ParameterizedStrings.Evaluate(cursorAddressFormat, top, left);
247-
WriteStdoutAnsiString(ansiStr);
247+
WriteTerminalAnsiString(ansiStr);
248248
}
249249

250250
SetCachedCursorPosition(left, top);
@@ -355,19 +355,18 @@ private static void GetWindowSize(out int width, out int height)
355355
// Invalidate before reading cached values.
356356
CheckTerminalSettingsInvalidated();
357357

358-
if (s_windowWidth == -1)
358+
Interop.Sys.WinSize winsize;
359+
if (s_windowWidth == -1 &&
360+
s_terminalHandle != null &&
361+
Interop.Sys.GetWindowSize(s_terminalHandle, out winsize) == 0)
359362
{
360-
Interop.Sys.WinSize winsize;
361-
if (Interop.Sys.GetWindowSize(out winsize) == 0)
362-
{
363-
s_windowWidth = winsize.Col;
364-
s_windowHeight = winsize.Row;
365-
}
366-
else
367-
{
368-
s_windowWidth = TerminalFormatStringsInstance.Columns;
369-
s_windowHeight = TerminalFormatStringsInstance.Lines;
370-
}
363+
s_windowWidth = winsize.Col;
364+
s_windowHeight = winsize.Row;
365+
}
366+
else
367+
{
368+
s_windowWidth = TerminalFormatStringsInstance.Columns;
369+
s_windowHeight = TerminalFormatStringsInstance.Lines;
371370
}
372371
width = s_windowWidth;
373372
height = s_windowHeight;
@@ -403,7 +402,7 @@ public static bool CursorVisible
403402
{
404403
if (!Console.IsOutputRedirected)
405404
{
406-
WriteStdoutAnsiString(value ?
405+
WriteTerminalAnsiString(value ?
407406
TerminalFormatStringsInstance.CursorVisible :
408407
TerminalFormatStringsInstance.CursorInvisible);
409408
}
@@ -412,6 +411,11 @@ public static bool CursorVisible
412411

413412
public static (int Left, int Top) GetCursorPosition()
414413
{
414+
if (Console.IsInputRedirected || Console.IsOutputRedirected)
415+
{
416+
return (0, 0);
417+
}
418+
415419
TryGetCursorPosition(out int left, out int top);
416420
return (left, top);
417421
}
@@ -436,14 +440,9 @@ public static (int Left, int Top) GetCursorPosition()
436440
/// <param name="reinitializeForRead">Indicates whether this method is called as part of a on-going Read operation.</param>
437441
internal static bool TryGetCursorPosition(out int left, out int top, bool reinitializeForRead = false)
438442
{
439-
left = top = 0;
443+
Debug.Assert(!Console.IsInputRedirected);
440444

441-
// Getting the cursor position involves both writing out a request string and
442-
// parsing a response string from the terminal. So if anything is redirected, bail.
443-
if (Console.IsInputRedirected || Console.IsOutputRedirected)
444-
{
445-
return false;
446-
}
445+
left = top = 0;
447446

448447
int cursorVersion;
449448
lock (Console.Out)
@@ -485,7 +484,7 @@ internal static bool TryGetCursorPosition(out int left, out int top, bool reinit
485484
{
486485
// Write out the cursor position report request.
487486
Debug.Assert(!string.IsNullOrEmpty(TerminalFormatStrings.CursorPositionReport));
488-
WriteStdoutAnsiString(TerminalFormatStrings.CursorPositionReport, mayChangeCursorPosition: false);
487+
WriteTerminalAnsiString(TerminalFormatStrings.CursorPositionReport, mayChangeCursorPosition: false);
489488

490489
// Read the cursor position report (CPR), of the form \ESC[row;colR. This is not
491490
// as easy as it sounds. Prior to the CPR having been supplied to stdin, other
@@ -802,7 +801,7 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
802801
string evaluatedString = s_fgbgAndColorStrings[fgbgIndex, ccValue]; // benign race
803802
if (evaluatedString != null)
804803
{
805-
WriteStdoutAnsiString(evaluatedString);
804+
WriteTerminalAnsiString(evaluatedString);
806805
return;
807806
}
808807

@@ -842,7 +841,7 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
842841
int ansiCode = consoleColorToAnsiCode[ccValue] % maxColors;
843842
evaluatedString = TermInfo.ParameterizedStrings.Evaluate(formatString, ansiCode);
844843

845-
WriteStdoutAnsiString(evaluatedString);
844+
WriteTerminalAnsiString(evaluatedString);
846845

847846
s_fgbgAndColorStrings[fgbgIndex, ccValue] = evaluatedString; // benign race
848847
}
@@ -854,7 +853,7 @@ private static void WriteResetColorString()
854853
{
855854
if (ConsoleUtils.EmitAnsiColorCodes)
856855
{
857-
WriteStdoutAnsiString(TerminalFormatStringsInstance.Reset);
856+
WriteTerminalAnsiString(TerminalFormatStringsInstance.Reset);
858857
}
859858
}
860859

@@ -897,16 +896,17 @@ private static unsafe void EnsureInitializedCore()
897896
throw new Win32Exception();
898897
}
899898

899+
s_terminalHandle = !Console.IsOutputRedirected ? Interop.Sys.FileDescriptors.STDOUT_FILENO :
900+
!Console.IsInputRedirected ? Interop.Sys.FileDescriptors.STDIN_FILENO :
901+
null;
902+
900903
// Provide the native lib with the correct code from the terminfo to transition us into
901904
// "application mode". This will both transition it immediately, as well as allow
902905
// the native lib later to handle signals that require re-entering the mode.
903-
if (!Console.IsOutputRedirected)
906+
if (s_terminalHandle != null &&
907+
TerminalFormatStringsInstance.KeypadXmit is string keypadXmit)
904908
{
905-
string? keypadXmit = TerminalFormatStringsInstance.KeypadXmit;
906-
if (keypadXmit != null)
907-
{
908-
Interop.Sys.SetKeypadXmit(keypadXmit);
909-
}
909+
Interop.Sys.SetKeypadXmit(s_terminalHandle, keypadXmit);
910910
}
911911

912912
if (!Console.IsInputRedirected)
@@ -952,17 +952,32 @@ private static unsafe int Read(SafeFileHandle fd, Span<byte> buffer)
952952
}
953953
}
954954

955+
internal static void WriteToTerminal(ReadOnlySpan<byte> buffer, bool mayChangeCursorPosition = true)
956+
{
957+
Debug.Assert(s_terminalHandle is not null);
958+
959+
lock (Console.Out) // synchronize with other writers
960+
{
961+
Write(s_terminalHandle, buffer, mayChangeCursorPosition);
962+
}
963+
}
964+
965+
internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan<byte> buffer)
966+
{
967+
EnsureConsoleInitialized();
968+
969+
lock (Console.Out) // synchronize with other writers
970+
{
971+
Write(fd, buffer);
972+
}
973+
}
974+
955975
/// <summary>Writes data from the buffer into the file descriptor.</summary>
956976
/// <param name="fd">The file descriptor.</param>
957977
/// <param name="buffer">The buffer from which to write data.</param>
958978
/// <param name="mayChangeCursorPosition">Writing this buffer may change the cursor position.</param>
959-
internal static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer, bool mayChangeCursorPosition = true)
979+
private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer, bool mayChangeCursorPosition = true)
960980
{
961-
// Console initialization might emit data to stdout.
962-
// In order to avoid splitting user data we need to
963-
// complete it before any writes are performed.
964-
EnsureConsoleInitialized();
965-
966981
fixed (byte* p = buffer)
967982
{
968983
byte* bufPtr = p;
@@ -1098,7 +1113,7 @@ private static void InvalidateTerminalSettings()
10981113
/// <summary>Writes a terminfo-based ANSI escape string to stdout.</summary>
10991114
/// <param name="value">The string to write.</param>
11001115
/// <param name="mayChangeCursorPosition">Writing this value may change the cursor position.</param>
1101-
internal static void WriteStdoutAnsiString(string? value, bool mayChangeCursorPosition = true)
1116+
internal static void WriteTerminalAnsiString(string? value, bool mayChangeCursorPosition = true)
11021117
{
11031118
if (string.IsNullOrEmpty(value))
11041119
return;
@@ -1115,10 +1130,8 @@ internal static void WriteStdoutAnsiString(string? value, bool mayChangeCursorPo
11151130
data = Encoding.UTF8.GetBytes(value);
11161131
}
11171132

1118-
lock (Console.Out) // synchronize with other writers
1119-
{
1120-
Write(Interop.Sys.FileDescriptors.STDOUT_FILENO, data, mayChangeCursorPosition);
1121-
}
1133+
EnsureConsoleInitialized();
1134+
WriteToTerminal(data, mayChangeCursorPosition);
11221135
}
11231136
}
11241137
}

src/libraries/System.Console/src/System/ConsolePal.Wasi.cs

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,20 @@ namespace System
1818
// to also change the test class.
1919
internal static partial class ConsolePal
2020
{
21-
// StdInReader is only used when input isn't redirected and we're working
22-
// with an interactive terminal. In that case, performance isn't critical
23-
// and we can use a smaller buffer to minimize working set.
24-
// there is no dup on WASI
2521
public static Stream OpenStandardInput()
2622
{
27-
return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.FileDescriptors.STDIN_FILENO), FileAccess.Read,
23+
return new UnixConsoleStream(Interop.Sys.FileDescriptors.STDIN_FILENO, FileAccess.Read,
2824
useReadLine: !Console.IsInputRedirected);
2925
}
3026

3127
public static Stream OpenStandardOutput()
3228
{
33-
return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.FileDescriptors.STDOUT_FILENO), FileAccess.Write);
29+
return new UnixConsoleStream(Interop.Sys.FileDescriptors.STDOUT_FILENO, FileAccess.Write);
3430
}
3531

3632
public static Stream OpenStandardError()
3733
{
38-
return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.FileDescriptors.STDERR_FILENO), FileAccess.Write);
34+
return new UnixConsoleStream(Interop.Sys.FileDescriptors.STDERR_FILENO, FileAccess.Write);
3935
}
4036

4137
public static Encoding InputEncoding
@@ -254,17 +250,21 @@ private static unsafe int Read(SafeFileHandle fd, Span<byte> buffer)
254250
}
255251
}
256252

253+
internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan<byte> buffer)
254+
{
255+
EnsureConsoleInitialized();
256+
257+
lock (Console.Out) // synchronize with other writers
258+
{
259+
Write(fd, buffer);
260+
}
261+
}
262+
257263
/// <summary>Writes data from the buffer into the file descriptor.</summary>
258264
/// <param name="fd">The file descriptor.</param>
259265
/// <param name="buffer">The buffer from which to write data.</param>
260-
/// <param name="mayChangeCursorPosition">Writing this buffer may change the cursor position.</param>
261-
internal static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer, bool mayChangeCursorPosition = true)
266+
private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer)
262267
{
263-
// Console initialization might emit data to stdout.
264-
// In order to avoid splitting user data we need to
265-
// complete it before any writes are performed.
266-
EnsureConsoleInitialized();
267-
268268
fixed (byte* p = buffer)
269269
{
270270
byte* bufPtr = p;
@@ -298,36 +298,11 @@ internal static unsafe void Write(SafeFileHandle fd, ReadOnlySpan<byte> buffer,
298298
throw Interop.GetExceptionForIoErrno(errorInfo);
299299
}
300300
}
301+
301302
count -= bytesWritten;
302303
bufPtr += bytesWritten;
303304
}
304305
}
305306
}
306-
307-
/// <summary>Writes a terminfo-based ANSI escape string to stdout.</summary>
308-
/// <param name="value">The string to write.</param>
309-
/// <param name="mayChangeCursorPosition">Writing this value may change the cursor position.</param>
310-
internal static void WriteStdoutAnsiString(string? value, bool mayChangeCursorPosition = true)
311-
{
312-
if (string.IsNullOrEmpty(value))
313-
return;
314-
315-
scoped Span<byte> data;
316-
if (value.Length <= 256) // except for extremely rare cases, ANSI escape strings are very short
317-
{
318-
data = stackalloc byte[Encoding.UTF8.GetMaxByteCount(value.Length)];
319-
int bytesToWrite = Encoding.UTF8.GetBytes(value, data);
320-
data = data.Slice(0, bytesToWrite);
321-
}
322-
else
323-
{
324-
data = Encoding.UTF8.GetBytes(value);
325-
}
326-
327-
lock (Console.Out) // synchronize with other writers
328-
{
329-
Write(Interop.Sys.FileDescriptors.STDOUT_FILENO, data, mayChangeCursorPosition);
330-
}
331-
}
332307
}
333308
}

0 commit comments

Comments
 (0)