spectreconsole / spectre.console

A .NET library that makes it easier to create beautiful console applications.
https://spectreconsole.net
MIT License
9.17k stars 472 forks source link

Markup is not properly rendered (when chunked) while a status is displayed #583

Open 0xced opened 2 years ago

0xced commented 2 years ago

Information

Describe the bug While a status is being displayed, markup may not be fully rendered. It works fine with a single call to console.MarkupLine() but several calls to console.Markup() followed by a call to console.MarkupLine("") renders only the last chunk.

To Reproduce Here's a minimal sample code that demonstrates the issue:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.42.0" />
  </ItemGroup>

</Project>
using System;
using System.Linq;
using System.Threading.Tasks;
using Spectre.Console;

var console = AnsiConsole.Console;
try
{
    var totalDuration = TimeSpan.FromSeconds(4);
    const int maxStep = 10;

    var task = console.Status().StartAsync("Performing a long running operation...", async _ => await Task.Delay(totalDuration / 2));

    var split = args.Contains("--split");
    foreach (var i in Enumerable.Range(1, maxStep))
    {
        var chunks = new[]
        {
            $"[grey][[[/][silver]{DateTime.Now:hh:mm:ss.ff}[/][grey]]][/] ",
            "Step ",
            $"[olive]{i,2}[/]",
            " / ",
            $"[purple]{maxStep}[/]"
        };

        if (split)
        {
            foreach (var chunk in chunks)
            {
                console.Markup(chunk);
            }
            console.MarkupLine("");
        }
        else
        {
            console.MarkupLine(string.Join("", chunks));
        }

        await Task.Delay(totalDuration / maxStep);
    }

    await task;
}
catch (Exception exception)
{
    console.WriteException(exception);
}

Then run the program with dotnet run -- --split

Expected behavior

The 10 steps should be fully printed, but step 2 to 5 are not properly rendered.

Screenshots Here's a screen recording of running the sample app with dotnet run (executing a single console.MarkupLine()) and dotnet run -- --split (executing several calls to console.Markup())

Additional context I'm currently trying to port Serilog.Sinks.Console to use Spectre.Console and writing to the console in several chunks is pretty much required in that situation.


Please upvote :+1: this issue if you are interested in it.

phil-scott-78 commented 2 years ago

Think you are running into a threading issue, not an issue with markup. The status is running side-by-side with the rendering which isn't supported out of the box.

Combining the two would work though, something like this

var console = AnsiConsole.Console;
try
{
    var totalDuration = TimeSpan.FromSeconds(4);
    const int maxStep = 10;

    await console.Status().StartAsync("Performing a long running operation...", async _ =>
    {

        var split = args.Contains("--split");
        foreach (var i in Enumerable.Range(1, maxStep))
        {
            var chunks = new[]
            {
                $"[grey][[[/][silver]{DateTime.Now:hh:mm:ss.ff}[/][grey]]][/] ",
                "Step ",
                $"[olive]{i,2}[/]",
                " / ",
                $"[purple]{maxStep}[/]"
            };

            if (split)
            {
                foreach (var chunk in chunks)
                {
                    console.Markup(chunk);
                }
                console.MarkupLine("");
            }
            else
            {
                console.MarkupLine(string.Join("", chunks));
            }

            await Task.Delay(totalDuration / maxStep);
        }

        await Task.Delay(totalDuration / 2);
    });
}
catch (Exception exception)
{
    console.WriteException(exception);
}
0xced commented 2 years ago

Hmmm, I just tried your suggestion but that doesn't solve the rendering issue at all. Here's the result that I get when running the chunked version (--split):

[11:50:24.92] Step  1 / 10
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10                                                    
10
phil-scott-78 commented 2 years ago

just had a chance to get on my pc, and you are right. at least it consently isn't working though rather than failing half the time.

Curious issue. Seems the same thing happens to Write too so it isn't just the markup parsing or anything like that.

patriksvensson commented 2 years ago

@0xced @phil-scott-78 I suspect that we're not locking the console properly when rendering and that we're only locking the render pipeline. I can take a look at this tomorrow evening.

0xced commented 2 years ago

Actually, I think that's because the cursor is periodically repositioned while the status is active and it looks like this is problematic on a line that has not (yet) been terminated by a newline.

patriksvensson commented 2 years ago

@0xced That is part of the LiveRenderable that hooks up to the render pipeline which is synchronized so shouldn't affect interlocking calls. Need to investigate if there is any call NOT using the render pipeline, or if we need to lock the console.

phil-scott-78 commented 2 years ago

I had a few moments tonight after the kiddo went to bed. Simplified the demo to something simpler

var console = AnsiConsole.Console;
const int maxStep = 10;

await console.Status().AutoRefresh(false).StartAsync("Performing a long running operation...", async ctx =>
{
    foreach (var i in Enumerable.Range(1, maxStep))
    {
        for (var j = 0; j < 1000; j++)
        {
            console.Write(j.ToString());
        }

        ctx.Refresh();
    }
});

First sequence of numbers gets fully printed to the screen, then I manually call Refresh(). After that first refresh it goes off the rails. Basically moving the cursor back to the start after every Write.

Another thing I noticed poking around that perhaps could be related - AnsiConsoleCursor is going right at the backend to write rather than using the AnsiConsoleFacade. Could this be causing some of the contention?

Some, my midnight brain dump before bed:

  1. We are resetting the cursor WAY more than we need to be for some reason after the first call to Refresh()
  2. The cursor reset doesn't respect the renderlock, but should it?
0xced commented 2 years ago

Do you have any update on this? I would love to continue my port of Serilog.Sinks.Console to Spectre.Console and this issue is a blocker.

XanderStoffels commented 1 year ago

Same problem here.

await AnsiConsole.Status()
    .Spinner(Spinner.Known.Dots2)
    .SpinnerStyle(Style.Parse("yellow"))
    .StartAsync("Standby...", async ctx =>
    {
        // Check if online
        AnsiConsole.Markup("Connecting to [bold yellow bold]SpaceTraders[/]...");

        var status = await spaceTraders.GetStatusAsync();
        AnsiConsole.MarkupLine("Done");

       // ...
    });

When calling MarkupLine("Done"), the text 'Connecting to' disappears from the same line.

kparkov commented 3 months ago

This makes the status feature pretty broken. It is not possible to report any progress at all. How do people use it?

patriksvensson commented 3 months ago

@kparkov I'm sorry, but "not possible to report any progress at all" seems excessive when this issue concerns edge cases.

We literally have working examples of how to report progress in our documentation.

kparkov commented 3 months ago

@patriksvensson hm, the basic example in the docs does not work in Windows Terminal, for instance. I don't think that is an edge case. And what is edge about the example described above? Not trying to be snarky, I'm perfectly open to the fact that I may be a little slow here.

patriksvensson commented 3 months ago

@kparkov It works perfectly for me. What exactly is not working in Windows Terminal?