Server 2.10.1: remote management from the MSCO Launcher

- Polls the keymaster every 5 s for commands from the owner's launcher, runs only allowed
  commands (say, kick, ban, save, backup, restart, mods reload, status, players), sends results
  and the player list back
- RemoteManagement in server.json / web panel settings (on by default) to turn it off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:11:14 +02:00
co-authored by Claude Opus 5.5
parent 3a79c928d1
commit bb3efb8cc3
8 changed files with 79 additions and 2 deletions
+35
View File
@@ -317,10 +317,45 @@ public sealed class GameServer
/// <summary>Spielerzahl hat sich geändert: gleich beim Master melden statt erst in 30 s.</summary>
void HeartbeatSoon() => nextHeartbeat = Math.Min(nextHeartbeat, clock.Elapsed.TotalSeconds + 1);
// ---------------------------------------------------------------- Verwalten aus dem Launcher (über den Keymaster)
const double RemotePollSeconds = 5;
double nextRemotePoll;
Task<List<MasterClient.RemoteCommand>> remotePoll;
readonly List<MasterClient.RemoteResult> remoteResults = new();
/// <summary>Befehle, die der Besitzer aus dem Launcher schicken darf (gleiche Liste wie im Keymaster).</summary>
static readonly System.Text.RegularExpressions.Regex RemoteAllowed =
new(@"^(say .{1,150}|kick \d{1,6}( .{0,100})?|ban \d{1,6}( .{0,100})?|save|backup|restart|mods reload|status|players)$");
void RemoteManagementTick(double now)
{
if (offline || !config.RemoteManagement) return;
if (remotePoll != null)
{
if (!remotePoll.IsCompleted) return;
var commands = remotePoll.IsCompletedSuccessfully ? remotePoll.Result : null;
remotePoll = null;
if (commands == null) return; // Master nicht erreichbar: Ergebnisse beim nächsten Mal nochmal schicken
remoteResults.Clear();
foreach (var cmd in commands.Take(10))
{
string line = (cmd.Command ?? "").Trim();
string by = "Launcher (" + (cmd.By ?? "owner") + ")";
string output = RemoteAllowed.IsMatch(line) ? RunCommandCaptured(line, by) : "Not allowed on this server";
remoteResults.Add(new MasterClient.RemoteResult(cmd.Id, output.Length > 2000 ? output[..2000] : output));
}
if (remoteResults.Count > 0) nextRemotePoll = 0; // Ergebnisse gleich abliefern
}
if (now < nextRemotePoll) return;
nextRemotePoll = now + RemotePollSeconds;
var players = clients.Values.Where(c => c.State == ClientState.Joined).Select(c => new MasterClient.RemotePlayer(c.Id, c.Name)).ToList();
remotePoll = master.PollAsync(config.LicenseKey, remoteResults.ToList(), players);
}
void Timers()
{
PullNewGameSave();
double now = clock.Elapsed.TotalSeconds;
RemoteManagementTick(now);
if (now >= nextAutosave)
{
nextAutosave = now + config.AutosaveSeconds;