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:
parent
48db200d3e
commit
b04a953190
6 changed files with 101 additions and 3 deletions
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue