diff --git a/docs/server-sektion.md b/docs/server-sektion.md index fb15912..5f40c5a 100644 --- a/docs/server-sektion.md +++ b/docs/server-sektion.md @@ -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“. - **Match & Maps:** Match neu starten, Map jetzt laden, Rotation per Häkchen, 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. ## 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` (echter Match-Server + echtes Panel: Verbinden, falsches Token, unerreichbar, 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). diff --git a/src/server/admin_texte.gd b/src/server/admin_texte.gd index 6f34e1f..c7adf5b 100644 --- a/src/server/admin_texte.gd +++ b/src/server/admin_texte.gd @@ -87,6 +87,10 @@ static func wirkung(effect: String) -> String: 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: if minutes <= 0: return "dauerhaft" diff --git a/src/ui/panels/server_panel.gd b/src/ui/panels/server_panel.gd index 8275141..588b7b4 100644 --- a/src/ui/panels/server_panel.gd +++ b/src/ui/panels/server_panel.gd @@ -10,6 +10,8 @@ var client: AdminClient var current_id := "" var last_notice := "" # letzte Meldung (Tests) 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 _conn_label: Label @@ -162,6 +164,9 @@ func _build_live() -> void: _live_box.add_child(_maps_box) _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") _log_filter = LineEdit.new() _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"]))) if not (r["needs_restart"] as Array).is_empty(): 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) last_notice = _settings_result.text _settings_loaded = r["settings"] @@ -555,6 +562,44 @@ func _do_restart() -> void: 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: var r: Variant = await _api("GET", "/maps") if r == null: diff --git a/tests/run_server_sektion_e2e.sh b/tests/run_server_sektion_e2e.sh index ce892af..5afe43f 100755 --- a/tests/run_server_sektion_e2e.sh +++ b/tests/run_server_sektion_e2e.sh @@ -39,14 +39,15 @@ state_file="$LOGDIR/admin_state.cfg" maps_dir="$LOGDIR/maps" max_upload_mb=20 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=$! for i in $(seq 1 30); do grep -q "API lauscht" "$LOGDIR/server.log" 2>/dev/null && break; sleep 1; done sleep 2 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" 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" echo "--- Logs: $LOGDIR ---" exit "$rc" diff --git a/tests/server_sektion_e2e.gd b/tests/server_sektion_e2e.gd index 02599c4..12edf14 100644 --- a/tests/server_sektion_e2e.gd +++ b/tests/server_sektion_e2e.gd @@ -122,6 +122,36 @@ func _initialize() -> void: await panel.load_logs() _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)) print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()]) quit(0 if _failures.is_empty() else 1) diff --git a/tests/test_server_sektion.gd b/tests/test_server_sektion.gd index da84495..5f84e29 100644 --- a/tests/test_server_sektion.gd +++ b/tests/test_server_sektion.gd @@ -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() _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()]) quit(0 if _failures.is_empty() else 1)