﻿#!/usr/bin/env python3
from __future__ import annotations
import logging, socket, subprocess, time
from pathlib import Path
# Konfiguration
TARGET_HOST="192.168.100.10"; TARGET_PORT=46181
HEARTBEAT_INTERVAL=5.0; REPLY_TIMEOUT=3.0; CONNECT_TIMEOUT=5.0; RECONNECT_INTERVAL=5.0
SENDMAIL="/usr/sbin/sendmail"; MAIL_FROM="tcp-monitor@example.com"; MAIL_TO="admin@example.com"
LOG_FILE=Path(__file__).resolve().with_suffix(".log")
def send_mail(subject:str,body:str)->bool:
    data=(f"From: {MAIL_FROM}\nTo: {MAIL_TO}\nSubject: [TCP-Monitor] {subject}\n"
          "MIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\n\n"+body.rstrip()+"\n").encode()
    try:
        p=subprocess.run([SENDMAIL,"-t","-i"],input=data,stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,timeout=10,check=False)
    except (OSError,subprocess.TimeoutExpired) as exc:
        logging.error("MAIL_FEHLER %s",exc); return False
    if p.returncode:
        logging.error("MAIL_FEHLER Exit=%d stderr=%r",p.returncode,p.stderr.decode(errors="replace").strip())
        return False
    logging.info("MAIL_OK Betreff=%r",subject); return True
def receive_line(sock:socket.socket,pending:bytes)->tuple[bytes,bytes]:
    while b"\n" not in pending:
        chunk=sock.recv(256)
        if not chunk: raise ConnectionError("Socket-Abbruch: Gegenstelle hat geschlossen")
        pending+=chunk
        if len(pending)>256: raise ConnectionError("Protokollfehler: Antwort zu lang")
    line,pending=pending.split(b"\n",1); return line.rstrip(b"\r"),pending
def main()->None:
    state,down_at,down_text="UNKNOWN",None,None
    counter=int(time.time())
    logging.info("START Ziel=%s:%d Startzaehler=%d",TARGET_HOST,TARGET_PORT,counter)
    while True:
        try:
            with socket.create_connection((TARGET_HOST,TARGET_PORT),timeout=CONNECT_TIMEOUT) as sock:
                sock.settimeout(REPLY_TIMEOUT); sock.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY,1)
                logging.info("VERBUNDEN Ziel=%s:%d",TARGET_HOST,TARGET_PORT); pending=b""
                while True:
                    counter+=1; expected=f"PONG {counter}".encode("ascii"); started=time.monotonic()
                    sock.sendall(f"PING {counter}\n".encode("ascii"))
                    reply,pending=receive_line(sock,pending); rtt=(time.monotonic()-started)*1000
                    if reply!=expected:
                        raise ConnectionError(f"Heartbeat-Mismatch: erwartet={expected!r}, empfangen={reply!r}")
                    logging.info("HEARTBEAT_OK Zaehler=%d RTT=%.2fms",counter,rtt)
                    if state!="UP":
                        if state=="DOWN":
                            duration=time.monotonic()-down_at
                            logging.info("WIEDERHERGESTELLT Dauer=%.1fs",duration)
                            send_mail(f"WIEDERHERGESTELLT {TARGET_HOST}",
                              f"Ziel: {TARGET_HOST}:{TARGET_PORT}\nStoerungsbeginn: {down_text}\n"
                              f"Dauer: {duration:.1f} Sekunden\nZaehler: {counter}\nRTT: {rtt:.2f} ms")
                        else: logging.info("BEREIT Erster Heartbeat erfolgreich")
                        state,down_at,down_text="UP",None,None
                    time.sleep(max(0,HEARTBEAT_INTERVAL-(time.monotonic()-started)))
        except (OSError,ConnectionError) as exc:
            detail=(f"Antworttimeout nach {REPLY_TIMEOUT:.1f} Sekunden"
                    if isinstance(exc,socket.timeout) else (str(exc) or type(exc).__name__))
            logging.warning("VERBINDUNGSFEHLER %s",detail)
            if state!="DOWN":
                event="Bestehende Verbindung gestoert" if state=="UP" else "Beim Start keine Verbindung"
                state,down_at,down_text="DOWN",time.monotonic(),time.strftime("%Y-%m-%d %H:%M:%S %z")
                logging.error("STOERUNG Ereignis=%r Fehler=%r",event,detail)
                send_mail(f"STOERUNG {TARGET_HOST}",f"Ziel: {TARGET_HOST}:{TARGET_PORT}\nZeit: {down_text}\n"
                          f"Ereignis: {event}\nFehler: {detail}\nReconnect alle {RECONNECT_INTERVAL:.1f} Sekunden.")
            time.sleep(RECONNECT_INTERVAL)
if __name__=="__main__":
    logging.basicConfig(level=logging.INFO,format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
      datefmt="%Y-%m-%d %H:%M:%S",handlers=[logging.FileHandler(LOG_FILE,encoding="utf-8"),logging.StreamHandler()])
    try: main()
    except KeyboardInterrupt: logging.info("ENDE Manuell beendet")
