Tab Server: Server neu starten mit Bestätigung und Warten auf den neuen Prozess, Bot-Warnung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
slyphax 2026-10-31 00:08:06 +01:00
parent 48db200d3e
commit b04a953190
6 changed files with 101 additions and 3 deletions

View file

@ -32,6 +32,12 @@ im Editor dann Adresse `127.0.0.1`) oder ein Reverse-Proxy mit https (Haken
- **Spieler:** Kick, Sperren (10 min … dauerhaft), Sperrliste mit „Aufheben“. - **Spieler:** Kick, Sperren (10 min … dauerhaft), Sperrliste mit „Aufheben“.
- **Match & Maps:** Match neu starten, Map jetzt laden, Rotation per Häkchen, - **Match & Maps:** Match neu starten, Map jetzt laden, Rotation per Häkchen,
hochgeladene Maps löschen. Alles Zerstörerische fragt vorher nach. hochgeladene Maps löschen. Alles Zerstörerische fragt vorher nach.
- **⏻ Server neu starten:** fragt nach, trennt alle Spieler, zeigt „Server startet neu…“
und verbindet sich wieder, sobald der neue Prozess antwortet (bis 120 s). Der Server
beendet sich dafür mit Exit-Code 75; neu starten muss ihn Docker (Restart-Policy),
systemd oder `tools/start_server.*` (`docs/admin-api.md` im Server-Repo).
- **Bot-Anzahl** geht bis 32, die Grenze kommt vom Server. Reicht die Teamgröße nicht
(2 × Teamgröße = Platz für Bots), zeigt das Speichern eine Warnung mit der nötigen Größe.
- **Server-Log** mit Filter. - **Server-Log** mit Filter.
## Code ## Code
@ -42,4 +48,4 @@ im Editor dann Adresse `127.0.0.1`) oder ein Reverse-Proxy mit https (Haken
`tests/test_server_sektion.gd` (Unit), `tests/run_server_sektion_e2e.sh` `tests/test_server_sektion.gd` (Unit), `tests/run_server_sektion_e2e.sh`
(echter Match-Server + echtes Panel: Verbinden, falsches Token, unerreichbar, (echter Match-Server + echtes Panel: Verbinden, falsches Token, unerreichbar,
Einstellungen, Push gültig/doppelt/ersetzen/lokal ungültig/422, Rotation, Einstellungen, Push gültig/doppelt/ersetzen/lokal ungültig/422, Rotation,
Map laden/löschen, Restart, Kick, Logs). Map laden/löschen, Restart, Kick, Logs, 20 Bots, Server-Neustart).

View file

@ -87,6 +87,10 @@ static func wirkung(effect: String) -> String:
return effect return effect
static func neustart_zeitueberschreitung(seconds: int) -> String:
return "Der Server ist nach %d s nicht zurück. Er kommt nur von selbst wieder, wenn Docker (Restart-Policy), systemd oder tools/start_server.* ihn gestartet haben." % seconds
static func sperrdauer_text(minutes: int) -> String: static func sperrdauer_text(minutes: int) -> String:
if minutes <= 0: if minutes <= 0:
return "dauerhaft" return "dauerhaft"

View file

@ -10,6 +10,8 @@ var client: AdminClient
var current_id := "" var current_id := ""
var last_notice := "" # letzte Meldung (Tests) var last_notice := "" # letzte Meldung (Tests)
var connected := false var connected := false
var restart_wait_s := 120.0 # so lange wartet „Server neu starten“ (Tests verkürzen)
var restart_poll_s := 2.0
var _sel: OptionButton var _sel: OptionButton
var _conn_label: Label var _conn_label: Label
@ -162,6 +164,9 @@ func _build_live() -> void:
_live_box.add_child(_maps_box) _live_box.add_child(_maps_box)
_live_box.add_child(_btn("💾 Rotation speichern", func() -> void: save_rotation())) _live_box.add_child(_btn("💾 Rotation speichern", func() -> void: save_rotation()))
_section("Server")
_live_box.add_child(_btn("⏻ Server neu starten", func() -> void: _ask("Server komplett neu starten? Alle Spieler werden getrennt und die laufende Runde endet.", func() -> void: await restart_server())))
_section("Server-Log") _section("Server-Log")
_log_filter = LineEdit.new() _log_filter = LineEdit.new()
_log_filter.placeholder_text = "Filter (z. B. Admin, Join, FEHLER)" _log_filter.placeholder_text = "Filter (z. B. Admin, Join, FEHLER)"
@ -476,6 +481,8 @@ func save_settings() -> void:
lines.append("⏭ Ab der nächsten Map: " + ", ".join(_labels(r["applies_next_map"]))) lines.append("⏭ Ab der nächsten Map: " + ", ".join(_labels(r["applies_next_map"])))
if not (r["needs_restart"] as Array).is_empty(): if not (r["needs_restart"] as Array).is_empty():
lines.append("🔁 Erst nach Server-Neustart: " + ", ".join(_labels(r["needs_restart"]))) lines.append("🔁 Erst nach Server-Neustart: " + ", ".join(_labels(r["needs_restart"])))
for w in r.get("warnings", []):
lines.append("⚠ " + str(w))
_settings_result.text = "\n".join(lines) _settings_result.text = "\n".join(lines)
last_notice = _settings_result.text last_notice = _settings_result.text
_settings_loaded = r["settings"] _settings_loaded = r["settings"]
@ -555,6 +562,44 @@ func _do_restart() -> void:
await refresh_status() await refresh_status()
## Ganzen Server neu starten und warten, bis er wieder antwortet; dann neu verbinden.
func restart_server() -> bool:
var id := current_id
if await _api("POST", "/restart", {}) == null:
return false
_set_connected(false, "Server startet neu…")
_notify("Server startet neu…")
var res: Dictionary = await _wait_for_new_server(id)
if current_id != id:
return false # inzwischen anderen Server gewählt
if not bool(res["ok"]):
_set_connected(false, "❌ " + str(res["text"]))
_notify(str(res["text"]))
return false
var ok: bool = await connect_selected()
if ok:
_notify("✅ Server wurde neu gestartet.")
return ok
## Der alte Prozess antwortet noch kurz: Erst eine Laufzeit, die kleiner ist als die
## Wartezeit bisher, beweist den neuen Prozess.
func _wait_for_new_server(id: String) -> Dictionary:
var start := Time.get_ticks_msec()
while current_id == id:
var waited := (Time.get_ticks_msec() - start) / 1000.0
if waited >= restart_wait_s:
return {"ok": false, "text": AdminTexte.neustart_zeitueberschreitung(int(restart_wait_s))}
var st: Dictionary = await client.call_api("GET", "/status")
if bool(st["ok"]):
if float(st["body"].get("uptime_s", INF)) <= (Time.get_ticks_msec() - start) / 1000.0:
return {"ok": true, "text": ""}
elif int(st.get("status", 0)) in [401, 429]:
return {"ok": false, "text": AdminTexte.erklaere(st)}
await get_tree().create_timer(restart_poll_s).timeout
return {"ok": false, "text": ""}
func refresh_maps() -> void: func refresh_maps() -> void:
var r: Variant = await _api("GET", "/maps") var r: Variant = await _api("GET", "/maps")
if r == null: if r == null:

View file

@ -39,14 +39,15 @@ state_file="$LOGDIR/admin_state.cfg"
maps_dir="$LOGDIR/maps" maps_dir="$LOGDIR/maps"
max_upload_mb=20 max_upload_mb=20
EOC EOC
MATCHD_ADMIN_TOKEN="$TOKEN" "$GODOT" --headless --path "$MATCH_DIR" -- --config "$LOGDIR/admin.cfg" > "$LOGDIR/server.log" 2>&1 & # Über das Start-Skript: der Neustart-Test braucht jemanden, der den Server wieder startet.
GODOT="$GODOT" MATCHD_ADMIN_TOKEN="$TOKEN" "$MATCH_DIR/tools/start_server.sh" --config "$LOGDIR/admin.cfg" > "$LOGDIR/server.log" 2>&1 &
SPID=$! SPID=$!
for i in $(seq 1 30); do grep -q "API lauscht" "$LOGDIR/server.log" 2>/dev/null && break; sleep 1; done for i in $(seq 1 30); do grep -q "API lauscht" "$LOGDIR/server.log" 2>/dev/null && break; sleep 1; done
sleep 2 sleep 2
SRV_PORT="$APORT" SRV_TOKEN="$TOKEN" SRV_SVMAP="$MATCH_DIR/maps/example_valley.svmap" \ SRV_PORT="$APORT" SRV_TOKEN="$TOKEN" SRV_SVMAP="$MATCH_DIR/maps/example_valley.svmap" \
"$GODOT" --headless --path "$EDITOR_DIR" -s res://tests/server_sektion_e2e.gd 2>&1 | tee "$LOGDIR/editor.log" | grep -E "^ (ok|FAIL)|CHECKS|SCRIPT ERROR" "$GODOT" --headless --path "$EDITOR_DIR" -s res://tests/server_sektion_e2e.gd 2>&1 | tee "$LOGDIR/editor.log" | grep -E "^ (ok|FAIL)|CHECKS|SCRIPT ERROR"
rc=${PIPESTATUS[0]} rc=${PIPESTATUS[0]}
kill "$SPID" 2>/dev/null; wait 2>/dev/null kill "$SPID" 2>/dev/null; pkill -f -- "--config $LOGDIR/admin.cfg" 2>/dev/null; wait 2>/dev/null
grep -q "$TOKEN" "$LOGDIR/editor.log" && { echo "FAIL: Token im Editor-Log"; rc=1; } || echo "PASS: Token nicht im Editor-Log" grep -q "$TOKEN" "$LOGDIR/editor.log" && { echo "FAIL: Token im Editor-Log"; rc=1; } || echo "PASS: Token nicht im Editor-Log"
echo "--- Logs: $LOGDIR ---" echo "--- Logs: $LOGDIR ---"
exit "$rc" exit "$rc"

View file

@ -122,6 +122,36 @@ func _initialize() -> void:
await panel.load_logs() await panel.load_logs()
_check(panel._log_text.text.contains("Admin") and not panel._log_text.text.contains(token), "Logs geladen, Token nicht enthalten") _check(panel._log_text.text.contains("Admin") and not panel._log_text.text.contains(token), "Logs geladen, Token nicht enthalten")
# Bots: Grenze kommt vom Server, 20 Bots brauchen Teamgröße 10
var bot_box: SpinBox = panel._settings_editors["bot_count"]
_check(bot_box.max_value >= 20.0, "Bot-Feld geht bis mindestens 20 (%d)" % int(bot_box.max_value))
(panel._settings_editors["bots_enabled"] as CheckBox).button_pressed = true
bot_box.value = 20
await panel.save_settings()
_check("⚠" in panel.last_notice and "Teamgröße" in panel.last_notice, "20 Bots bei kleiner Teamgröße -> Warnung: " + panel.last_notice.replace("\n", " | "))
(panel._settings_editors["team_size"] as SpinBox).value = 10
await panel.save_settings()
_check("⚠" not in panel.last_notice, "Teamgröße 10 -> keine Warnung")
await panel.refresh_status()
_check("+ 20 Bots" in panel._status_label.text, "Übersicht zeigt 20 Bots: " + panel._status_label.text.split("\n")[2])
# Server-Neustart über das Start-Skript
panel.restart_poll_s = 0.5
var restart_done := [false, false]
var restart_call := func() -> void:
restart_done[1] = await panel.restart_server()
restart_done[0] = true
restart_call.call()
while panel.connected:
await process_frame
_check("startet neu" in panel._conn_label.text, "Status zeigt: " + panel._conn_label.text)
var wait_end := Time.get_ticks_msec() + 60000
while not restart_done[0] and Time.get_ticks_msec() < wait_end:
await process_frame
_check(restart_done[1] and panel.connected and "neu gestartet" in panel.last_notice, "nach Neustart wieder verbunden: " + panel.last_notice)
_check("Läuft seit 0 h 0 min" in panel._status_label.text, "neuer Prozess läuft")
_check(int((panel._settings_editors["bot_count"] as SpinBox).value) == 20, "Einstellungen überleben den Neustart")
DirAccess.remove_absolute(ProjectSettings.globalize_path(panel.store_path)) DirAccess.remove_absolute(ProjectSettings.globalize_path(panel.store_path))
print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()]) print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()])
quit(0 if _failures.is_empty() else 1) quit(0 if _failures.is_empty() else 1)

