- CaptureServer : serveur HTTP daemon sur l'agent Windows - Capture fraîche mss en ~94ms à chaque requête - Plus de lecture de vieux heartbeats sur disque - Fallback capture locale si agent indisponible - Firewall Windows port 5006 configuré Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
4.0 KiB
Python
124 lines
4.0 KiB
Python
"""
|
|
Mini serveur HTTP sur l'agent Windows pour les captures d'ecran a la demande.
|
|
|
|
Ecoute sur le port 5006 (configurable via RPA_CAPTURE_PORT).
|
|
Endpoints :
|
|
GET /capture -> screenshot frais en base64 (JPEG)
|
|
GET /health -> {"status": "ok"}
|
|
"""
|
|
import threading
|
|
import logging
|
|
import json
|
|
import base64
|
|
import io
|
|
import os
|
|
import time
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006"))
|
|
|
|
|
|
class CaptureHandler(BaseHTTPRequestHandler):
|
|
"""Retourne un screenshot frais a chaque requete GET /capture."""
|
|
|
|
def do_GET(self):
|
|
if self.path == "/capture":
|
|
self._handle_capture()
|
|
elif self.path == "/health":
|
|
self._send_json(200, {"status": "ok"})
|
|
else:
|
|
self._send_json(404, {"error": "not found"})
|
|
|
|
def do_OPTIONS(self):
|
|
"""Gestion CORS preflight."""
|
|
self.send_response(200)
|
|
self._cors_headers()
|
|
self.send_header("Content-Length", "0")
|
|
self.end_headers()
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def _handle_capture(self):
|
|
"""Capture l'ecran principal et le renvoie en base64 JPEG."""
|
|
t0 = time.perf_counter()
|
|
try:
|
|
import mss
|
|
from PIL import Image
|
|
|
|
with mss.mss() as sct:
|
|
monitor = sct.monitors[1] # ecran principal
|
|
raw = sct.grab(monitor)
|
|
|
|
img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX")
|
|
|
|
buf = io.BytesIO()
|
|
img.save(buf, format="JPEG", quality=80)
|
|
img_b64 = base64.b64encode(buf.getvalue()).decode()
|
|
|
|
elapsed_ms = (time.perf_counter() - t0) * 1000
|
|
logger.info(f"Capture {img.width}x{img.height} en {elapsed_ms:.0f}ms")
|
|
|
|
self._send_json(200, {
|
|
"image": img_b64,
|
|
"width": img.width,
|
|
"height": img.height,
|
|
"format": "jpeg",
|
|
"source": "windows_live",
|
|
"capture_ms": round(elapsed_ms),
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur capture : {e}")
|
|
self._send_json(500, {"error": str(e)})
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def _send_json(self, code: int, data: dict):
|
|
body = json.dumps(data).encode()
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", "application/json")
|
|
self._cors_headers()
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def _cors_headers(self):
|
|
self.send_header("Access-Control-Allow-Origin", "*")
|
|
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
|
|
def log_message(self, format, *args):
|
|
"""Supprime les logs HTTP par defaut (trop verbeux)."""
|
|
pass
|
|
|
|
|
|
class CaptureServer:
|
|
"""Serveur de capture d'ecran en temps reel (thread daemon)."""
|
|
|
|
def __init__(self, port: int = CAPTURE_PORT):
|
|
self._port = port
|
|
self._server: HTTPServer | None = None
|
|
self._thread: threading.Thread | None = None
|
|
|
|
def start(self):
|
|
"""Demarre le serveur dans un thread daemon."""
|
|
try:
|
|
self._server = HTTPServer(("0.0.0.0", self._port), CaptureHandler)
|
|
self._thread = threading.Thread(
|
|
target=self._server.serve_forever, daemon=True
|
|
)
|
|
self._thread.start()
|
|
logger.info(f"Capture server demarre sur le port {self._port}")
|
|
print(f"[CAPTURE] Serveur de capture demarre sur le port {self._port}")
|
|
except Exception as e:
|
|
logger.error(f"Impossible de demarrer le capture server : {e}")
|
|
print(f"[CAPTURE] ERREUR demarrage : {e}")
|
|
|
|
def stop(self):
|
|
"""Arrete le serveur proprement."""
|
|
if self._server:
|
|
self._server.shutdown()
|
|
logger.info("Capture server arrete")
|