Skip to content

Commit

Permalink
Added capture of server console input for hotkeys
Browse files Browse the repository at this point in the history
  • Loading branch information
Measurity committed Oct 4, 2023
1 parent 91748dc commit 492993d
Showing 1 changed file with 144 additions and 62 deletions.
206 changes: 144 additions & 62 deletions NitroxServer-Subnautica/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Net.NetworkInformation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NitroxModel.Core;
Expand All @@ -27,9 +27,6 @@ public class Program
private static readonly Dictionary<string, Assembly> resolvedAssemblyCache = new();
private static Lazy<string> gameInstallDir;

// Prevents Garbage Collection freeing this callback's memory. Causing an exception to occur for this handle.
private static readonly ConsoleEventDelegate consoleCtrlCheckDelegate = ConsoleEventCallback;

private static async Task Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
Expand All @@ -53,13 +50,29 @@ private static async Task StartServer(string[] args)
AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;

ConfigureCultureInfo();
Console.TreatControlCAsInput = true;
Log.Info($"Starting NitroxServer {NitroxEnvironment.ReleasePhase} v{NitroxEnvironment.Version} for Subnautica");

AppMutex.Hold(() => { Log.Info("Waiting on other Nitrox servers to initialize before starting.."); }, 120000);
Server server;
Task listenForCommands;
Task handleConsoleInput;
CancellationTokenSource cancellationToken = new();
ConsoleCommandProcessor cmdProcessor = null;
try
{
handleConsoleInput = HandleConsoleInput(submit =>
{
try
{
cmdProcessor ??= NitroxServiceLocator.LocateService<ConsoleCommandProcessor>();
}
catch (Exception)
{
// ignored
}
cmdProcessor?.ProcessCommand(submit, Optional.Empty, Perms.CONSOLE);
}, cancellationToken);
AppMutex.Hold(() => Log.Info("Waiting on other Nitrox servers to initialize before starting.."), 120000);

Stopwatch watch = Stopwatch.StartNew();

// Allow game path to be given as command argument
Expand All @@ -85,10 +98,7 @@ private static async Task StartServer(string[] args)
server = NitroxServiceLocator.LocateService<Server>();

await WaitForAvailablePortAsync(server.Port);
CatchExitEvent();
listenForCommands = ListenForCommandsAsync(server);

CancellationTokenSource cancellationToken = new();
if (!server.Start(cancellationToken) && !cancellationToken.IsCancellationRequested)
{
throw new Exception("Unable to start server.");
Expand All @@ -110,20 +120,138 @@ private static async Task StartServer(string[] args)
AppMutex.Release();
}

await listenForCommands;
await handleConsoleInput;

Console.Write($"{Environment.NewLine}Server is closing..");
}

