Files
RobustToolbox/Robust.Server/Console/SystemConsoleManager.cs
qrtDaniil 14d3699ae2 Fix for server consoles without width and length (#5507)
* Update SystemConsoleManager.cs

* Update SystemConsoleManager.cs
2024-11-01 23:39:30 +01:00

339 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Con = System.Console;
namespace Robust.Server.Console
{
internal sealed class SystemConsoleManager : ISystemConsoleManager, IPostInjectInit, IDisposable
{
[Dependency] private readonly IServerConsoleHost _conShell = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IGameTiming _time = default!;
//
// Command entry stuff.
//
private readonly Dictionary<int, string> commandHistory = new();
private string currentBuffer = "";
private int historyIndex;
private int internalCursor;
private List<string> tabCompleteList = new();
private int tabCompleteIndex;
private ConsoleKey lastKeyPressed = ConsoleKey.NoName;
//
// Title update stuff.
//
// This is ridiculously expensive to fetch for some reason.
// I'm gonna just assume that this can't change during the lifetime of the process. I hope.
// I want this ridiculous 0.1% CPU usage off my profiler.
private readonly bool _userInteractive = Environment.UserInteractive && !System.Console.IsInputRedirected;
private TimeSpan _lastTitleUpdate;
private long _lastReceivedBytes;
private long _lastSentBytes;
private bool _hasCancelled;
public void Dispose()
{
if (_rdLine != null)
{
_rdLine.Value.cts.Cancel();
_rdLine.Value.cts.Dispose();
try
{
_rdLine.Value.task.Dispose();
}
catch
{
// Don't care LOL
}
}
if (Environment.UserInteractive)
{
Con.CancelKeyPress -= CancelKeyHandler;
}
}
public void PostInject()
{
if (Environment.UserInteractive)
{
Con.CancelKeyPress += CancelKeyHandler;
}
}
public void UpdateTick()
{
UpdateTitle();
}
/// <summary>
/// Updates the console window title with performance statistics.
/// </summary>
private void UpdateTitle()
{
if (!_userInteractive)
return;
// every 1 second update stats in the console window title
if ((_time.RealTime - _lastTitleUpdate).TotalSeconds < 1.0)
return;
var netStats = UpdateBps();
var privateSize = Process.GetCurrentProcess().GetPrivateMemorySize64NotSlowHolyFuckingShitMicrosoft();
System.Console.Title = string.Format("FPS: {0:N2} SD: {1:N2}ms | Net: ({2}) | Memory: {3:N0} KiB",
Math.Round(_time.FramesPerSecondAvg, 2),
_time.RealFrameTimeStdDev.TotalMilliseconds,
netStats,
privateSize >> 10);
_lastTitleUpdate = _time.RealTime;
}
private string UpdateBps()
{
var stats = _netManager.Statistics;
var bps =
$"Send: {(stats.SentBytes - _lastSentBytes) >> 10:N0} KiB/s, Recv: {(stats.ReceivedBytes - _lastReceivedBytes) >> 10:N0} KiB/s";
_lastSentBytes = stats.SentBytes;
_lastReceivedBytes = stats.ReceivedBytes;
return bps;
}
private (Task task, CancellationTokenSource cts, Channel<string> chan)? _rdLine = null;
public void UpdateInput()
{
if (_userInteractive)
{
HandleKeyboard();
return;
}
if (_rdLine.HasValue) // Already running, check the channel.
{
if (_rdLine.Value.chan.Reader.TryRead(out var cmd))
_conShell.ExecuteCommand(cmd);
return;
}
// Set up the new thread & thread accessories
var rlc = new CancellationTokenSource();
var chan = Channel.CreateBounded<string>(new BoundedChannelOptions(capacity: 32)
{
FullMode=BoundedChannelFullMode.Wait,
SingleReader=true,
}
);
_rdLine = (
task: Task.Run(
async () =>
{
while (!rlc.IsCancellationRequested)
{
var str = await Con.In
.ReadLineAsync()
.WaitAsync(TimeSpan.FromSeconds(2.0), rlc.Token);
await chan.Writer.WriteAsync(str!, rlc.Token);
}
},
rlc.Token
),
cts: rlc,
chan: chan
);
}
public void HandleKeyboard()
{
// Process keyboard input
while (Con.KeyAvailable)
{
ConsoleKeyInfo key = Con.ReadKey(true);
if (Con.WindowWidth > 0)
Con.SetCursorPosition(0, Con.CursorTop);
if (!Char.IsControl(key.KeyChar))
{
currentBuffer = currentBuffer.Insert(internalCursor++, key.KeyChar.ToString());
DrawCommandLine();
}
else
{
switch (key.Key)
{
case ConsoleKey.Enter:
if (currentBuffer.Length == 0)
break;
Con.WriteLine("> " + currentBuffer);
commandHistory.Add(commandHistory.Count, currentBuffer);
historyIndex = commandHistory.Count;
_conShell.ExecuteCommand(currentBuffer);
currentBuffer = "";
internalCursor = 0;
break;
case ConsoleKey.Backspace:
if (currentBuffer.Length > 0 && internalCursor > 0)
{
currentBuffer = currentBuffer.Remove(internalCursor - 1, 1);
internalCursor--;
}
break;
case ConsoleKey.Delete:
if (currentBuffer.Length > 0 && internalCursor < currentBuffer.Length)
{
currentBuffer = currentBuffer.Remove(internalCursor, 1);
}
break;
case ConsoleKey.UpArrow:
if (historyIndex > 0)
historyIndex--;
if (commandHistory.ContainsKey(historyIndex))
currentBuffer = commandHistory[historyIndex];
else
currentBuffer = "";
internalCursor = currentBuffer.Length;
break;
case ConsoleKey.DownArrow:
if (historyIndex < commandHistory.Count)
historyIndex++;
if (commandHistory.ContainsKey(historyIndex))
currentBuffer = commandHistory[historyIndex];
else
currentBuffer = "";
internalCursor = currentBuffer.Length;
break;
case ConsoleKey.Escape:
historyIndex = commandHistory.Count;
currentBuffer = "";
internalCursor = 0;
break;
case ConsoleKey.LeftArrow:
if (internalCursor > 0)
internalCursor--;
break;
case ConsoleKey.RightArrow:
if (internalCursor < currentBuffer.Length)
internalCursor++;
break;
case ConsoleKey.Tab:
if (lastKeyPressed != ConsoleKey.Tab)
{
tabCompleteList.Clear();
}
string tabCompleteResult = TabComplete();
if (tabCompleteResult != String.Empty)
{
currentBuffer = tabCompleteResult;
internalCursor = currentBuffer.Length;
}
break;
}
lastKeyPressed = key.Key;
DrawCommandLine();
}
}
}
public void Print(string text)
{
Con.Write(text);
}
public void DrawCommandLine()
{
if (Con.WindowWidth <= 0) return;
ClearCurrentLine();
Con.SetCursorPosition(0, Con.CursorTop);
Con.Write("> " + currentBuffer);
Con.SetCursorPosition(internalCursor + 2, Con.CursorTop); //+2 is for the "> " at the beginning of the line
}
private static void ClearCurrentLine()
{
var currentLineCursor = Con.CursorTop;
Con.SetCursorPosition(0, Con.CursorTop);
Con.Write(new string(' ', Con.WindowWidth - 1));
Con.SetCursorPosition(0, currentLineCursor);
}
private string TabComplete()
{
if (currentBuffer.Trim() == String.Empty)
{
return String.Empty;
}
if (tabCompleteList.Count == 0)
{
tabCompleteList = _conShell.AvailableCommands.Keys.Where(key => key.StartsWith(currentBuffer))
.ToList();
if (tabCompleteList.Count == 0)
{
return String.Empty;
}
}
if (tabCompleteIndex + 1 > tabCompleteList.Count)
{
tabCompleteIndex = 0;
}
string result = tabCompleteList[tabCompleteIndex];
tabCompleteIndex++;
return result;
}
private void CancelKeyHandler(object? sender, ConsoleCancelEventArgs args)
{
if (_hasCancelled)
{
Con.WriteLine("Double CancelKey, terminating process.");
return;
}
// Handle process exiting ourselves.
args.Cancel = true;
_hasCancelled = true;
_taskManager.RunOnMainThread(() => { _baseServer.Shutdown("CancelKey"); });
}
}
}