Skip to content

Commit

Permalink
fix(core): enable ANSI colors console mode on windows
Browse files Browse the repository at this point in the history
closes #131
  • Loading branch information
ostridm committed Dec 14, 2022
1 parent 3c6a95c commit e108b6f
Show file tree
Hide file tree
Showing 13 changed files with 454 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static IServiceCollection AddSecTesterConfig(this IServiceCollection coll
collection
.AddSingleton(configuration)
.AddSingleton<SystemTimeProvider>(new UtcSystemTimeProvider())
.AddSingleton<AnsiCodeColorizer>(new DefaultAnsiCodeColorizer(ConsoleUtils.IsColored))
.AddLogging(builder =>
{
builder.SetMinimumLevel(configuration.LogLevel)
Expand Down
49 changes: 49 additions & 0 deletions src/SecTester.Core/Logger/AnsiCodeColor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;

namespace SecTester.Core.Logger;

public class AnsiCodeColor : IEquatable<AnsiCodeColor>
{
private readonly string _color;
private readonly int _hashcode;

public static readonly AnsiCodeColor DefaultForeground = new("\x1B[39m\x1B[22m");
public static readonly AnsiCodeColor Red = new("\x1B[1m\x1B[31m");
public static readonly AnsiCodeColor DarkRed = new("\x1B[31m");
public static readonly AnsiCodeColor Yellow = new("\x1B[1m\x1B[33m");
public static readonly AnsiCodeColor DarkGreen = new("\x1B[32m");
public static readonly AnsiCodeColor White = new("\x1B[1m\x1B[37m");
public static readonly AnsiCodeColor Cyan = new("\x1B[1m\x1B[36m");

public AnsiCodeColor(string color)
{
if (string.IsNullOrEmpty(color))
{
throw new ArgumentNullException(nameof(color));
}

_color = color;
_hashcode = StringComparer.OrdinalIgnoreCase.GetHashCode(_color);
}

public override string ToString() => _color;
public override int GetHashCode() => _hashcode;
public override bool Equals(object obj) => Equals(obj as AnsiCodeColor);

public bool Equals(AnsiCodeColor? other)
{
return other is not null && _color.Equals(other._color, StringComparison.OrdinalIgnoreCase);
}

public static implicit operator string(AnsiCodeColor codeColor) => codeColor.ToString();

public static bool operator ==(AnsiCodeColor? left, AnsiCodeColor? right)
{
return left is null || right is null ? ReferenceEquals(left, right) : left.Equals(right);
}

public static bool operator !=(AnsiCodeColor? left, AnsiCodeColor? right)
{
return !(left == right);
}
}
6 changes: 6 additions & 0 deletions src/SecTester.Core/Logger/AnsiCodeColorizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace SecTester.Core.Logger;

public interface AnsiCodeColorizer
{
string Colorize(AnsiCodeColor ansiCodeColor, string input);
}
30 changes: 16 additions & 14 deletions src/SecTester.Core/Logger/ColoredConsoleFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,35 @@ namespace SecTester.Core.Logger;

public class ColoredConsoleFormatter : DefaultConsoleFormatter
{
const string DefaultForegroundColor = "\x1B[39m\x1B[22m";
private readonly AnsiCodeColorizer _ansiCodeColorizer;


public ColoredConsoleFormatter(IOptionsMonitor<ConsoleFormatterOptions> options, SystemTimeProvider systemTimeProvider)
public ColoredConsoleFormatter(IOptionsMonitor<ConsoleFormatterOptions> options, SystemTimeProvider systemTimeProvider,
AnsiCodeColorizer ansiCodeColorizer)
: base(nameof(ColoredConsoleFormatter), options, systemTimeProvider)
{
_ansiCodeColorizer = ansiCodeColorizer;
}

protected override void WriteHeader<TState>(
in LogEntry<TState> logEntry,
TextWriter textWriter)
{
textWriter.Write(GetForegroundColorAnsiCode(logEntry.LogLevel));
textWriter.Write(FormatHeader(logEntry.LogLevel));
textWriter.Write(DefaultForegroundColor);
textWriter.Write(
_ansiCodeColorizer.Colorize(
GetForegroundColor(logEntry.LogLevel),
FormatHeader(logEntry.LogLevel)));
}

static string GetForegroundColorAnsiCode(LogLevel level) =>
static AnsiCodeColor GetForegroundColor(LogLevel level) =>
level switch
{
LogLevel.Critical => "\x1B[1m\x1B[31m", // ConsoleColor.Red
LogLevel.Error => "\x1B[31m", // ConsoleColor.DarkRed
LogLevel.Warning => "\x1B[1m\x1B[33m", // ConsoleColor.Yellow
LogLevel.Information => "\x1B[32m", // ConsoleColor.DarkGreen
LogLevel.Debug => "\x1B[1m\x1B[37m", // ConsoleColor.White
LogLevel.Trace => "\x1B[1m\x1B[36m", // ConsoleColor.Cyan
LogLevel.Critical => AnsiCodeColor.Red,
LogLevel.Error => AnsiCodeColor.DarkRed,
LogLevel.Warning => AnsiCodeColor.Yellow,
LogLevel.Information => AnsiCodeColor.DarkGreen,
LogLevel.Debug => AnsiCodeColor.White,
LogLevel.Trace => AnsiCodeColor.Cyan,

_ => DefaultForegroundColor
_ => AnsiCodeColor.DefaultForeground
};
}
16 changes: 16 additions & 0 deletions src/SecTester.Core/Logger/DefaultAnsiCodeColorizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace SecTester.Core.Logger;

public class DefaultAnsiCodeColorizer : AnsiCodeColorizer
{
private readonly bool _enabled;

public DefaultAnsiCodeColorizer(bool enabled)
{
_enabled = enabled;
}

public string Colorize(AnsiCodeColor ansiCodeColor, string input)
{
return !_enabled ? input : $"{ansiCodeColor}{input}{AnsiCodeColor.DefaultForeground}";
}
}
61 changes: 61 additions & 0 deletions src/SecTester.Core/Utils/ConsoleUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace SecTester.Core.Utils;

// This is from https://github.com/silkfire/Pastel/blob/master/src/ConsoleExtensions.cs
[ExcludeFromCodeCoverage]
internal static class ConsoleUtils
{
private const string Kernel32 = "kernel32";

private const int StdOutHandle = -11;
private const int StdErrHandle = -12;

private const uint EnableProcessedOutput = 0x0001;
private const uint EnableVirtualTerminalProcessing = 0x0004;
private const uint AnsiColorRequiredMode = EnableProcessedOutput | EnableVirtualTerminalProcessing;

[DllImport(Kernel32)]
private static extern bool GetConsoleMode(SafeFileHandle hConsoleHandle, out uint lpMode);

[DllImport(Kernel32)]
private static extern bool SetConsoleMode(SafeFileHandle hConsoleHandle, uint dwMode);

[DllImport(Kernel32, SetLastError = true)]
private static extern SafeFileHandle GetStdHandle(int nStdHandle);

public static bool IsColored { get; }

static ConsoleUtils()
{
IsColored = Environment.GetEnvironmentVariable("NO_COLOR") == null;
IsColored = IsColored && EnableAnsiColors();
}

private static bool EnableAnsiColors()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return true;
}

return EnableWindowsAnsiColors(StdOutHandle) && EnableWindowsAnsiColors(StdErrHandle);
}

private static bool EnableWindowsAnsiColors(int consoleHandle)
{
var handle = GetStdHandle(consoleHandle);

if (handle.IsInvalid || !GetConsoleMode(handle, out var outConsoleMode))
{
return false;
}

return AnsiColorRequiredMode == (outConsoleMode & AnsiColorRequiredMode) ||
SetConsoleMode(handle, outConsoleMode | AnsiColorRequiredMode);
}
}

7 changes: 5 additions & 2 deletions src/SecTester.Repeater/DefaultRepeaterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SecTester.Core;
using SecTester.Core.Logger;
using SecTester.Core.Utils;
using SecTester.Repeater.Api;
using SecTester.Repeater.Bus;
Expand All @@ -16,14 +17,16 @@ public class DefaultRepeaterFactory : RepeaterFactory
private readonly Repeaters _repeaters;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILoggerFactory _loggerFactory;
private readonly AnsiCodeColorizer _ansiCodeColorizer;

public DefaultRepeaterFactory(IServiceScopeFactory scopeFactory, Repeaters repeaters, RepeaterEventBusFactory eventBusFactory, Configuration configuration, ILoggerFactory loggerFactory)
public DefaultRepeaterFactory(IServiceScopeFactory scopeFactory, Repeaters repeaters, RepeaterEventBusFactory eventBusFactory, Configuration configuration, ILoggerFactory loggerFactory, AnsiCodeColorizer ansiCodeColorizer)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_repeaters = repeaters ?? throw new ArgumentNullException(nameof(repeaters));
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_eventBusFactory = eventBusFactory ?? throw new ArgumentNullException(nameof(eventBusFactory));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_ansiCodeColorizer = ansiCodeColorizer ?? throw new ArgumentNullException(nameof(ansiCodeColorizer));
}