private static async Task ListenForCommandsAsync(Server server)
/// <summary>
/// Handles per-key input of the console and passes input submit to <see cref="ConsoleCommandProcessor"/>.
/// </summary>
static async Task HandleConsoleInput(Action<string> submitHandler, CancellationTokenSource cancellation = default)
{
while (!server.IsRunning)
StringBuilder inputLineBuilder = new();

void ClearInputLine()
{
inputLineBuilder.Clear();
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r");
}

void RedrawInput(Range redrawRange = default)
{
await Task.Delay(100);
int lastPosition = Console.CursorLeft;
if (redrawRange.Start.Value == 0 && redrawRange.End.Value == 0)
{
// Redraw entire line
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r{inputLineBuilder}");
}
else
{
// Redraw part of line
string changedInputSegment = inputLineBuilder.ToString(redrawRange.Start.Value, redrawRange.End.Value);
Console.CursorVisible = false;
Console.Write($"{changedInputSegment}{new string(' ', inputLineBuilder.Length - changedInputSegment.Length - Console.CursorLeft + 1)}");
Console.CursorVisible = true;
}
Console.CursorLeft = lastPosition;
}

ConsoleCommandProcessor cmdProcessor = NitroxServiceLocator.LocateService<ConsoleCommandProcessor>();
while (server.IsRunning)
while (!cancellation?.IsCancellationRequested ?? false)
{
cmdProcessor.ProcessCommand(Console.ReadLine(), Optional.Empty, Perms.CONSOLE);
if (!Console.KeyAvailable)
{
await Task.Delay(10, cancellation.Token);
continue;
}
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
// Handle (ctrl) hotkeys
if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0)
{
switch (keyInfo.Key)
{
case ConsoleKey.C:
if (inputLineBuilder.Length > 0)
{
ClearInputLine();
continue;
}

cancellation.Cancel();
return;
case ConsoleKey.D:
cancellation.Cancel();
return;
default:
// Unhandled modifier key
continue;
}
}

if (keyInfo.Modifiers == 0)
{
switch (keyInfo.Key)
{
case ConsoleKey.LeftArrow when Console.CursorLeft > 0:
Console.CursorLeft--;
continue;
case ConsoleKey.RightArrow when Console.CursorLeft < inputLineBuilder.Length:
Console.CursorLeft++;
continue;
case ConsoleKey.Backspace:
if (inputLineBuilder.Length > 0 && Console.CursorLeft > 0)
{
inputLineBuilder.Remove(Console.CursorLeft - 1, 1);
Console.CursorLeft--;
Console.Write(' ');
Console.CursorLeft--;
RedrawInput();
}
continue;
case ConsoleKey.Delete:
if (inputLineBuilder.Length > 0 && Console.CursorLeft < inputLineBuilder.Length)
{
inputLineBuilder.Remove(Console.CursorLeft, 1);
RedrawInput(new Range(Console.CursorLeft, inputLineBuilder.Length - Console.CursorLeft));
}
continue;
case ConsoleKey.Home:
Console.CursorLeft = 0;
continue;
case ConsoleKey.End:
Console.CursorLeft = inputLineBuilder.Length;
continue;
case ConsoleKey.Escape:
ClearInputLine();
continue;
}
}
// Handle input submit to submit handler
if (keyInfo.Key == ConsoleKey.Enter)
{
if (inputLineBuilder.Length > 0)
{
submitHandler?.Invoke(inputLineBuilder.ToString());
inputLineBuilder.Clear();
Console.WriteLine();
}
continue;
}

// If unhandled key, append as input.
if (keyInfo.KeyChar != 0)
{
Console.Write(keyInfo.KeyChar);
if (Console.CursorLeft - 1 < inputLineBuilder.Length)
{
inputLineBuilder.Remove(Console.CursorLeft - 1, 1);
inputLineBuilder.Insert(Console.CursorLeft - 1, keyInfo.KeyChar);
}
else
{
inputLineBuilder.Append(keyInfo.KeyChar);
}
}
}
}

Expand Down Expand Up @@ -252,50 +380,4 @@ private static void ConfigureCultureInfo()
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
}

private static void CatchExitEvent()
{
// Catch Exit Event
PlatformID platid = Environment.OSVersion.Platform;

// using *nix signal system to catch Ctrl+C
if (platid == PlatformID.Unix || platid == PlatformID.MacOSX || platid == PlatformID.Win32NT || (int)platid == 128) // mono = 128
{
Console.CancelKeyPress += OnCtrlCPressed;
}

// better catch using WinAPI. This will handled process kill
if (platid == PlatformID.Win32NT)
{
SetConsoleCtrlHandler(consoleCtrlCheckDelegate, true);
}
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate callback, bool add);

private static bool ConsoleEventCallback(int eventType)
{
if (eventType >= 2) // close, logoff, or shutdown
{
StopServer();
}

return false;
}

private static void OnCtrlCPressed(object sender, ConsoleCancelEventArgs e)
{
e.Cancel = true; // Prevents process from terminating
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r"); // Clears current line
}

private static void StopServer()
{
Log.Info("Exiting ...");
Server.Instance.Stop();
}

// See: https://docs.microsoft.com/en-us/windows/console/setconsolectrlhandler
private delegate bool ConsoleEventDelegate(int eventType);
}

0 comments on commit 492993d

Please sign in to comment.