|
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
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, bool writeProgressImmediatelyAfterOutput, int updateEvery) : 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 bool _writeProgressImmediatelyAfterOutput = writeProgressImmediatelyAfterOutput;
private readonly int _updateEvery = updateEvery;
private TestProgressState?[] _progressItems = [];
/// <summary>
/// The thread that performs periodic refresh of the console output.
/// </summary>
private Thread? _refresher;
private long _counter;
/// <summary>
/// The <see cref="_refresher"/> thread proc.
/// </summary>
private void ThreadProc()
{
try
{
while (!_cts.Token.WaitHandle.WaitOne(_updateEvery))
{
lock (_lock)
{
_terminal.StartUpdate();
try
{
_terminal.RenderProgress(_progressItems);
}
finally
{
_terminal.StopUpdate();
}
}
}
}
catch (ObjectDisposedException)
{
// When we dispose _cts too early this will throw.
}
_terminal.EraseProgress();
}
public int AddWorker(TestProgressState testWorker)
{
if (_showProgress)
{
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();
// 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 (_showProgress)
{
_cts.Cancel();
_refresher?.Join();
_terminal.EraseProgress();
_terminal.StopBusyIndicator();
}
}
public void Dispose() =>
((IDisposable)_cts).Dispose();
internal void WriteToTerminal(Action<ITerminal> write)
{
if (_showProgress)
{
lock (_lock)
{
try
{
_terminal.StartUpdate();
_terminal.EraseProgress();
write(_terminal);
if (_writeProgressImmediatelyAfterOutput)
{
_terminal.RenderProgress(_progressItems);
}
}
finally
{
_terminal.StopUpdate();
}
}
}
else
{
lock (_lock)
{
try
{
_terminal.StartUpdate();
write(_terminal);
}
finally
{
_terminal.StopUpdate();
}
}
}
}
internal void RemoveWorker(int slotIndex)
{
if (_showProgress)
{
_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.
_counter++;
TestProgressState? progress = _progressItems[slotIndex];
if (progress != null)
{
progress.Version = _counter;
}
}
}
}
|