Skip to content
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
@@ -0,0 +1,75 @@
using System.Runtime.InteropServices;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared.ConsoleOutput;
using Microsoft.Extensions.Logging;
using SwiftlyS2.Shared.Profiler;

namespace SwiftlyS2.Core.ConsoleOutput;

internal delegate void ConsoleOutputListenerCallbackDelegate(nint message);

internal abstract class ConsoleOutputCallbackBase : IDisposable
{
public Guid Guid { get; protected init; }
public IContextedProfilerService Profiler { get; }
public ILoggerFactory LoggerFactory { get; }

protected ConsoleOutputCallbackBase(ILoggerFactory loggerFactory, IContextedProfilerService profiler)
{
LoggerFactory = loggerFactory;
Profiler = profiler;
}

public abstract void Dispose();
}

internal class ConsoleOutputListenerCallback : ConsoleOutputCallbackBase
{
private IConsoleOutputService.ConsoleOutputHandler _handler;
private ConsoleOutputListenerCallbackDelegate _unmanagedCallback;
private nint _unmanagedCallbackPtr;
private ulong _nativeListenerId;
private ILogger<ConsoleOutputListenerCallback> _logger;

public ConsoleOutputListenerCallback(IConsoleOutputService.ConsoleOutputHandler handler, ILoggerFactory loggerFactory, IContextedProfilerService profiler)
: base(loggerFactory, profiler)
{
_logger = LoggerFactory.CreateLogger<ConsoleOutputListenerCallback>();
Guid = Guid.NewGuid();

_handler = handler;

_unmanagedCallback = (messagePtr) =>
{
try
{
var category = "ConsoleOutputListenerCallback";
Profiler.StartRecording(category);
var messageString = Marshal.PtrToStringUTF8(messagePtr)!;
_handler(messageString);
Profiler.StopRecording(category);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to handle console output listener.");
}
};

_unmanagedCallbackPtr = Marshal.GetFunctionPointerForDelegate(_unmanagedCallback);
_nativeListenerId = NativeConsoleOutput.AddConsoleListener(_unmanagedCallbackPtr);
}

public override void Dispose()
{
try
{
NativeConsoleOutput.RemoveConsoleListener(_nativeListenerId);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to unregister console output listener.");
}

GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared.ConsoleOutput;
using SwiftlyS2.Shared.Profiler;

namespace SwiftlyS2.Core.ConsoleOutput;

internal class ConsoleOutputService : IConsoleOutputService, IDisposable
{
private List<ConsoleOutputCallbackBase> _callbacks = new();
private ILogger<ConsoleOutputService> _logger { get; init; }
private ILoggerFactory _loggerFactory { get; init; }
private IContextedProfilerService _profiler { get; init; }

private object _lock = new();

public ConsoleOutputService(ILogger<ConsoleOutputService> logger, ILoggerFactory loggerFactory, IContextedProfilerService profiler)
{
_logger = logger;
_loggerFactory = loggerFactory;
_profiler = profiler;
}

public Guid RegisterConsoleOutputListener(IConsoleOutputService.ConsoleOutputHandler handler)
{
var callback = new ConsoleOutputListenerCallback(handler, _loggerFactory, _profiler);
lock (_lock)
{
_callbacks.Add(callback);
}

return callback.Guid;
}

public void UnregisterConsoleOutputListener(Guid guid)
{
lock (_lock)
{
_callbacks.RemoveAll(callback =>
{
if (callback.Guid == guid)
{
callback.Dispose();
return true;
}
return false;
});
}
}

public bool IsFilterEnabled()
{
return NativeConsoleOutput.IsEnabled();
}

public void ToggleFilter()
{
NativeConsoleOutput.ToggleFilter();
}

public void ReloadFilterConfiguration()
{
NativeConsoleOutput.ReloadFilterConfiguration();
}

public bool NeedsFiltering(string message)
{
return NativeConsoleOutput.NeedsFiltering(message);
}

public string GetCounterText()
{
return NativeConsoleOutput.GetCounterText();
}

public void WriteToServerConsole(string message)
{
NativeEngineHelpers.SendMessageToConsole(message);
}

public void Dispose()
{
lock (_lock)
{
foreach (var callback in _callbacks)
{
callback.Dispose();
}
_callbacks.Clear();
}
GC.SuppressFinalize(this);
}
}
7 changes: 7 additions & 0 deletions managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SwiftlyS2.Core.Commands;
using SwiftlyS2.Core.ConsoleOutput;
using SwiftlyS2.Core.Events;
using SwiftlyS2.Core.GameEvents;
using SwiftlyS2.Core.Misc;
Expand All @@ -10,6 +11,7 @@
using SwiftlyS2.Shared.Events;
using SwiftlyS2.Shared.GameEvents;
using SwiftlyS2.Shared.Commands;
using SwiftlyS2.Shared.ConsoleOutput;
using SwiftlyS2.Shared.NetMessages;
using SwiftlyS2.Shared.Services;
using SwiftlyS2.Core.AttributeParsers;
Expand Down Expand Up @@ -50,6 +52,7 @@ internal class SwiftlyCore : ISwiftlyCore, IDisposable
public PluginConfigurationService Configuration { get; init; }
public ILoggerFactory LoggerFactory { get; init; }
public CommandService CommandService { get; init; }
public ConsoleOutputService ConsoleOutputService { get; init; }
public EntitySystemService EntitySystemService { get; init; }
public ConVarService ConVarService { get; init; }
public GameDataService GameDataService { get; init; }
Expand Down Expand Up @@ -91,6 +94,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata
.AddSingleton<GameEventService>()
.AddSingleton<NetMessageService>()
.AddSingleton<CommandService>()
.AddSingleton<ConsoleOutputService>()
.AddSingleton<EntitySystemService>()
.AddSingleton<ConVarService>()
.AddSingleton<MemoryService>()
Expand All @@ -109,6 +113,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata
.AddSingleton<INetMessageService>(provider => provider.GetRequiredService<NetMessageService>())
.AddSingleton<IPluginConfigurationService>(provider => provider.GetRequiredService<PluginConfigurationService>())
.AddSingleton<ICommandService>(provider => provider.GetRequiredService<CommandService>())
.AddSingleton<IConsoleOutputService>(provider => provider.GetRequiredService<ConsoleOutputService>())
.AddSingleton<IEntitySystemService>(provider => provider.GetRequiredService<EntitySystemService>())
.AddSingleton<IConVarService>(provider => provider.GetRequiredService<ConVarService>())
.AddSingleton<IGameDataService>(provider => provider.GetRequiredService<GameDataService>())
Expand Down Expand Up @@ -140,6 +145,7 @@ public SwiftlyCore(string contextId, string contextBaseDirectory, PluginMetadata
LoggerFactory = _ServiceProvider.GetRequiredService<ILoggerFactory>();
NetMessageService = _ServiceProvider.GetRequiredService<NetMessageService>();
CommandService = _ServiceProvider.GetRequiredService<CommandService>();
ConsoleOutputService = _ServiceProvider.GetRequiredService<ConsoleOutputService>();
EntitySystemService = _ServiceProvider.GetRequiredService<EntitySystemService>();
GameDataService = _ServiceProvider.GetRequiredService<GameDataService>();
PlayerManagerService = _ServiceProvider.GetRequiredService<PlayerManagerService>();
Expand Down Expand Up @@ -179,6 +185,7 @@ public void Dispose()
IGameEventService ISwiftlyCore.GameEvent => GameEventService;
INetMessageService ISwiftlyCore.NetMessage => NetMessageService;
ICommandService ISwiftlyCore.Command => CommandService;
IConsoleOutputService ISwiftlyCore.ConsoleOutput => ConsoleOutputService;
IEntitySystemService ISwiftlyCore.EntitySystem => EntitySystemService;
IConVarService ISwiftlyCore.ConVar => ConVarService;
IGameDataService ISwiftlyCore.GameData => GameDataService;
Expand Down
46 changes: 46 additions & 0 deletions managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ SwiftlyS2 is licensed under the GNU General Public License v3.0 or later.
}
ProfilerCommand(context);
break;
case "confilter":
if (context.IsSentByPlayer)
{
context.Reply("This command can only be executed from the server console.");
return;
}
ConfilterCommand(context);
break;
default:
ShowHelp(context);
break;
Expand All @@ -132,6 +140,7 @@ private static void ShowHelp(ICommandContext context)
table.AddRow("status", "Show the status of the server");
if (!context.IsSentByPlayer)
{
table.AddRow("confilter", "Console Filter Menu");
table.AddRow("plugins", "Plugin Management Menu");
table.AddRow("gc", "Show garbage collection information on managed");
table.AddRow("profiler", "Profiler Menu");
Expand All @@ -140,6 +149,43 @@ private static void ShowHelp(ICommandContext context)
AnsiConsole.Write(table);
}

private void ConfilterCommand(ICommandContext context)
{
var args = context.Args;
if (args.Length == 1)
{
var table = new Table().AddColumn("Command").AddColumn("Description");
table.AddRow("enable", "Enable console filtering");
table.AddRow("disable", "Disable console filtering");
table.AddRow("status", "Show the status of the console filter");
table.AddRow("reload", "Reload console filter configuration");
AnsiConsole.Write(table);
return;
}

switch (args[1])
{
case "enable":
if (!_Core.ConsoleOutput.IsFilterEnabled()) _Core.ConsoleOutput.ToggleFilter();
_Logger.LogInformation("Console filtering has been enabled.");
break;
case "disable":
if (_Core.ConsoleOutput.IsFilterEnabled()) _Core.ConsoleOutput.ToggleFilter();
_Logger.LogInformation("Console filtering has been disabled.");
break;
case "status":
_Logger.LogInformation($"Console filtering is currently {(_Core.ConsoleOutput.IsFilterEnabled() ? "enabled" : "disabled")}.\nBelow are some statistics for the filtering process:\n{_Core.ConsoleOutput.GetCounterText()}");
break;
case "reload":
_Core.ConsoleOutput.ReloadFilterConfiguration();
_Logger.LogInformation("Console filter configuration reloaded.");
break;
default:
_Logger.LogWarning("Unknown command");
break;
}
}

private void ProfilerCommand(ICommandContext context)
{
var args = context.Args;
Expand Down
92 changes: 92 additions & 0 deletions managed/src/SwiftlyS2.Generated/Natives/ConsoleOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#pragma warning disable CS0649
#pragma warning disable CS0169

using System.Buffers;
using System.Text;
using System.Threading;
using SwiftlyS2.Shared.Natives;

namespace SwiftlyS2.Core.Natives;

internal static class NativeConsoleOutput {
private static int _MainThreadID;

private unsafe static delegate* unmanaged<nint, ulong> _AddConsoleListener;

/// <summary>
/// callback should receive: string message
/// </summary>
public unsafe static ulong AddConsoleListener(nint callback) {
var ret = _AddConsoleListener(callback);
return ret;
}

private unsafe static delegate* unmanaged<ulong, void> _RemoveConsoleListener;

public unsafe static void RemoveConsoleListener(ulong listenerId) {
_RemoveConsoleListener(listenerId);
}

private unsafe static delegate* unmanaged<byte> _IsEnabled;

/// <summary>
/// returns whether console filtering is enabled
/// </summary>
public unsafe static bool IsEnabled() {
var ret = _IsEnabled();
return ret == 1;
}

private unsafe static delegate* unmanaged<void> _ToggleFilter;

/// <summary>
/// toggles the console filter on/off
/// </summary>
public unsafe static void ToggleFilter() {
_ToggleFilter();
}

private unsafe static delegate* unmanaged<void> _ReloadFilterConfiguration;

/// <summary>
/// reloads the filter configuration from file
/// </summary>
public unsafe static void ReloadFilterConfiguration() {
_ReloadFilterConfiguration();
}

private unsafe static delegate* unmanaged<byte*, byte> _NeedsFiltering;

/// <summary>
/// checks if a message needs filtering
/// </summary>
public unsafe static bool NeedsFiltering(string text) {
var pool = ArrayPool<byte>.Shared;
var textLength = Encoding.UTF8.GetByteCount(text);
var textBuffer = pool.Rent(textLength + 1);
Encoding.UTF8.GetBytes(text, textBuffer);
textBuffer[textLength] = 0;
fixed (byte* textBufferPtr = textBuffer) {
var ret = _NeedsFiltering(textBufferPtr);
pool.Return(textBuffer);
return ret == 1;
}
}

private unsafe static delegate* unmanaged<byte*, int> _GetCounterText;

/// <summary>
/// gets the counter text showing how many messages were filtered
/// </summary>
public unsafe static string GetCounterText() {
var ret = _GetCounterText(null);
var pool = ArrayPool<byte>.Shared;
var retBuffer = pool.Rent(ret + 1);
fixed (byte* retBufferPtr = retBuffer) {
ret = _GetCounterText(retBufferPtr);
var retString = Encoding.UTF8.GetString(retBufferPtr, ret);
pool.Return(retBuffer);
return retString;
}
}
}
Loading
Loading