public async Task<IRepeater> CreateRepeater(RepeaterOptions? options = default)
Expand All @@ -37,6 +40,6 @@ public async Task<IRepeater> CreateRepeater(RepeaterOptions? options = default)
var timerProvider = scope.ServiceProvider.GetRequiredService<TimerProvider>();
var version = new Version(_configuration.RepeaterVersion);

return new Repeater(repeaterId, eventBus, version, _loggerFactory.CreateLogger<Repeater>(), timerProvider);
return new Repeater(repeaterId, eventBus, version, _loggerFactory.CreateLogger<Repeater>(), timerProvider, _ansiCodeColorizer);
}
}
15 changes: 9 additions & 6 deletions src/SecTester.Repeater/Repeater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SecTester.Core.Bus;
using SecTester.Core.Exceptions;
using SecTester.Core.Extensions;
using SecTester.Core.Logger;
using SecTester.Core.Utils;
using SecTester.Repeater.Bus;

Expand All @@ -18,14 +19,17 @@ public class Repeater : IRepeater
private readonly ILogger _logger;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly Version _version;
private readonly AnsiCodeColorizer _ansiCodeColorizer;

public Repeater(string repeaterId, EventBus eventBus, Version version, ILogger<Repeater> logger, TimerProvider heartbeat)
public Repeater(string repeaterId, EventBus eventBus, Version version, ILogger<Repeater> logger, TimerProvider heartbeat,
AnsiCodeColorizer ansiCodeColorizer)
{
RepeaterId = repeaterId ?? throw new ArgumentNullException(nameof(repeaterId));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_version = version ?? throw new ArgumentNullException(nameof(version));
_eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
_heartbeat = heartbeat ?? throw new ArgumentNullException(nameof(heartbeat));
_ansiCodeColorizer = ansiCodeColorizer ?? throw new ArgumentNullException(nameof(ansiCodeColorizer));
}

