From 0be816a4ecbabaec8dcc9f022d4ff19b36759234 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 25 May 2026 20:39:58 +0200 Subject: [PATCH] Evaluation: depth 15 statt 10s, HTTP/1.1 Chunked-Streaming, Abbruch-Support --- server.py | 114 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 18 deletions(-) diff --git a/server.py b/server.py index 20ba60c..64d34a9 100644 --- a/server.py +++ b/server.py @@ -13,6 +13,7 @@ import re import sys import threading import socket +import queue PORT = int(os.environ.get("PORT", 8111)) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -34,6 +35,11 @@ class StockfishEngine: def __init__(self, path): self.path = path self.proc = None + self._output_queue = queue.Queue() + self._reader_thread = None + self._reader_alive = threading.Event() + self._cmd_lock = threading.Lock() + self._searching = False def start(self): if self.proc: @@ -51,25 +57,78 @@ class StockfishEngine: bufsize=1, creationflags=flags, ) + + self._reader_alive.set() + self._reader_thread = threading.Thread(target=self._read_output, daemon=True) + self._reader_thread.start() + self._send("uci") self._read_until("uciok") self._send("isready") self._read_until("readyok") + print(f"[STOCKFISH] Engine gestartet: {self.path}") + def _read_output(self): + while self._reader_alive.is_set(): + try: + line = self.proc.stdout.readline() + except Exception: + break + if not line: + break + self._output_queue.put(line) + def _send(self, cmd): - self.proc.stdin.write(cmd + "\n") - self.proc.stdin.flush() + with self._cmd_lock: + self.proc.stdin.write(cmd + "\n") + self.proc.stdin.flush() def _read_until(self, marker): while True: - line = self.proc.stdout.readline().strip() - if line == marker: + try: + line = self._output_queue.get(timeout=5.0) + except queue.Empty: + return + if line.strip() == marker: + return + + def _drain_queue(self): + """Leert die Queue von allen pending Zeilen.""" + drained = 0 + while True: + try: + self._output_queue.get_nowait() + drained += 1 + except queue.Empty: + break + if drained > 0: + print(f"[STOCKFISH] {drained} alte Zeilen verworfen") + + def _stop_and_wait(self): + """Stoppt laufende Suche und wartet auf bestmove.""" + if not self._searching: + return + self._send("stop") + while True: + try: + line = self._output_queue.get(timeout=3.0).strip() + except queue.Empty: + # Stockfish antwortet nicht auf stop (z.B. weil Suche schon beendet) + self._searching = False + return + if line.startswith("bestmove"): + self._searching = False return def evaluate(self, fen): + # Alte Suche abbrechen und Queue leeren + self._stop_and_wait() + self._drain_queue() + self._send(f"position fen {fen}") - self._send(f"go movetime 10000") + self._send(f"go depth {STOCKFISH_DEPTH}") + self._searching = True score_cp = None score_mate = None @@ -78,7 +137,11 @@ class StockfishEngine: last_depth = 0 while True: - line = self.proc.stdout.readline().strip() + try: + line = self._output_queue.get(timeout=1.0).strip() + except queue.Empty: + continue + if not line: continue @@ -109,6 +172,7 @@ class StockfishEngine: if line.startswith("bestmove"): parts = line.split() bestmove = parts[1] if len(parts) > 1 else None + self._searching = False break yield { @@ -119,6 +183,7 @@ class StockfishEngine: } def stop(self): + self._reader_alive.clear() if self.proc: self.proc.terminate() self.proc = None @@ -128,6 +193,7 @@ _engine = StockfishEngine(STOCKFISH_PATH) class Handler(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" def do_GET(self): if self.path == "/": @@ -181,7 +247,7 @@ class Handler(http.server.BaseHTTPRequestHandler): self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Cache-Control", "no-cache") - self.send_header("Connection", "close") + self.send_header("Transfer-Encoding", "chunked") self.end_headers() self.wfile.flush() @@ -191,28 +257,40 @@ class Handler(http.server.BaseHTTPRequestHandler): except Exception: pass - self.close_connection = True + def _write_chunk(chunk_data): + size = len(chunk_data) + self.wfile.write(f"{size:x}\r\n".encode()) + self.wfile.write(chunk_data) + self.wfile.write(b"\r\n") + self.wfile.flush() + try: with _stockfish_lock: try: _engine.start() except FileNotFoundError: - self.wfile.write( - json.dumps({"error": "Stockfish nicht gefunden"}).encode("utf-8") + b"\n" + _write_chunk( + json.dumps({"error": "Stockfish nicht gefunden"}).encode("utf-8") ) - self.wfile.flush() return for result in _engine.evaluate(fen): - data = json.dumps(result).encode("utf-8") + b"\n" - self.wfile.write(data) - self.wfile.flush() + try: + data = json.dumps(result).encode("utf-8") + b"\n" + _write_chunk(data) + except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError): + # Client hat Verbindung getrennt (neuer Zug angeklickt / Pfeiltasten) + break + # Abschluss-Chunk + _write_chunk(b"") except Exception as e: print(f"[STOCKFISH] Fehler: {e}") - self.wfile.write( - json.dumps({"error": str(e)}).encode("utf-8") + b"\n" - ) - self.wfile.flush() + try: + _write_chunk( + json.dumps({"error": str(e)}).encode("utf-8") + ) + except Exception: + pass else: self._send_json({"error": "Not found"}, 404)