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
+2
View File
@@ -17,6 +17,7 @@ public static class ConfigEditor
publicAddress = config.PublicAddress,
saveSyncMinutes = config.SaveSyncMinutes,
autosaveSeconds = config.AutosaveSeconds,
remoteManagement = config.RemoteManagement,
port = config.Port,
panelEnabled = config.PanelEnabled,
panelPort = config.PanelPort,
@@ -77,6 +78,7 @@ public static class ConfigEditor
config.PublicAddress = Text("publicAddress", config.PublicAddress, 200);
config.SaveSyncMinutes = Number("saveSyncMinutes", config.SaveSyncMinutes, 0, 240);
config.AutosaveSeconds = Number("autosaveSeconds", config.AutosaveSeconds, 10, 3600);
config.RemoteManagement = Flag("remoteManagement", config.RemoteManagement);
int before = changed.Count;
int oldPort = config.Port, oldPanelPort = config.PanelPort;
+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;
+1 -1
View File
@@ -7,7 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MSCO.Server</RootNamespace>
<AssemblyName>MSCO.Server</AssemblyName>
<Version>2.10.0</Version>
<Version>2.10.1</Version>
<Company>MBM</Company>
<Product>MSCO Dedicated Server</Product>
<InvariantGlobalization>true</InvariantGlobalization>
+19
View File
@@ -59,6 +59,25 @@ public sealed class MasterClient
public sealed record HeartbeatResult(int Status, bool? Reachable, bool Listed, string Address, int Port);
record HeartbeatReply(bool? Reachable, bool Listed, string Address, int Port);
public sealed record RemoteCommand(string Id, string Command, string By);
public sealed record RemoteResult(string Id, string Output);
public sealed record RemotePlayer(int Id, string Name);
record PollReply(List<RemoteCommand> Commands);
/// <summary>Befehle aus dem Launcher abholen (und Ergebnisse der letzten Befehle abliefern). null = Master nicht erreichbar.</summary>
public async Task<List<RemoteCommand>> PollAsync(string key, List<RemoteResult> results, List<RemotePlayer> players)
{
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/poll", new { key, results, players }, cts.Token);
if (!response.IsSuccessStatusCode) return null;
var reply = await response.Content.ReadFromJsonAsync<PollReply>(cancellationToken: cts.Token);
return reply?.Commands ?? new List<RemoteCommand>();
}
catch { return null; }
}
/// <summary>Beim Beenden: sofort aus der Serverliste nehmen (sonst erst nach dem Heartbeat-Timeout).</summary>
public async Task UnregisterAsync(string key)
{
+3 -1
View File
@@ -407,7 +407,9 @@ function renderSettings(page) {
number("maxPlayers", "Max players", c.maxPlayers, { min: 1, max: 64 }),
text("publicAddress", "Public address", c.publicAddress, { max: 200, placeholder: "automatic", hint: "IP or domain players connect to (empty = automatic)" }),
toggle("isPublic", "Listed in the server browser", c.isPublic),
toggle("requireSteam", "Steam required", c.requireSteam, "Off = players without Steam can join too"))),
toggle("requireSteam", "Steam required", c.requireSteam, "Off = players without Steam can join too"),
toggle("remoteManagement", "Manage from the MSCO Launcher", c.remoteManagement,
"Owners who linked this server with its license key in the launcher can send commands (say, kick, ban, save, backup, restart, mods reload)"))),
h("div", { class: "section" },
h("div", { class: "section-head" }, h("h2", {}, "Saving")),
h("div", { class: "section-body form-grid wide" },
+5
View File
@@ -16,6 +16,11 @@
"The web panel is turned off - turn it on again with 'panel on' in the server console or PanelEnabled in server.json":
"Das Web-Panel ist ausgeschaltet - wieder an mit 'panel on' in der Server-Konsole oder PanelEnabled in server.json",
// Verwalten aus dem Launcher
"Manage from the MSCO Launcher": "Verwalten aus dem MSCO Launcher",
"Owners who linked this server with its license key in the launcher can send commands (say, kick, ban, save, backup, restart, mods reload)":
"Wer diesen Server im Launcher mit seinem Lizenzschlüssel verknüpft hat, kann Befehle schicken (say, kick, ban, save, backup, restart, mods reload)",
// Mods
"Mods": "Mods",
"Add a mod": "Mod hinzufügen",
+9
View File
@@ -114,6 +114,7 @@ und neu starten.
| `Public` | `false` = nicht in der Liste, nur Direct Connect |
| `RequireSteam` | `true` = nur Spieler mit Steam; `false` = Non-Steam-Server (Spieler werden über den Namen erkannt) |
| `PublicAddress` | öffentliche IP/Domain für die Liste (nur hinter NAT/Proxy nötig) |
| `RemoteManagement` | `true` = der Besitzer kann den Server im MSCO Launcher verwalten (Befehle über den Keymaster) |
| `AutosaveSeconds` | wie oft der Weltzustand gespeichert wird |
| `SaveSyncMinutes` | wie oft (Minuten) sich der Server von einem Spieler im Spiel den **aktuellen Spielstand** hochladen lässt (Standard 10, zusätzlich bei jedem Beitritt; nur wenn sich etwas geändert hat; 0 = nur beim Beitritt) |
| `SteamWebApiKey` | Steam-Anmeldetickets der Spieler bei Steam prüfen – **nötig für Admins** (kostenlos: https://steamcommunity.com/dev/apikey) |
@@ -207,6 +208,14 @@ erreichbar. Ganz aus: `"PanelEnabled": false`.
> (`- "40120:40120/tcp"`). Schon ausgesperrt? `"PanelPort": 0` in `data/server.json` setzen und
> `docker compose restart` – dann ist das Panel wieder auf dem Spiel-Port.
## Verwalten aus dem MSCO Launcher
Im Launcher unter **Meine Server** verknüpft der Besitzer seinen Server einmal mit dem Lizenzschlüssel. Danach sieht
er dort Status und Spieler und kann Befehle schicken: `say`, `kick`, `ban`, `save`, `backup`, `restart`,
`mods reload`, `status`, `players`. Der Server holt sie alle 5 Sekunden beim Keymaster ab und führt nur Befehle aus
dieser Liste aus (im Log mit „Launcher (Konto)“). Abschalten: im Web-Panel unter *Einstellungen* „Verwalten aus dem
MSCO Launcher“ bzw. `"RemoteManagement": false` in `server.json`.
## Mods
MSCO-Mods (.dll, siehe [MODDING.md](https://git.mbm.wtf/MrMaho/MSCO/src/branch/main/MODDING.md)) in `data/mods` – oder
+5
View File
@@ -41,6 +41,11 @@ public sealed class ServerConfig
// ---------------------------------------------------------------- Web-Panel
/// <summary>Web-Panel einschalten.</summary>
public bool PanelEnabled { get; set; } = true;
/// <summary>
/// Verwalten aus dem MSCO Launcher: wer den Server dort mit dem Lizenzschlüssel verknüpft hat, kann über den Keymaster
/// Befehle schicken (say, kick, ban, save, backup, restart, mods reload, status, players). false = aus.
/// </summary>
public bool RemoteManagement { get; set; } = true;
/// <summary>0 = derselbe Port wie der Gameserver (Browser und Spiel werden automatisch unterschieden), sonst eigener Port.</summary>
public int PanelPort { get; set; } = 0;
/// <summary>Nur bei eigenem PanelPort: Adresse, auf der gelauscht wird ("0.0.0.0" = alle, "127.0.0.1" = nur lokal, z.B. hinter einem Proxy).</summary>