mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
339 lines
11 KiB
C#
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"); });
|
|
}
|
|
}
|
|
}
|