| File: Commands\Test\MTP\Terminal\SimpleTerminalBase.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using Microsoft.DotNet.Cli.Commands; using Microsoft.DotNet.Cli.Commands.Test.Terminal; namespace Microsoft.Testing.Platform.OutputDevice.Terminal; internal abstract class SimpleTerminal : ITerminal { private object? _batchingLock; private bool _isBatching; public SimpleTerminal(IConsole console) => Console = console; #pragma warning disable CA1416 // Validate platform compatibility public int Width => Console.IsOutputRedirected ? int.MaxValue : Console.BufferWidth; public int Height => Console.IsOutputRedirected ? int.MaxValue : Console.BufferHeight; protected IConsole Console { get; } public void Append(char value) => Console.Write(value); public virtual void Append(string value) => Console.Write(value); public void AppendLine() => Console.WriteLine(); public virtual void AppendLine(string value) => Console.WriteLine(value); public void AppendLink(string path, int? lineNumber) { Append(path); if (lineNumber.HasValue) { Append($":{lineNumber}"); } } public void EraseProgress() { // nop } public void HideCursor() { // nop } public void RenderProgress(TestProgressState?[] progress) { int count = 0; foreach (TestProgressState? p in progress) { if (p == null) { continue; } count++; string durationString = HumanReadableDurationFormatter.Render(p.Stopwatch.Elapsed); int discovered = p.DiscoveredTests; int passed = p.PassedTests; int failed = p.FailedTests; int skipped = p.SkippedTests; if (!p.IsDiscovery) { // Use just ascii here, so we don't put too many restrictions on fonts needing to // properly show unicode, or logs being saved in particular encoding. Append('['); SetColor(TerminalColor.DarkGreen); Append('+'); Append(passed.ToString(CultureInfo.CurrentCulture)); ResetColor(); Append('/'); SetColor(TerminalColor.DarkRed); Append('x'); Append(failed.ToString(CultureInfo.CurrentCulture)); ResetColor(); Append('/'); SetColor(TerminalColor.DarkYellow); Append('?'); Append(skipped.ToString(CultureInfo.CurrentCulture)); ResetColor(); Append(']'); } else { // Use just ascii here, so we don't put too many restrictions on fonts needing to // properly show unicode, or logs being saved in particular encoding. Append('['); SetColor(TerminalColor.DarkMagenta); Append('+'); Append(discovered.ToString(CultureInfo.CurrentCulture)); ResetColor(); Append(']'); } Append(' '); Append(p.AssemblyName); if (p.TargetFramework != null || p.Architecture != null) { Append(" ("); if (p.TargetFramework != null) { Append(p.TargetFramework); Append('|'); } if (p.Architecture != null) { Append(p.Architecture); } Append(')'); } TestDetailState? activeTest = p.TestNodeResultsState?.GetRunningTasks(1).FirstOrDefault(); if (!String.IsNullOrWhiteSpace(activeTest?.Text)) { Append(" - "); Append(activeTest.Text); Append(' '); } Append(durationString); AppendLine(); } // Do not render empty lines when there is nothing to show. if (count > 0) { AppendLine(); } } public void ShowCursor() { // nop } public void StartBusyIndicator() { // nop } // NonAnsiTerminal and AnsiTerminal could be refactored such that we don't need StartUpdate/StopUpdate. // It's much better if we use lock C# keyword instead of manually calling Monitor.Enter/Exit // Using lock also ensures we don't accidentally have `await`s in between that could cause Exit to be on a different thread. public void StartUpdate() { if (_isBatching) { throw new InvalidOperationException(CliCommandStrings.ConsoleIsAlreadyInBatchingMode); } bool lockTaken = false; // We store Console.Out in a field to make sure we will be doing // the Monitor.Exit call on the same instance. _batchingLock = System.Console.Out; // Note that we need to lock on System.Out for batching to work correctly. // Consider the following scenario: // 1. We call StartUpdate // 2. We call a Write("A") // 3. User calls Console.Write("B") from another thread. // 4. We call a Write("C"). // 5. We call StopUpdate. // The expectation is that we see either ACB, or BAC, but not ABC. // Basically, when doing batching, we want to ensure that everything we write is // written continuously, without anything in-between. // One option (and we used to do it), is that we append to a StringBuilder while batching // Then at StopUpdate, we write the whole string at once. // This works to some extent, but we cannot get it to work when SetColor kicks in. // Console methods will internally lock on Console.Out, so we are locking on the same thing. // This locking is the easiest way to get coloring to work correctly while preventing // interleaving with user's calls to Console.Write methods. // One extra note: // It's very important to lock on Console.Out (the current Console.Out). // Consider the following scenario: // 1. SystemConsole captures the original Console.Out set by runtime. // 2. Framework author sets his own Console.Out which wraps the original Console.Out. // 3. Two threads are writing concurrently: // - One thread is writing using Console.Write* APIs, which will use the Console.Out set by framework author. // - The other thread is writing using NonAnsiTerminal. // 4. **If** we lock the original Console.Out. The following may happen (subject to race) [NOT THE CURRENT CASE - imaginary situation if we lock on the original Console.Out]: // - First thread enters the Console.Write, which will acquire the lock for the current Console.Out (set by framework author). // - Second thread executes StartUpdate, and acquires the lock for the original Console.Out. // - First thread continues in the Write implementation of the framework author, which tries to run Console.Write on the original Console.Out. // - First thread can't make any progress, because the second thread is holding the lock already. // - Second thread continues execution, and reaches into runtime code (ConsolePal.WriteFromConsoleStream - on Unix) which tries to acquire the lock for the current Console.Out (set by framework author). // - (see https://github.com/dotnet/runtime/blob/8a9d492444f06df20fcc5dfdcf7a6395af18361f/src/libraries/System.Console/src/System/ConsolePal.Unix.cs#L963) // - No thread can progress. // - Basically, what happened is that the first thread acquires the lock for current Console.Out, then for the original Console.Out. // - while the second thread acquires the lock for the original Console.Out, then for the current Console.Out. // - That's a typical deadlock where two threads are acquiring two locks in reverse order. // 5. By locking the *current* Console.Out, we avoid the situation described in 4. Monitor.Enter(_batchingLock, ref lockTaken); if (!lockTaken) { // Can this happen? :/ throw new InvalidOperationException(); } _isBatching = true; } public void StopBusyIndicator() { // nop } public void StopUpdate() { Monitor.Exit(_batchingLock!); _batchingLock = null; _isBatching = false; } public abstract void SetColor(TerminalColor color); public abstract void ResetColor(); }