362e089be0
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
"""Length-prefixed MessagePack-Framing (ADR-0003).
|
|
|
|
4-Byte-Big-Endian-Länge, danach MessagePack-Payload. Maximale Payloadgröße
|
|
schützt vor unkontrollierten Queues/Resourcenerschöpfung (§6.2, §33).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import socket
|
|
import struct
|
|
|
|
import msgpack
|
|
|
|
MAX_PAYLOAD_SIZE = 16 * 1024 * 1024 # 16 MiB Obergrenze je Nachricht
|
|
_LENGTH = struct.Struct(">I")
|
|
|
|
|
|
def encode_frame(payload: dict) -> bytes:
|
|
"""Serialisiert ein dict zu length-prefixed MessagePack."""
|
|
body = msgpack.packb(payload, use_bin_type=True)
|
|
if len(body) > MAX_PAYLOAD_SIZE:
|
|
raise ValueError(f"payload too large: {len(body)} > {MAX_PAYLOAD_SIZE}")
|
|
return _LENGTH.pack(len(body)) + body
|
|
|
|
|
|
def decode_frame(frame: bytes) -> dict:
|
|
"""Dekodiert einen vollständigen Frame (Länge + Body)."""
|
|
if len(frame) < _LENGTH.size:
|
|
raise ValueError("frame too short")
|
|
(length,) = _LENGTH.unpack_from(frame, 0)
|
|
if length > MAX_PAYLOAD_SIZE:
|
|
raise ValueError(f"declared length {length} exceeds limit")
|
|
body = frame[_LENGTH.size : _LENGTH.size + length]
|
|
if len(body) != length:
|
|
raise ValueError(f"truncated frame: expected {length}, got {len(body)}")
|
|
return msgpack.unpackb(body, raw=False)
|
|
|
|
|
|
def read_frame(sock: socket.socket) -> dict:
|
|
"""Liest einen Frame von einem verbundenen Socket."""
|
|
header = _recv_exact(sock, _LENGTH.size)
|
|
(length,) = _LENGTH.unpack(header)
|
|
if length > MAX_PAYLOAD_SIZE:
|
|
raise ValueError(f"declared length {length} exceeds limit")
|
|
body = _recv_exact(sock, length)
|
|
return msgpack.unpackb(body, raw=False)
|
|
|
|
|
|
async def read_frame_async(reader: asyncio.StreamReader) -> dict:
|
|
"""Liest einen Frame von einem asyncio-StreamReader (IPC-Server/Client)."""
|
|
header = await reader.readexactly(_LENGTH.size)
|
|
(length,) = _LENGTH.unpack(header)
|
|
if length > MAX_PAYLOAD_SIZE:
|
|
raise ValueError(f"declared length {length} exceeds limit")
|
|
body = await reader.readexactly(length)
|
|
return msgpack.unpackb(body, raw=False)
|
|
|
|
|
|
def write_frame(sock: socket.socket, payload: dict) -> None:
|
|
"""Schreibt einen Frame auf einen verbundenen Socket."""
|
|
sock.sendall(encode_frame(payload))
|
|
|
|
|
|
def _recv_exact(sock: socket.socket, count: int) -> bytes:
|
|
buf = bytearray()
|
|
while len(buf) < count:
|
|
chunk = sock.recv(count - len(buf))
|
|
if not chunk:
|
|
raise ConnectionError("socket closed mid-frame")
|
|
buf.extend(chunk)
|
|
return bytes(buf)
|