Files
watchdogs/watchdog_gui.py
T

732 lines
24 KiB
Python
Raw Permalink Normal View History

2026-03-18 11:07:16 +09:00
# -*- coding: utf-8 -*-
import os
import sys
import json
import time
import traceback
import subprocess
2026-03-18 11:08:20 +09:00
import ctypes
from ctypes import wintypes
2026-03-18 11:07:16 +09:00
from pathlib import Path
from logging.handlers import RotatingFileHandler
import logging
import psutil
from PyQt5 import QtCore, QtGui, QtWidgets
APP_NAME = "Watchdog GUI"
2026-03-18 11:08:20 +09:00
APP_VERSION = "1.1.0"
2026-03-18 11:07:16 +09:00
BASE_DIR = Path(__file__).resolve().parent
CONFIG_PATH = BASE_DIR / "watchdog_config.json"
LOG_DIR = BASE_DIR / "watchdog_logs"
LOG_FILE = LOG_DIR / "watchdog.log"
DEFAULT_CONFIG = {
"target_exe_path": "",
"check_interval_sec": 5,
"restart_cooldown_sec": 10,
"auto_start_monitoring": False,
"start_minimized_to_tray": False,
"hide_on_close": True,
"show_log_panel": False
}
# =========================
# 로거
# =========================
def setup_logger():
LOG_DIR.mkdir(parents=True, exist_ok=True)
logger = logging.getLogger("watchdog_gui")
logger.setLevel(logging.INFO)
if logger.handlers:
return logger
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
file_handler = RotatingFileHandler(
str(LOG_FILE),
maxBytes=2 * 1024 * 1024,
backupCount=5,
encoding="utf-8"
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
LOGGER = setup_logger()
# =========================
# UI 로그 핸들러
# =========================
class QTextEditLogger(logging.Handler, QtCore.QObject):
log_signal = QtCore.pyqtSignal(str)
def __init__(self, text_edit: QtWidgets.QPlainTextEdit):
logging.Handler.__init__(self)
QtCore.QObject.__init__(self)
self.text_edit = text_edit
self.log_signal.connect(self.append_log)
def emit(self, record):
try:
msg = self.format(record)
self.log_signal.emit(msg)
except Exception:
pass
@QtCore.pyqtSlot(str)
def append_log(self, msg: str):
self.text_edit.appendPlainText(msg)
sb = self.text_edit.verticalScrollBar()
sb.setValue(sb.maximum())
# =========================
# 설정
# =========================
def load_config():
if not CONFIG_PATH.exists():
save_config(DEFAULT_CONFIG)
return DEFAULT_CONFIG.copy()
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
cfg = DEFAULT_CONFIG.copy()
cfg.update(data)
return cfg
except Exception:
LOGGER.exception("설정 파일 로드 실패, 기본값 사용")
return DEFAULT_CONFIG.copy()
def save_config(cfg: dict):
try:
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)
except Exception:
LOGGER.exception("설정 파일 저장 실패")
2026-03-18 11:08:20 +09:00
# =========================
# 버전 유틸
# =========================
def get_file_version(file_path: str) -> str:
"""
Windows EXE/DLL 파일 버전 읽기
실패 시 '알 수 없음' 반환
"""
try:
if not file_path or not os.path.exists(file_path):
return "알 수 없음"
size = ctypes.windll.version.GetFileVersionInfoSizeW(file_path, None)
if not size:
return "알 수 없음"
res = ctypes.create_string_buffer(size)
ctypes.windll.version.GetFileVersionInfoW(file_path, 0, size, res)
u_len = wintypes.UINT()
u_ptr = ctypes.c_void_p()
ok = ctypes.windll.version.VerQueryValueW(
res,
"\\",
ctypes.byref(u_ptr),
ctypes.byref(u_len)
)
if not ok:
return "알 수 없음"
class VS_FIXEDFILEINFO(ctypes.Structure):
_fields_ = [
("dwSignature", wintypes.DWORD),
("dwStrucVersion", wintypes.DWORD),
("dwFileVersionMS", wintypes.DWORD),
("dwFileVersionLS", wintypes.DWORD),
("dwProductVersionMS", wintypes.DWORD),
("dwProductVersionLS", wintypes.DWORD),
("dwFileFlagsMask", wintypes.DWORD),
("dwFileFlags", wintypes.DWORD),
("dwFileOS", wintypes.DWORD),
("dwFileType", wintypes.DWORD),
("dwFileSubtype", wintypes.DWORD),
("dwFileDateMS", wintypes.DWORD),
("dwFileDateLS", wintypes.DWORD),
]
ffi = ctypes.cast(u_ptr, ctypes.POINTER(VS_FIXEDFILEINFO)).contents
major = (ffi.dwFileVersionMS >> 16) & 0xFFFF
minor = ffi.dwFileVersionMS & 0xFFFF
build = (ffi.dwFileVersionLS >> 16) & 0xFFFF
revision = ffi.dwFileVersionLS & 0xFFFF
return f"{major}.{minor}.{build}.{revision}"
except Exception:
LOGGER.exception("파일 버전 읽기 실패")
return "알 수 없음"
2026-03-18 11:07:16 +09:00
# =========================
# 감시 유틸
# =========================
def normalize_path(path: str) -> str:
try:
return os.path.normcase(os.path.abspath(path))
except Exception:
return path
def get_target_process_name(target_exe_path: str) -> str:
return os.path.basename(target_exe_path).lower().strip()
def is_process_running_by_path(target_exe_path: str) -> bool:
if not target_exe_path:
return False
target_exe_path_n = normalize_path(target_exe_path)
target_name = get_target_process_name(target_exe_path)
for proc in psutil.process_iter(["pid", "name", "exe"]):
try:
proc_name = (proc.info.get("name") or "").lower().strip()
proc_exe = proc.info.get("exe") or ""
if proc_exe:
proc_exe_n = normalize_path(proc_exe)
if proc_exe_n == target_exe_path_n:
return True
if proc_name == target_name:
return True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
except Exception:
LOGGER.exception("프로세스 확인 중 오류")
return False
def start_target_program(target_exe_path: str) -> bool:
if not target_exe_path:
LOGGER.error("실행할 대상 프로그램 경로가 비어 있습니다.")
return False
if not os.path.exists(target_exe_path):
LOGGER.error(f"대상 프로그램이 존재하지 않습니다: {target_exe_path}")
return False
try:
work_dir = os.path.dirname(target_exe_path) or None
subprocess.Popen(
[target_exe_path],
cwd=work_dir,
shell=False
)
LOGGER.info(f"프로그램 재실행 성공: {target_exe_path}")
return True
except Exception:
LOGGER.exception(f"프로그램 재실행 실패: {target_exe_path}")
return False
# =========================
# 메인 윈도우
# =========================
class WatchdogWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
2026-03-18 11:08:20 +09:00
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
self.resize(560, 300)
self.setMinimumSize(520, 270)
2026-03-18 11:07:16 +09:00
self.cfg = load_config()
self.monitoring = False
self.last_restart_time = 0.0
self._really_quit = False
self._tray_shown_once = False
self._build_ui()
self._connect_signals()
self._setup_log_to_ui()
self._load_config_to_ui()
self._create_tray_icon()
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.check_target_once)
LOGGER.info("=" * 60)
2026-03-18 11:08:20 +09:00
LOGGER.info(f"{APP_NAME} 시작 - 버전 {APP_VERSION}")
2026-03-18 11:07:16 +09:00
LOGGER.info(f"로그 파일: {LOG_FILE}")
LOGGER.info("=" * 60)
if self.cfg.get("auto_start_monitoring", False):
self.start_monitoring()
self.update_target_status_label()
2026-03-18 11:08:20 +09:00
self.update_version_labels()
2026-03-18 11:07:16 +09:00
if self.cfg.get("start_minimized_to_tray", False):
QtCore.QTimer.singleShot(300, self.hide_to_tray)
# ---------------------
# UI 구성
# ---------------------
def _build_ui(self):
cw = QtWidgets.QWidget()
self.setCentralWidget(cw)
main_layout = QtWidgets.QVBoxLayout(cw)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(8)
2026-03-18 11:08:20 +09:00
# 버전 표시
version_layout = QtWidgets.QGridLayout()
version_layout.setHorizontalSpacing(8)
version_layout.setVerticalSpacing(4)
self.lbl_watchdog_version = QtWidgets.QLabel(f"Watchdog 버전: {APP_VERSION}")
self.lbl_watchdog_version.setStyleSheet("font-weight: bold; color: #1f1f1f;")
self.lbl_target_version = QtWidgets.QLabel("대상 버전: 알 수 없음")
self.lbl_target_version.setStyleSheet("font-weight: bold; color: #1f1f1f;")
version_layout.addWidget(self.lbl_watchdog_version, 0, 0)
version_layout.addWidget(self.lbl_target_version, 0, 1)
2026-03-18 11:07:16 +09:00
# 경로 행
path_layout = QtWidgets.QHBoxLayout()
path_layout.setSpacing(6)
self.edit_exe_path = QtWidgets.QLineEdit()
self.edit_exe_path.setPlaceholderText("감시할 EXE 경로 선택")
self.btn_browse = QtWidgets.QPushButton("찾기")
self.btn_browse.setFixedWidth(70)
path_layout.addWidget(QtWidgets.QLabel("대상"))
path_layout.addWidget(self.edit_exe_path, 1)
path_layout.addWidget(self.btn_browse)
# 옵션 행
option_layout = QtWidgets.QGridLayout()
option_layout.setHorizontalSpacing(8)
option_layout.setVerticalSpacing(6)
self.spin_interval = QtWidgets.QSpinBox()
self.spin_interval.setRange(1, 3600)
self.spin_interval.setSuffix(" 초")
self.spin_interval.setFixedWidth(90)
self.spin_cooldown = QtWidgets.QSpinBox()
self.spin_cooldown.setRange(1, 3600)
self.spin_cooldown.setSuffix(" 초")
self.spin_cooldown.setFixedWidth(90)
self.chk_auto_start = QtWidgets.QCheckBox("실행 시 감시 자동 시작")
self.chk_start_minimized = QtWidgets.QCheckBox("실행 시 트레이로 숨김")
self.chk_hide_on_close = QtWidgets.QCheckBox("닫기(X) 시 종료하지 않고 트레이로 숨김")
option_layout.addWidget(QtWidgets.QLabel("체크 주기"), 0, 0)
option_layout.addWidget(self.spin_interval, 0, 1)
option_layout.addWidget(QtWidgets.QLabel("재실행 대기"), 0, 2)
option_layout.addWidget(self.spin_cooldown, 0, 3)
option_layout.addWidget(self.chk_auto_start, 1, 0, 1, 2)
option_layout.addWidget(self.chk_start_minimized, 1, 2, 1, 2)
option_layout.addWidget(self.chk_hide_on_close, 2, 0, 1, 4)
# 버튼/상태 행
top_btn_layout = QtWidgets.QHBoxLayout()
top_btn_layout.setSpacing(6)
self.btn_start = QtWidgets.QPushButton("감시 시작")
self.btn_stop = QtWidgets.QPushButton("감시 중지")
self.btn_stop.setEnabled(False)
self.btn_toggle_log = QtWidgets.QPushButton("로그 보기")
self.btn_open_log_file = QtWidgets.QPushButton("로그 파일 열기")
self.btn_start.setFixedHeight(30)
self.btn_stop.setFixedHeight(30)
self.btn_toggle_log.setFixedHeight(30)
self.btn_open_log_file.setFixedHeight(30)
top_btn_layout.addWidget(self.btn_start)
top_btn_layout.addWidget(self.btn_stop)
top_btn_layout.addWidget(self.btn_toggle_log)
top_btn_layout.addWidget(self.btn_open_log_file)
# 상태 표시
status_layout = QtWidgets.QVBoxLayout()
status_layout.setSpacing(4)
self.lbl_monitor = QtWidgets.QLabel("감시 상태: 중지")
self.lbl_monitor.setStyleSheet("font-weight: bold; color: #b00020;")
self.lbl_target = QtWidgets.QLabel("대상 상태: 미확인")
self.lbl_target.setStyleSheet("font-weight: bold; color: #444;")
self.lbl_hint = QtWidgets.QLabel("안내: 창을 닫아도 트레이로 숨길 수 있습니다.")
self.lbl_hint.setStyleSheet("color: #666; font-size: 11px;")
status_layout.addWidget(self.lbl_monitor)
status_layout.addWidget(self.lbl_target)
status_layout.addWidget(self.lbl_hint)
# 로그 패널
self.log_group = QtWidgets.QGroupBox("로그")
log_layout = QtWidgets.QVBoxLayout(self.log_group)
log_layout.setContentsMargins(8, 8, 8, 8)
self.text_log = QtWidgets.QPlainTextEdit()
self.text_log.setReadOnly(True)
self.text_log.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
self.text_log.setMinimumHeight(180)
self.btn_clear_log = QtWidgets.QPushButton("로그창 비우기")
self.btn_clear_log.setFixedHeight(28)
log_layout.addWidget(self.text_log)
log_layout.addWidget(self.btn_clear_log)
2026-03-18 11:08:20 +09:00
main_layout.addLayout(version_layout)
2026-03-18 11:07:16 +09:00
main_layout.addLayout(path_layout)
main_layout.addLayout(option_layout)
main_layout.addLayout(top_btn_layout)
main_layout.addLayout(status_layout)
main_layout.addWidget(self.log_group)
def _connect_signals(self):
self.btn_browse.clicked.connect(self.browse_exe)
self.btn_start.clicked.connect(self.start_monitoring)
self.btn_stop.clicked.connect(self.stop_monitoring)
self.btn_toggle_log.clicked.connect(self.toggle_log_panel)
self.btn_open_log_file.clicked.connect(self.open_log_file)
self.btn_clear_log.clicked.connect(self.text_log.clear)
2026-03-18 11:08:20 +09:00
self.edit_exe_path.editingFinished.connect(self.on_path_edited)
2026-03-18 11:07:16 +09:00
self.spin_interval.valueChanged.connect(self.save_ui_to_config)
self.spin_cooldown.valueChanged.connect(self.save_ui_to_config)
self.chk_auto_start.stateChanged.connect(self.save_ui_to_config)
self.chk_start_minimized.stateChanged.connect(self.save_ui_to_config)
self.chk_hide_on_close.stateChanged.connect(self.save_ui_to_config)
def _setup_log_to_ui(self):
self.ui_log_handler = QTextEditLogger(self.text_log)
self.ui_log_handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)
LOGGER.addHandler(self.ui_log_handler)
def _load_config_to_ui(self):
self.edit_exe_path.setText(self.cfg.get("target_exe_path", ""))
self.spin_interval.setValue(int(self.cfg.get("check_interval_sec", 5)))
self.spin_cooldown.setValue(int(self.cfg.get("restart_cooldown_sec", 10)))
self.chk_auto_start.setChecked(bool(self.cfg.get("auto_start_monitoring", False)))
self.chk_start_minimized.setChecked(bool(self.cfg.get("start_minimized_to_tray", False)))
self.chk_hide_on_close.setChecked(bool(self.cfg.get("hide_on_close", True)))
show_log = bool(self.cfg.get("show_log_panel", False))
self.log_group.setVisible(show_log)
self.btn_toggle_log.setText("로그 숨기기" if show_log else "로그 보기")
if show_log:
2026-03-18 11:08:20 +09:00
self.resize(600, 540)
2026-03-18 11:07:16 +09:00
else:
2026-03-18 11:08:20 +09:00
self.resize(560, 300)
2026-03-18 11:07:16 +09:00
def save_ui_to_config(self):
self.cfg["target_exe_path"] = self.edit_exe_path.text().strip()
self.cfg["check_interval_sec"] = int(self.spin_interval.value())
self.cfg["restart_cooldown_sec"] = int(self.spin_cooldown.value())
self.cfg["auto_start_monitoring"] = bool(self.chk_auto_start.isChecked())
self.cfg["start_minimized_to_tray"] = bool(self.chk_start_minimized.isChecked())
self.cfg["hide_on_close"] = bool(self.chk_hide_on_close.isChecked())
self.cfg["show_log_panel"] = bool(self.log_group.isVisible())
save_config(self.cfg)
2026-03-18 11:08:20 +09:00
def on_path_edited(self):
self.save_ui_to_config()
self.update_target_status_label()
self.update_version_labels()
def update_version_labels(self):
exe_path = self.edit_exe_path.text().strip()
target_ver = get_file_version(exe_path)
self.lbl_watchdog_version.setText(f"Watchdog 버전: {APP_VERSION}")
self.lbl_target_version.setText(f"대상 버전: {target_ver}")
2026-03-18 11:07:16 +09:00
# ---------------------
# 트레이
# ---------------------
def _create_tray_icon(self):
self.tray_icon = QtWidgets.QSystemTrayIcon(self)
icon = self.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
self.setWindowIcon(icon)
self.tray_icon.setIcon(icon)
menu = QtWidgets.QMenu()
self.action_show = menu.addAction("창 열기")
self.action_hide = menu.addAction("트레이로 숨기기")
menu.addSeparator()
self.action_start = menu.addAction("감시 시작")
self.action_stop = menu.addAction("감시 중지")
menu.addSeparator()
self.action_quit = menu.addAction("종료")
self.action_show.triggered.connect(self.show_normal_window)
self.action_hide.triggered.connect(self.hide_to_tray)
self.action_start.triggered.connect(self.start_monitoring)
self.action_stop.triggered.connect(self.stop_monitoring)
self.action_quit.triggered.connect(self.quit_application)
self.tray_icon.setContextMenu(menu)
self.tray_icon.activated.connect(self.on_tray_icon_activated)
self.tray_icon.show()
def on_tray_icon_activated(self, reason):
if reason == QtWidgets.QSystemTrayIcon.DoubleClick:
self.show_normal_window()
def show_normal_window(self):
self.show()
self.raise_()
self.activateWindow()
def hide_to_tray(self):
self.hide()
if not self._tray_shown_once:
self.tray_icon.showMessage(
APP_NAME,
"프로그램이 트레이로 숨겨졌습니다. 트레이 아이콘에서 다시 열 수 있습니다.",
QtWidgets.QSystemTrayIcon.Information,
3000
)
self._tray_shown_once = True
def quit_application(self):
self._really_quit = True
self.save_ui_to_config()
LOGGER.info("사용자 요청으로 Watchdog GUI 종료")
QtWidgets.QApplication.quit()
# ---------------------
# 기능
# ---------------------
def browse_exe(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
"감시할 EXE 선택",
self.edit_exe_path.text().strip() or str(BASE_DIR),
"Executable Files (*.exe);;All Files (*.*)"
)
if path:
self.edit_exe_path.setText(path)
self.save_ui_to_config()
2026-03-18 11:08:20 +09:00
self.update_version_labels()
2026-03-18 11:07:16 +09:00
LOGGER.info(f"감시 대상 선택: {path}")
self.update_target_status_label()
def toggle_log_panel(self):
visible = not self.log_group.isVisible()
self.log_group.setVisible(visible)
self.btn_toggle_log.setText("로그 숨기기" if visible else "로그 보기")
if visible:
2026-03-18 11:08:20 +09:00
self.resize(max(self.width(), 600), 540)
2026-03-18 11:07:16 +09:00
else:
2026-03-18 11:08:20 +09:00
self.resize(max(560, self.width()), 300)
2026-03-18 11:07:16 +09:00
self.save_ui_to_config()
def open_log_file(self):
try:
if not LOG_FILE.exists():
LOGGER.warning("열 로그 파일이 아직 없습니다.")
return
os.startfile(str(LOG_FILE))
except Exception:
LOGGER.exception("로그 파일 열기 실패")
def start_monitoring(self):
self.save_ui_to_config()
exe_path = self.edit_exe_path.text().strip()
if not exe_path:
QtWidgets.QMessageBox.warning(self, "경고", "감시할 프로그램을 먼저 선택하세요.")
return
if not os.path.exists(exe_path):
QtWidgets.QMessageBox.warning(self, "경고", "선택한 프로그램 경로가 존재하지 않습니다.")
return
interval_ms = int(self.spin_interval.value()) * 1000
self.timer.start(interval_ms)
self.monitoring = True
self.btn_start.setEnabled(False)
self.btn_stop.setEnabled(True)
self.action_start.setEnabled(False)
self.action_stop.setEnabled(True)
self.lbl_monitor.setText("감시 상태: 실행 중")
self.lbl_monitor.setStyleSheet("font-weight: bold; color: #007a1f;")
2026-03-18 11:08:20 +09:00
self.update_version_labels()
2026-03-18 11:07:16 +09:00
LOGGER.info(f"감시 시작: {exe_path}")
LOGGER.info(f"체크 주기: {self.spin_interval.value()}초 / 쿨다운: {self.spin_cooldown.value()}초")
self.check_target_once()
def stop_monitoring(self):
self.timer.stop()
self.monitoring = False
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
self.action_start.setEnabled(True)
self.action_stop.setEnabled(False)
self.lbl_monitor.setText("감시 상태: 중지")
self.lbl_monitor.setStyleSheet("font-weight: bold; color: #b00020;")
LOGGER.info("감시 중지")
def update_target_status_label(self, running=None):
if running is None:
exe_path = self.edit_exe_path.text().strip()
running = is_process_running_by_path(exe_path) if exe_path else False
if running:
self.lbl_target.setText("대상 상태: 실행 중")
self.lbl_target.setStyleSheet("font-weight: bold; color: #007a1f;")
self.tray_icon.setToolTip(f"{APP_NAME} - 대상 실행 중")
else:
self.lbl_target.setText("대상 상태: 종료됨")
self.lbl_target.setStyleSheet("font-weight: bold; color: #b00020;")
self.tray_icon.setToolTip(f"{APP_NAME} - 대상 종료됨")
def check_target_once(self):
try:
exe_path = self.edit_exe_path.text().strip()
if not exe_path:
self.update_target_status_label(False)
2026-03-18 11:08:20 +09:00
self.update_version_labels()
2026-03-18 11:07:16 +09:00
LOGGER.warning("감시 대상 경로가 비어 있습니다.")
return
if not os.path.exists(exe_path):
self.update_target_status_label(False)
2026-03-18 11:08:20 +09:00
self.update_version_labels()
2026-03-18 11:07:16 +09:00
LOGGER.error(f"감시 대상 파일이 존재하지 않습니다: {exe_path}")
return
running = is_process_running_by_path(exe_path)
self.update_target_status_label(running)
2026-03-18 11:08:20 +09:00
self.update_version_labels()
2026-03-18 11:07:16 +09:00
if running:
return
now = time.time()
cooldown = int(self.spin_cooldown.value())
elapsed = now - self.last_restart_time
LOGGER.warning("대상 프로그램이 종료된 것으로 감지됨")
if elapsed < cooldown:
remain = int(cooldown - elapsed)
LOGGER.warning(f"재실행 쿨다운 중: {remain}초 남음")
return
ok = start_target_program(exe_path)
if ok:
self.last_restart_time = time.time()
self.tray_icon.showMessage(
APP_NAME,
"대상 프로그램이 종료되어 자동 재실행했습니다.",
QtWidgets.QSystemTrayIcon.Warning,
2500
)
2026-03-18 11:08:20 +09:00
QtCore.QTimer.singleShot(1500, self._refresh_after_restart)
2026-03-18 11:07:16 +09:00
else:
self.update_target_status_label(False)
except Exception:
LOGGER.error("감시 루프 오류 발생")
LOGGER.error(traceback.format_exc())
2026-03-18 11:08:20 +09:00
def _refresh_after_restart(self):
self.update_target_status_label()
self.update_version_labels()
2026-03-18 11:07:16 +09:00
# ---------------------
# 종료 / 최소화
# ---------------------
def changeEvent(self, event):
if event.type() == QtCore.QEvent.WindowStateChange:
if self.isMinimized():
QtCore.QTimer.singleShot(0, self._handle_minimize)
super().changeEvent(event)
def _handle_minimize(self):
if self.chk_hide_on_close.isChecked():
self.hide_to_tray()
def closeEvent(self, event: QtGui.QCloseEvent):
try:
self.save_ui_to_config()
except Exception:
pass
if self._really_quit:
LOGGER.info("Watchdog GUI 종료")
event.accept()
return
if self.chk_hide_on_close.isChecked():
event.ignore()
self.hide_to_tray()
return
LOGGER.info("Watchdog GUI 종료")
event.accept()
# =========================
# 메인
# =========================
def main():
app = QtWidgets.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
app.setApplicationName(APP_NAME)
font = QtGui.QFont("맑은 고딕", 9)
app.setFont(font)
win = WatchdogWindow()
if not win.cfg.get("start_minimized_to_tray", False):
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()