Files
vssm/tests/FakeGameServer/Program.cs
T

206 lines
5.9 KiB
C#

// A stand-in for VintagestoryServer, used by tests. It copies the behavior measured
// on 1.22.7 (see the design doc) and can be told to misbehave through environment
// variables:
// FAKE_MODE Normal | NoReady | FailOnStart | CrashAfterReady | IgnoreStop | IgnoreAll
// FAKE_STARTUP_MS delay before the ready line (default 500)
// FAKE_STOP_MS time the shutdown sequence takes (default 200)
// FAKE_CRASH_AFTER_MS delay between ready and the crash in CrashAfterReady (default 300)
// FAKE_CRASH_EXIT_CODE exit code used when crashing (default 0, like the real game)
// FAKE_STDIN_EOF "stop" makes the server stop when stdin closes (default: ignore it)
//
// Deliberate differences from the real game:
// - --dataPath is required, so a test can never touch a default location.
// - Only strict JSON is accepted by --setconfig, not the relaxed syntax.
// - If a value-taking flag swallows --dataPath, this exits with 2 where the real
// game would fall back to its default path.
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using FakeGameServer;
using static FakeGameServer.FakeOutput;
const int SetConfigSuccessExitCode = 200;
var cli = CommandLine.Parse(args);
if (cli.DataPath is null)
{
await Console.Error.WriteLineAsync("fake: --dataPath is required, refusing to use a default location");
return 2;
}
var fake = FakeSettings.FromEnvironment();
var dataPath = Path.GetFullPath(cli.DataPath);
Directory.CreateDirectory(dataPath);
Log("Server Notification", "Server logger started.");
Log("Server Notification", "Game Version: v0.0.0 (Fake)");
var configPath = Path.Combine(dataPath, "serverconfig.json");
JsonObject config;
if (File.Exists(configPath))
{
config = JsonNode.Parse(File.ReadAllText(configPath))?.AsObject() ?? new JsonObject();
}
else
{
Log("Server Notification", "serverconfig.json not found, creating new one");
config = new JsonObject
{
["ServerName"] = "Vintage Story Server",
["Port"] = 42420,
["MaxClients"] = 16,
};
}
if (cli.SetConfig is not null)
{
JsonObject patch;
try
{
patch = JsonNode.Parse(cli.SetConfig)?.AsObject() ?? throw new JsonException("null value");
}
catch (Exception ex) when (ex is JsonException or InvalidOperationException)
{
// Unverified: whether the real game leaves a config file behind after this failure.
// The fake does not.
var first = cli.SetConfig.TrimStart('-').FirstOrDefault();
return Crash(
dataPath,
fake.CrashExitCode,
"Newtonsoft.Json.JsonReaderException: Unexpected character encountered while " +
$"parsing value: {first}. Path '', line 0, position 0.");
}
foreach (var (key, value) in patch.ToList())
{
config[key] = value?.DeepClone();
Log("Server Notification", $"Ok, value {value} set for {key}.");
}
File.WriteAllText(configPath, config.ToJsonString(JsonOptions.Indented));
return SetConfigSuccessExitCode;
}
File.WriteAllText(configPath, config.ToJsonString(JsonOptions.Indented));
var port = cli.Port ?? config["Port"]?.GetValue<int>() ?? 42420;
using var shutdown = new CancellationTokenSource();
var gate = new object();
string? stopReason = null;
var stopViaConsole = false;
void RequestStop(string reason, bool viaConsole)
{
lock (gate)
{
if (stopReason is not null)
{
return;
}
stopReason = reason;
stopViaConsole = viaConsole;
}
shutdown.Cancel();
}
void OnSignal(PosixSignalContext context)
{
context.Cancel = true;
if (fake.Mode != FakeMode.IgnoreAll)
{
RequestStop("Shutdown via signal", false);
}
}
using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OnSignal);
using var sigint = PosixSignalRegistration.Create(PosixSignal.SIGINT, OnSignal);
_ = Task.Run(async () =>
{
string? line;
while ((line = await Console.In.ReadLineAsync()) is not null)
{
line = line.Trim();
if (line.Length == 0)
{
continue;
}
Log("Server Notification", $"Handling Console Command {line}");
var ignoresStop = fake.Mode is FakeMode.IgnoreStop or FakeMode.IgnoreAll;
if (line == "/stop" && !ignoresStop)
{
RequestStop("Shutdown via server command", true);
}
}
if (fake.StdinEofStopsServer)
{
RequestStop("stdin closed", false);
}
});
Log("Server Event", "Launching server...");
Log("Server Notification", "Entering runphase Configuration");
Log("Server Notification", "Loading savegame");
try
{
await Task.Delay(fake.StartupMs, shutdown.Token);
}
catch (OperationCanceledException)
{
// A stop request during startup is handled below.
}
if (!shutdown.IsCancellationRequested)
{
if (fake.Mode == FakeMode.FailOnStart)
{
return Crash(dataPath, fake.CrashExitCode, $"Fake failure: could not bind port {port}");
}
if (fake.Mode != FakeMode.NoReady)
{
Log("Server Event", $"Dedicated Server now running on Port {port} and all ips!");
}
var wait = fake.Mode == FakeMode.CrashAfterReady ? fake.CrashAfterMs : Timeout.Infinite;
try
{
await Task.Delay(wait, shutdown.Token);
}
catch (OperationCanceledException)
{
// Stop requested.
}
if (!shutdown.IsCancellationRequested)
{
return Crash(dataPath, fake.CrashExitCode, "Fake failure: simulated crash while running");
}
}
string reason;
bool viaConsole;
lock (gate)
{
reason = stopReason ?? "unknown";
viaConsole = stopViaConsole;
}
Log("Server Notification", $"Server stop requested, begin shutdown sequence. Stop reason: {reason}");
Log("Server Notification", "Entering runphase Shutdown");
await Task.Delay(fake.StopMs);
Log("Server Event", "World saved!");
Log("Server Event", "Stopped the server!");
if (viaConsole)
{
Log("Server Notification", "Shut down command executed");
}
return 0;