public RunningStatus Status { get; private set; } = RunningStatus.Off;
Expand All @@ -43,6 +47,7 @@ public async ValueTask DisposeAsync()
public async Task Start(CancellationToken cancellationToken = default)
{
using var _ = await _semaphore.LockAsync(cancellationToken).ConfigureAwait(false);

try
{
if (Status != RunningStatus.Off)
Expand Down Expand Up @@ -100,6 +105,7 @@ private async Task Register()
public async Task Stop(CancellationToken cancellationToken = default)
{
using var _ = await _semaphore.LockAsync(cancellationToken).ConfigureAwait(false);

try
{
if (Status != RunningStatus.Running)
Expand Down Expand Up @@ -129,11 +135,8 @@ private void EnsureRegistrationStatus(RegisterRepeaterPayload result)
{
if (new Version(result.Version!).CompareTo(_version) != 0)
{
// TODO: colorize an output in the same manner like sectester-js does
_logger.LogWarning(
"(!) IMPORTANT: A new Repeater version ({Version}) is available, please update SecTester.",
result.Version
);
_logger.LogWarning("{Prefix}: A new Repeater version ({Version}) is available, please update SecTester",
_ansiCodeColorizer.Colorize(AnsiCodeColor.Yellow, "(!) IMPORTANT"), result.Version);
}
}
}
Expand Down
Loading

0 comments on commit e108b6f

Please sign in to comment.