dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.06k stars 4.69k forks source link

Invalid cached console cursor position on Unix #77995

Closed qt-kaneko closed 1 year ago

qt-kaneko commented 1 year ago

Description

Console cursor moves incorrectly from the end of the line to its beginning.

Reproduction Steps

Console.CursorLeft = Console.BufferWidth - 1;
Console.Write("2");

Console.CursorLeft = 0;
Console.Write("1");

Expected behavior

1 should be printed at the beginning of the line

Actual behavior

1 is printed after 2 (resize terminal to see this, 1 may be wrapped to the next line)

Regression?

No response

Known Workarounds

Reset cached cursor position, either with moving cursor to [0, 0], or using reflection to invalidate ConsolePal.s_cursorLeft

Configuration

Other information

Somewhere in ConsolePal, in things related to the cached cursor position and its calculation.

I think this is due to cached cursor position is computed like on windows (when cursor reaches end of line, and last char is printed, it moves to the next line), while on my tested configuration this is not.

Check ConsolePal.s_cursorLeft and ConsolePal.s_cursorTop with provided example. Cached position is on 0 row instead of Console.WindowWidth row, so it "thinks" that cursor is already at the beginning of the line and there is no need to move it.

Test code:

using System.Diagnostics;
using System.Reflection;

var ConsolePal = Assembly.Load("System.Console").GetType("System.ConsolePal")!;
var s_cursorLeft = ConsolePal.GetField("s_cursorLeft", BindingFlags.NonPublic | BindingFlags.Static)!;
var s_cursorTop = ConsolePal.GetField("s_cursorTop", BindingFlags.NonPublic | BindingFlags.Static)!;

Console.CursorLeft = Console.BufferWidth - 1;
Console.Write("2");

Debug.WriteLine("[X: " + s_cursorLeft.GetValue(null) + "], [Y: " + s_cursorTop.GetValue(null) + "]");

Console.CursorLeft = 0;
Console.Write("1"); // 1 will be printed after 2 instead of at the beginning of the line (resize terminal to see this, 1 may be wrapped to the next line)

Task.Delay(Timeout.Infinite).Wait();
image image

(On the 2nd image terminal is made wider by 1 symbol)

ghost commented 1 year ago

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

Issue Details
### Description Console cursor moves incorrectly from the end of the line to its beginning. ### Reproduction Steps ``` Console.CursorLeft = Console.BufferWidth - 1; Console.Write("2"); Console.CursorLeft = 0; Console.Write("1"); ``` ### Expected behavior 1 should be printed at the beginning of the line ### Actual behavior 1 is printed after 2 (resize terminal to see this, 1 may be wrapped to the next line) ### Regression? _No response_ ### Known Workarounds Reset cached cursor position, either with moving cursor to [0, 0], or using reflection to invalidate ConsolePal.s_cursorLeft ### Configuration - .NET 6 macOS 12.6 ARM64 - .NET 7 macOS 12.6 ARM64 - .NET 7 Arco Linux x64 ### Other information Somewhere in [ConsolePal](https://source.dot.net/#System.Console/System/ConsolePal.Unix.cs), in things related to the cached cursor position and its calculation. I think this is due to cached cursor position is computed like on windows (when cursor reaches end of line, and last char is printed, it moves to the next line), while on my tested configuration this is not. Check ConsolePal.s_cursorLeft and ConsolePal.s_cursorTop with provided example. Cached position is on 0 row instead of `Console.WindowWidth` row, so it "thinks" that cursor is already at the beginning of the line and there is no need to move it. Test code: ``` using System.Diagnostics; using System.Reflection; var ConsolePal = Assembly.Load("System.Console").GetType("System.ConsolePal")!; var s_cursorLeft = ConsolePal.GetField("s_cursorLeft", BindingFlags.NonPublic | BindingFlags.Static)!; var s_cursorTop = ConsolePal.GetField("s_cursorTop", BindingFlags.NonPublic | BindingFlags.Static)!; Console.CursorLeft = Console.BufferWidth - 1; Console.Write("2"); Debug.WriteLine("[X: " + s_cursorLeft.GetValue(null) + "], [Y: " + s_cursorTop.GetValue(null) + "]"); Console.CursorLeft = 0; Console.Write("1"); // 1 will be printed after 2 instead of at the beginning of the line (resize terminal to see this, 1 may be wrapped to the next line) Task.Delay(Timeout.Infinite).Wait(); ``` image image (On the 2nd image terminal is made wider by 1 symbol)
Author: qt-kaneko
Assignees: -
Labels: `area-System.Console`, `untriaged`
Milestone: -
tmds commented 1 year ago

Somewhere in ConsolePal

It happens here:

https://github.com/dotnet/runtime/blob/f33badf332c43c0d325e8c802466ea5fd362fb91/src/libraries/System.Console/src/System/ConsolePal.Unix.cs#L1055-L1059

A reproducer without resizing:

Console.WriteLine("Cached:");
Console.CursorLeft = Console.BufferWidth - 1;
Console.Write("2");
Console.CursorLeft = 0;
Console.Write("1");
Console.WriteLine();

Console.WriteLine("Invalidated cache:");
Console.CursorLeft = Console.BufferWidth - 1;
Console.Write((char)0xA3);
Console.CursorLeft = 0;
Console.Write("1");
Console.WriteLine();

You'd expect the same number of lines to be printed in both cases, but that's not the true. In the terminal window (on Linux) you see:

Cached position:
                                         2
1
Invalidated position:
1                                        £

And after resizing:

Cached position:
                                         21
Invalidated position:
1                                        £

I think this is due to cached cursor position is computed like on windows

Have you tried what happens on Windows? Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

adamsitnik commented 1 year ago

Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

Before resize:

image

After resize:

image

Running again after resize:

image

qt-kaneko commented 1 year ago

Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

Wrapping enabled:

Before resize

image

After resize + re-run

image

Wrapping disabled:

image
qt-kaneko commented 1 year ago

I think, trying to provide simpler example, I have broke it (due to windows moves cursor to next line, we need to reset it back to the initial line, so Console.CursorLeft is not enough), sorry.

This code should work:

Console.Clear();

Console.SetCursorPosition(Console.WindowWidth - 1, Console.WindowHeight - 1);
Console.Write("2");

Console.SetCursorPosition(0, Console.WindowHeight - 1);
Console.Write("1");

Without invalidation:

Windows:

Before resizing:

image

After resizing:

image

macOS (the same on Arco Linux):

Before resizing:

image

After resizing:

image

With invalidation:

macOS (the same on Arco Linux):

Before resizing:

image

After resizing:

image

Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

Wrapping enabled:

Before resize image

After resize + re-run image

Wrapping disabled:

image
tmds commented 1 year ago

On Windows the behavior is the same as for the invalidated character on Linux: even when the characters are printed on the next line due to wrapping, the terminal still considers them to be part of the previous 'logical' line.

As we have no idea of what a 'logical' line is, we should invalidate the cache when we're at the window boundary.