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,
|
publicAddress = config.PublicAddress,
|
||||||
saveSyncMinutes = config.SaveSyncMinutes,
|
saveSyncMinutes = config.SaveSyncMinutes,
|
||||||
autosaveSeconds = config.AutosaveSeconds,
|
autosaveSeconds = config.AutosaveSeconds,
|
||||||
|
remoteManagement = config.RemoteManagement,
|
||||||
port = config.Port,
|
port = config.Port,
|
||||||
panelEnabled = config.PanelEnabled,
|
panelEnabled = config.PanelEnabled,
|
||||||
panelPort = config.PanelPort,
|
panelPort = config.PanelPort,
|
||||||
@@ -77,6 +78,7 @@ public static class ConfigEditor
|
|||||||
config.PublicAddress = Text("publicAddress", config.PublicAddress, 200);
|
config.PublicAddress = Text("publicAddress", config.PublicAddress, 200);
|
||||||
config.SaveSyncMinutes = Number("saveSyncMinutes", config.SaveSyncMinutes, 0, 240);
|
config.SaveSyncMinutes = Number("saveSyncMinutes", config.SaveSyncMinutes, 0, 240);
|
||||||
config.AutosaveSeconds = Number("autosaveSeconds", config.AutosaveSeconds, 10, 3600);
|
config.AutosaveSeconds = Number("autosaveSeconds", config.AutosaveSeconds, 10, 3600);
|
||||||
|
config.RemoteManagement = Flag("remoteManagement", config.RemoteManagement);
|
||||||
|
|
||||||
int before = changed.Count;
|
int before = changed.Count;
|
||||||
int oldPort = config.Port, oldPanelPort = config.PanelPort;
|
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>
|
/// <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);
|
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()
|
void Timers()
|
||||||
{
|
{
|
||||||
PullNewGameSave();
|
PullNewGameSave();
|
||||||
double now = clock.Elapsed.TotalSeconds;
|
double now = clock.Elapsed.TotalSeconds;
|
||||||
|
RemoteManagementTick(now);
|
||||||
if (now >= nextAutosave)
|
if (now >= nextAutosave)
|
||||||
{
|
{
|
||||||
nextAutosave = now + config.AutosaveSeconds;
|
nextAutosave = now + config.AutosaveSeconds;
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>MSCO.Server</RootNamespace>
|
<RootNamespace>MSCO.Server</RootNamespace>
|
||||||
<AssemblyName>MSCO.Server</AssemblyName>
|
<AssemblyName>MSCO.Server</AssemblyName>
|
||||||
<Version>2.10.0</Version>
|
<Version>2.10.1</Version>
|
||||||
<Company>MBM</Company>
|
<Company>MBM</Company>
|
||||||
<Product>MSCO Dedicated Server</Product>
|
<Product>MSCO Dedicated Server</Product>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<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);
|
public sealed record HeartbeatResult(int Status, bool? Reachable, bool Listed, string Address, int Port);
|
||||||
record HeartbeatReply(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>
|
/// <summary>Beim Beenden: sofort aus der Serverliste nehmen (sonst erst nach dem Heartbeat-Timeout).</summary>
|
||||||
public async Task UnregisterAsync(string key)
|
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 }),
|
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)" }),
|
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("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" },
|
||||||
h("div", { class: "section-head" }, h("h2", {}, "Saving")),
|
h("div", { class: "section-head" }, h("h2", {}, "Saving")),
|
||||||
h("div", { class: "section-body form-grid wide" },
|
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":
|
"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",
|
"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": "Mods",
|
"Mods": "Mods",
|
||||||
"Add a mod": "Mod hinzufügen",
|
"Add a mod": "Mod hinzufügen",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ und neu starten.
|
|||||||
| `Public` | `false` = nicht in der Liste, nur Direct Connect |
|
| `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) |
|
| `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) |
|
| `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 |
|
| `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) |
|
| `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) |
|
| `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
|
> (`- "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.
|
> `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
|
## Mods
|
||||||
|
|
||||||
MSCO-Mods (.dll, siehe [MODDING.md](https://git.mbm.wtf/MrMaho/MSCO/src/branch/main/MODDING.md)) in `data/mods` – oder
|
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
|
// ---------------------------------------------------------------- Web-Panel
|
||||||
/// <summary>Web-Panel einschalten.</summary>
|
/// <summary>Web-Panel einschalten.</summary>
|
||||||
public bool PanelEnabled { get; set; } = true;
|
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>
|
/// <summary>0 = derselbe Port wie der Gameserver (Browser und Spiel werden automatisch unterschieden), sonst eigener Port.</summary>
|
||||||
public int PanelPort { get; set; } = 0;
|
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>
|
/// <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