# -*- coding: utf-8 -*-
"""
DZW 뷰어 서버 감시장치 (watchdog)
------------------------------------------------------------
목적: 3D 뷰어 서버(8090·8095)가 소리 없이 죽으면 CEO 브라우저가
      캐시된 옛 코드로만 돌아가는 사태가 재발한다. 60초마다 살아있는지
      확인하고, 죽어 있으면 자동으로 다시 띄운다.

- 표준 라이브러리만 사용
- 자기 자신은 lock 소켓(포트)으로 1개만 실행되도록 보장
- 재기동 시각·사유를 dzw_watchdog.log 에 한 줄씩 기록(최근 500줄 유지)
"""
import os
import sys
import time
import socket
import subprocess
import urllib.request
from datetime import datetime

# ------------------------------------------------------------------
# 설정 (동결값 — 함부로 바꾸지 말 것)
# ------------------------------------------------------------------
PYTHON_EXE = r"C:\Users\user\AppData\Local\Programs\Python\Python312\python.exe"
CHECK_URL_PATH = "/DOZIKWORKS_OS_v5_locked.dc.html"
CHECK_INTERVAL = 60          # 초
HTTP_TIMEOUT = 8             # 초
SELF_LOCK_PORT = 8099        # 감시장치 중복 실행 방지용 (서버 포트와 겹치지 않게)
LOG_MAX_LINES = 500

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_PATH = os.path.join(BASE_DIR, "dzw_watchdog.log")

# 감시 대상: 포트 -> (작업폴더, 서버스크립트)
TARGETS = {
    8090: {
        "cwd": r"E:\도진팩토리\3D스캔및티칭시스템",
        "script": "dzw_server.py",
    },
    8095: {
        "cwd": r"E:\도진팩토리\3D스캔티칭_20260709판",
        "script": "dzw_server.py",
    },
}

# Windows: 창 숨김
CREATE_NO_WINDOW = 0x08000000


def log(msg):
    """로그 한 줄 기록 후 최근 LOG_MAX_LINES 줄만 유지."""
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    try:
        with open(LOG_PATH, "a", encoding="utf-8") as f:
            f.write(line + "\n")
        # 최근 500줄 유지
        with open(LOG_PATH, "r", encoding="utf-8", errors="replace") as f:
            lines = f.readlines()
        if len(lines) > LOG_MAX_LINES:
            with open(LOG_PATH, "w", encoding="utf-8") as f:
                f.writelines(lines[-LOG_MAX_LINES:])
    except Exception:
        # 로그 실패가 감시를 멈추게 해선 안 된다.
        pass


def acquire_single_instance_lock():
    """lock 소켓을 잡아 감시장치가 하나만 돌게 한다. 실패 시 None."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        # SO_REUSEADDR 는 쓰지 않는다 — 이미 떠 있으면 바인딩이 실패해야 한다.
        s.bind(("127.0.0.1", SELF_LOCK_PORT))
        s.listen(1)
        return s
    except OSError:
        s.close()
        return None


def is_alive(port):
    """해당 포트의 뷰어 페이지가 응답하면 True."""
    url = f"http://localhost:{port}{CHECK_URL_PATH}"
    try:
        req = urllib.request.Request(url, method="HEAD")
        with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp:
            return 200 <= resp.status < 500
    except Exception:
        # HEAD 미지원 서버 대비 GET 재시도
        try:
            req = urllib.request.Request(url, method="GET")
            with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp:
                return 200 <= resp.status < 500
        except Exception:
            return False


def restart_server(port):
    """죽은 서버를 다시 띄운다."""
    cfg = TARGETS[port]
    cwd = cfg["cwd"]
    script = cfg["script"]
    try:
        subprocess.Popen(
            [PYTHON_EXE, script, str(port)],
            cwd=cwd,
            creationflags=CREATE_NO_WINDOW,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            close_fds=True,
        )
        log(f"재기동 실행: 포트 {port} (사유: 무응답) -> {PYTHON_EXE} {script} {port} @ {cwd}")
        return True
    except Exception as e:
        log(f"재기동 실패: 포트 {port} 사유: {e!r}")
        return False


def main():
    lock = acquire_single_instance_lock()
    if lock is None:
        # 이미 다른 감시장치가 돌고 있다. 조용히 종료.
        log(f"감시장치 이미 실행 중(lock 포트 {SELF_LOCK_PORT}) — 이번 인스턴스 종료.")
        sys.exit(0)

    log(f"감시장치 시작 (대상 포트: {', '.join(map(str, TARGETS))}, 주기 {CHECK_INTERVAL}s)")
    try:
        while True:
            for port in TARGETS:
                if not is_alive(port):
                    log(f"무응답 감지: 포트 {port} — 재기동 시도")
                    restart_server(port)
                    # 부팅 시간을 준다.
                    time.sleep(5)
            time.sleep(CHECK_INTERVAL)
    finally:
        try:
            lock.close()
        except Exception:
            pass


if __name__ == "__main__":
    main()
