#!/usr/bin/env python3
"""netconsole-recv.py —— 在 DSH 机（192.168.31.76 / 192.168.31.114）接收 GPU 机发来的内核日志

背景：2026-09-15 18:38 GPU 机（zywpc, 192.168.31.31）整机冻结——屏幕还亮着、光标不动、
输入无响应，磁盘里没有留下任何死因（`panic=0`、无 pstore、无 kdump、journald 默认 5 分钟才刷盘）。
整机冻死时**磁盘写不进去，但 netconsole 的 UDP 路径仍可能把最后几条 printk 发出来**，
所以接收端要独立、常驻、每条都立刻 flush 落盘。

用法：
    python3 netconsole-recv.py                # 前台，默认监听 0.0.0.0:6666
    PORT=6666 OUT=~/netconsole-zywpc.log python3 netconsole-recv.py
建议用 systemd 单元常驻（见 netconsole-recv.service）。
"""

from __future__ import annotations

import os
import socket
import sys
import time

PORT = int(os.environ.get("PORT", "6666"))
OUT = os.path.expanduser(os.environ.get("OUT", "~/netconsole-zywpc.log"))
MAX_BYTES = int(os.environ.get("MAX_BYTES", str(64 * 1024 * 1024)))


def rotate(path: str) -> None:
    try:
        if os.path.exists(path) and os.path.getsize(path) > MAX_BYTES:
            os.replace(path, path + ".1")
    except OSError:
        pass


def main() -> int:
    os.makedirs(os.path.dirname(OUT) or ".", exist_ok=True)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(("0.0.0.0", PORT))

    with open(OUT, "a", buffering=1, encoding="utf-8", errors="replace") as fh:
        fh.write(f"\n=== netconsole receiver started on udp/{PORT} at {time.strftime('%Y-%m-%dT%H:%M:%S%z')} ===\n")
        fh.flush()
        os.fsync(fh.fileno())
        rotate(OUT)
        while True:
            data, addr = sock.recvfrom(65535)
            stamp = time.strftime("%Y-%m-%dT%H:%M:%S")
            text = data.decode("utf-8", errors="replace").rstrip("\n")
            for line in text.splitlines() or [""]:
                fh.write(f"{stamp} {addr[0]} {line}\n")
            fh.flush()
            os.fsync(fh.fileno())   # 关键：立刻落盘，别留在用户态缓冲里


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit(0)
