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:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user