Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): enable ansi colors console mode on windows #132

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}";
}
}
65 changes: 65 additions & 0 deletions src/SecTester.Core/Utils/ConsoleUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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;
}

if ((outConsoleMode & AnsiColorRequiredMode) == AnsiColorRequiredMode)
{
return true;
}

return 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);
}
}
17 changes: 10 additions & 7 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);
ostridm marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -147,7 +150,7 @@ private void HandleRegisterError(RepeaterRegisteringError error)
RepeaterRegisteringError.Busy => new SecTesterException(
$"Access Refused: There is an already running Repeater with ID {RepeaterId}"),
RepeaterRegisteringError.RequiresToBeUpdated => new SecTesterException(
"(!) CRITICAL: The current running version is no longer supported, please update SecTester."),
$"{_ansiCodeColorizer.Colorize(AnsiCodeColor.Red, "(!) CRITICAL")}: The current running version is no longer supported, please update SecTester."),
_ => new ArgumentOutOfRangeException(nameof(error), error, "Something went wrong. Unknown error.")
};
}
Expand Down
Loading