View file

@ -74,5 +74,17 @@ func _initialize() -> void:
var spec: Dictionary = panel.call("spec") if panel.has_method("spec") else load("res://src/ui/panels/server_panel.gd").spec() var spec: Dictionary = panel.call("spec") if panel.has_method("spec") else load("res://src/ui/panels/server_panel.gd").spec()
_check(spec["titel"] == "Server" and spec["gruppe"] == "rechts", "Registry-spec") _check(spec["titel"] == "Server" and spec["gruppe"] == "rechts", "Registry-spec")
# Neustart: Server kommt nicht zurück -> verständliche Meldung statt ewig warten
var fake_src := GDScript.new()
fake_src.source_code = "extends AdminClient\nfunc call_api(method: String, _path: String, _body: Variant = null, _query: Dictionary = {}, _raw: PackedByteArray = PackedByteArray()) -> Dictionary:\n\tawait get_tree().process_frame\n\tif method == \"POST\":\n\t\treturn {\"ok\": true, \"status\": 202, \"body\": {\"ok\": true}, \"transport_error\": \"\"}\n\treturn _transport_fail(\"weg\")\n"
fake_src.reload()
var fake_client: Node = fake_src.new()
panel.add_child(fake_client)
panel.client = fake_client
panel.restart_wait_s = 0.3
panel.restart_poll_s = 0.1
var ok_restart: bool = await panel.restart_server()
_check(not ok_restart and not panel.connected and "nicht zurück" in panel._conn_label.text, "Zeitüberschreitung: " + panel._conn_label.text)
print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()]) print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()])
quit(0 if _failures.is_empty() else 1) quit(0 if _failures.is_empty() else 1)