206 lines
5.9 KiB
C#
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;
|