File: parent\dotnet\Commands\Test\MTP\Terminal\TestProgressStateAwareTerminal.cs
Web Access
Project: dotnet-aot.csproj (dotnet-aot)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Cli.Commands.Test.Terminal;

/// <summary>
/// Terminal that updates the progress in place when progress reporting is enabled.
/// </summary>
internal sealed partial class TestProgressStateAwareTerminal(ITerminal terminal, bool showProgress, IProgressRenderer? renderer = null) : IDisposable
{
    /// <summary>
    /// A cancellation token to signal the rendering thread that it should exit.
    /// </summary>
    private readonly CancellationTokenSource _cts = new();

    /// <summary>
    /// Protects access to state shared between the logger callbacks and the rendering thread.
    /// </summary>
    private readonly object _lock = Console.Out;

    private readonly ITerminal _terminal = terminal;
    private readonly bool _showProgress = showProgress;
    private readonly IProgressRenderer _renderer = renderer ?? new CursorProgressRenderer();
    private TestProgressState?[] _progressItems = [];

    /// <summary>
    /// The thread that performs periodic refresh of the console output.
    /// </summary>
    private Thread? _refresher;
    private long _counter;
    private int _stopped;

    /// <summary>
    /// The <see cref="_refresher"/> thread proc.
    /// </summary>
    private void ThreadProc()
    {
        try
        {
            while (!_cts.Token.WaitHandle.WaitOne(_renderer.TickInterval))
            {
                lock (_lock)
                {
                    _terminal.StartUpdate();
                    try
                    {
                        _renderer.OnTick(_terminal, _progressItems);
                    }
                    finally
                    {
                        _terminal.StopUpdate();
                    }
                }
            }
        }
        catch (ObjectDisposedException)
        {
            // When we dispose _cts too early this will throw.
        }
        catch (Exception ex)
        {
            Logger.LogTrace($"Unexpected exception while rendering test progress: {ex}");
        }

        _terminal.EraseProgress();
    }

    public int AddWorker(TestProgressState testWorker)
    {
        if (_showProgress)
        {
            lock (_lock)
            {
                for (int i = 0; i < _progressItems.Length; i++)
                {
                    if (_progressItems[i] == null)
                    {
                        _progressItems[i] = testWorker;
                        return i;
                    }
                }

                throw new InvalidOperationException("No empty slot found");
            }
        }

        return 0;
    }

    public void StartShowingProgress(int workerCount)
    {
        if (_showProgress)
        {
            _progressItems = new TestProgressState[workerCount];
            _terminal.StartBusyIndicator();
            _renderer.OnStart();
            // If we crash unexpectedly without completing this thread we don't want it to keep the process running.
            _refresher = new Thread(ThreadProc) { IsBackground = true };
            _refresher.Start();
        }
    }

    internal void StopShowingProgress()
    {
        if (Interlocked.Exchange(ref _stopped, 1) != 0)
        {
            return;
        }

        if (_showProgress)
        {
            _cts.Cancel();
            _refresher?.Join();

            _terminal.EraseProgress();
            _terminal.StopBusyIndicator();
        }
    }

    public void Dispose()
    {
        StopShowingProgress();
        _cts.Dispose();
    }

    internal void NotifyTestCompleted() => _renderer.OnTestCompleted();

    internal void WriteToTerminal(Action<ITerminal> write)
    {
        if (_showProgress)
        {
            lock (_lock)
            {
                try
                {
                    _terminal.StartUpdate();
                    _renderer.OnWrite(_terminal, _progressItems, write);
                }
                finally
                {
                    _terminal.StopUpdate();
                }
            }
        }
        else
        {
            lock (_lock)
            {
                try
                {
                    _terminal.StartUpdate();
                    write(_terminal);
                }
                finally
                {
                    _terminal.StopUpdate();
                }
            }
        }
    }

    internal void RemoveWorker(int slotIndex)
    {
        if (_showProgress)
        {
            lock (_lock)
            {
                _progressItems[slotIndex] = null;
            }
        }
    }

    internal void UpdateWorker(int slotIndex)
    {
        if (_showProgress)
        {
            // We increase the counter to say that this version of data is newer than what we had before and
            // it should be completely re-rendered. Another approach would be to use timestamps, or to replace the
            // instance and compare that, but that means more objects floating around.
            lock (_lock)
            {
                _counter++;
                TestProgressState? progress = _progressItems[slotIndex];
                if (progress != null)
                {
                    progress.Version = _counter;
                }
            }
        }
    }
}