Writing some tests, namely the fake game server
This commit is contained in:
1 parent
4f168a6061
commit
30a6fac573
3 files changed
+321
-1
No files matched your search
@@ -1 +1,205 @@
|
||||
Console.WriteLine("Hello, World!");
|
||||
// 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;
|
||||
Reference in new issue
Block a user