/
renath
/
WebsiteMonitor
Обзор
Документация
Войти
/
renath
/
WebsiteMonitor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
website_monitor.py
604 строки
28 KB
Renatka
Переделал на Python
27 ноя 2025, 14:10
27 ноя 2025, 14:10
96539e1
Код
Авторство
О чём код?
import requests import subprocess import configparser import time import logging import sys import os import socket from datetime import datetime from urllib.parse import urlparse from typing import List, Tuple, Dict, Optional import threading import urllib3 # Отключаем предупреждения SSL для urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class WebsiteMonitor: def __init__(self, config_path: str = "config.ini"): self.config_path = config_path self.config = None self.pt_path = "" self.check_interval = 30 self.max_retries = 2 self.retry_delay = 5 self.hosts = [] self.window_visible = True self.running_processes = {} self.last_config_mod_time = 0 self.config_cache_seconds = 5 self.last_config_check = 0 # Инициализируем основные методы до загрузки конфига self.setup_logging() self.load_config() self.setup_window_visibility() def setup_window_visibility(self): """Настройка видимости окна консоли""" try: if hasattr(sys, 'frozen'): # Если программа собрана в exe import ctypes console_window = ctypes.windll.kernel32.GetConsoleWindow() if not self.window_visible and console_window: # Скрываем консольное окно ctypes.windll.user32.ShowWindow(console_window, 0) # SW_HIDE self.logger.info("Окно программы скрыто") else: # Показываем консольное окно ctypes.windll.user32.ShowWindow(console_window, 1) # SW_SHOW self.logger.info("Окно программы видимо") except Exception as e: self.logger.warning(f"Не удалось настроить видимость окна: {e}") def setup_logging(self): """Настройка системы логирования""" # Создаем логгер logger = logging.getLogger() logger.setLevel(logging.INFO) # Очищаем существующие handlers for handler in logger.handlers[:]: logger.removeHandler(handler) # Форматтер formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') # File handler (всегда работает) file_handler = logging.FileHandler('website_monitor.log', encoding='utf-8') file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Console handler console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) logger.addHandler(console_handler) self.logger = logger def safe_get_boolean(self, config, section, option, fallback=True): """Безопасное получение boolean значения""" try: value = config.get(section, option, fallback=str(fallback)) # Удаляем возможные комментарии value = value.split(';')[0].split('#')[0].strip() if value.lower() in ('true', 'yes', '1', 'on'): return True elif value.lower() in ('false', 'no', '0', 'off'): return False else: self.logger.warning(f"Неизвестное boolean значение '{value}' для {option}. Используется значение по умолчанию: {fallback}") return fallback except Exception as e: self.logger.warning(f"Ошибка чтения {option}: {e}. Используется значение по умолчанию: {fallback}") return fallback def safe_get_int(self, config, section, option, fallback=30): """Безопасное получение integer значения""" try: value = config.get(section, option, fallback=str(fallback)) # Удаляем возможные комментарии value = value.split(';')[0].split('#')[0].strip() return int(value) except (ValueError, TypeError) as e: self.logger.warning(f"Ошибка чтения {option}: {e}. Используется значение по умолчанию: {fallback}") return fallback def safe_get_string(self, config, section, option, fallback=''): """Безопасное получение string значения""" try: value = config.get(section, option, fallback=fallback) # Удаляем возможные комментарии value = value.split(';')[0].split('#')[0].strip() return value except Exception as e: self.logger.warning(f"Ошибка чтения {option}: {e}. Используется значение по умолчанию: {fallback}") return fallback def load_config(self, force_reload: bool = False): """Загрузка конфигурации из файла""" # Если конфиг не существует, создаем его if not os.path.exists(self.config_path): self.logger.warning(f"Конфигурационный файл {self.config_path} не найден! Создаем новый.") self.create_default_config() try: mod_time = os.path.getmtime(self.config_path) temp_config = configparser.ConfigParser() temp_config.read(self.config_path, encoding='utf-8') # Создаем секции если их нет if not temp_config.has_section('Settings'): temp_config.add_section('Settings') if not temp_config.has_section('Hosts'): temp_config.add_section('Hosts') # Сохраняем старые значения для сравнения old_hosts = self.hosts.copy() if hasattr(self, 'hosts') else [] old_interval = self.check_interval if hasattr(self, 'check_interval') else 30 old_retries = self.max_retries if hasattr(self, 'max_retries') else 2 old_visible = self.window_visible if hasattr(self, 'window_visible') else True # Обновляем конфигурацию с безопасными методами self.config = temp_config # Загрузка настроек с безопасными методами self.pt_path = self.safe_get_string(temp_config, 'Settings', 'pt_path', '') self.check_interval = self.safe_get_int(temp_config, 'Settings', 'check_interval', 30) self.max_retries = self.safe_get_int(temp_config, 'Settings', 'max_retries', 2) self.retry_delay = self.safe_get_int(temp_config, 'Settings', 'retry_delay', 5) self.window_visible = self.safe_get_boolean(temp_config, 'Settings', 'window_visible', True) # Загрузка хостов hosts_str = self.safe_get_string(temp_config, 'Hosts', 'hosts', '') new_hosts = [host.strip() for host in hosts_str.split(',') if host.strip()] # Проверяем изменения config_changed = False if new_hosts != old_hosts: self.logger.info(f"Обновлен список хостов: было {len(old_hosts)}, стало {len(new_hosts)}") config_changed = True if self.check_interval != old_interval: self.logger.info(f"Изменен интервал проверки: было {old_interval} сек, стало {self.check_interval} сек") config_changed = True if self.max_retries != old_retries: self.logger.info(f"Изменено количество попыток: было {old_retries}, стало {self.max_retries}") config_changed = True if self.window_visible != old_visible: self.logger.info(f"Изменен режим окна: было {'видимо' if old_visible else 'скрыто'}, стало {'видимо' if self.window_visible else 'скрыто'}") config_changed = True self.hosts = new_hosts self.last_config_mod_time = mod_time if self.validate_config() and config_changed and not force_reload: self.logger.info("Конфигурация успешно загружена!") return True except Exception as e: self.logger.error(f"Критическая ошибка загрузки конфигурации: {e}") # Создаем конфиг по умолчанию при ошибке self.create_default_config() return False def create_default_config(self): """Создает конфиг по умолчанию если его нет или он поврежден""" default_config = """[Settings] pt_path = C:\\Windows\\System32\\pt.exe check_interval = 30 max_retries = 2 retry_delay = 5 window_visible = true [Hosts] hosts = https://google.com, https://github.com, 8.8.8.8, 192.168.1.1 """ try: with open(self.config_path, 'w', encoding='utf-8') as f: f.write(default_config) self.logger.info(f"Создан конфиг по умолчанию: {self.config_path}") return True except Exception as e: self.logger.error(f"Не удалось создать конфиг: {e}") return False def validate_config(self): """Проверка корректности конфигурации""" if not self.pt_path: self.logger.warning("pt_path не указан в конфигурации. .exe файл не будет запускаться.") if not os.path.exists(self.pt_path) and self.pt_path: self.logger.warning(f"Файл {self.pt_path} не найден!") if not self.hosts: self.logger.error("Не указаны хосты для мониторинга!") return False self.logger.info(f"Загружено {len(self.hosts)} хостов для мониторинга") self.logger.info(f"Интервал проверки: {self.check_interval} секунд") self.logger.info(f"Максимум попыток: {self.max_retries}") self.logger.info(f"Режим окна: {'видимый' if self.window_visible else 'скрытый'}") return True def check_internet_connectivity(self) -> bool: """Проверка наличия интернета на локальной машине""" test_hosts = [ "8.8.8.8", # Google DNS "1.1.1.1", # Cloudflare DNS "google.com" ] for host in test_hosts: try: socket.create_connection((host, 80), timeout=5) self.logger.debug(f"Интернет доступен (проверка через {host})") return True except OSError: continue return False def check_dns_resolution(self, hostname: str) -> bool: """Проверка разрешения DNS имени""" try: if hostname.startswith(('http://', 'https://')): parsed = urlparse(hostname) hostname = parsed.hostname else: try: socket.inet_aton(hostname) return True except socket.error: pass socket.gethostbyname(hostname) return True except socket.gaierror: return False def check_port_access(self, host: str, port: int = 80) -> bool: """Проверка доступности порта на хосте""" try: if host.startswith(('http://', 'https://')): parsed = urlparse(host) hostname = parsed.hostname port = parsed.port or (443 if parsed.scheme == 'https' else 80) else: hostname = host with socket.create_connection((hostname, port), timeout=10): return True except (OSError, socket.gaierror, socket.timeout): return False def analyze_problem(self, host: str, exception: Exception = None) -> str: """Детальный анализ причины проблемы""" if not self.check_internet_connectivity(): return "LOCAL_NO_INTERNET", "Нет интернета на локальной машине" if not self.check_dns_resolution(host): return "DNS_ERROR", f"Не удается разрешить DNS имя для {host}" if not self.check_port_access(host): return "HOST_UNREACHABLE", f"Хост {host} недостижим (порт закрыт)" if exception: error_str = str(exception).lower() if "certificate verify failed" in error_str: return "SSL_ERROR", f"Ошибка SSL сертификата на {host}" elif "connection refused" in error_str: return "CONNECTION_REFUSED", f"Соединение отклонено на {host} (сервер не принимает подключения)" elif "timed out" in error_str: return "TIMEOUT", f"Таймаут подключения к {host}" elif "name or service not known" in error_str: return "DNS_ERROR", f"DNS ошибка для {host}" return "UNKNOWN_ERROR", f"Неизвестная ошибка для {host}" def format_url(self, host: str) -> str: """Форматирование URL для корректного запроса""" if host.startswith(('http://', 'https://')): return host else: return f"http://{host}" def check_site(self, host: str) -> Tuple[str, str, Optional[int]]: """Проверка доступности сайта с детальным анализом""" url = self.format_url(host) session = requests.Session() session.verify = False for attempt in range(self.max_retries): try: response = session.get( url, timeout=10, headers={'User-Agent': 'WebsiteMonitor/1.0'}, allow_redirects=True ) status_code = response.status_code if status_code == 200: return "OK", f"Сайт {host} доступен (статус 200)", status_code elif 300 <= status_code < 400: return "REDIRECT", f"Сайт {host} перенаправляет (статус {status_code})", status_code elif status_code == 400: return "BAD_REQUEST", f"Сайт {host} - неверный запрос (400)", status_code elif status_code == 401: return "UNAUTHORIZED", f"Сайт {host} требует авторизацию (401)", status_code elif status_code == 403: return "FORBIDDEN", f"Доступ к {host} запрещен (403)", status_code elif status_code == 404: return "NOT_FOUND", f"Страница {host} не найдена (404)", status_code elif 400 <= status_code < 500: return "CLIENT_ERROR", f"Ошибка клиента на {host} (статус {status_code})", status_code elif status_code == 500: return "SERVER_ERROR", f"Внутренняя ошибка сервера {host} (500)", status_code elif status_code == 502: return "BAD_GATEWAY", f"Плохой шлюз на {host} (502)", status_code elif status_code == 503: return "SERVICE_UNAVAILABLE", f"Сервис {host} недоступен (503)", status_code elif status_code == 504: return "GATEWAY_TIMEOUT", f"Таймаут шлюза на {host} (504)", status_code elif 500 <= status_code < 600: return "SERVER_ERROR", f"Ошибка сервера {host} (статус {status_code})", status_code else: return "UNKNOWN_STATUS", f"Неизвестный статус {host} (статус {status_code})", status_code except requests.exceptions.ConnectionError as e: if attempt < self.max_retries - 1: time.sleep(self.retry_delay) continue error_type, error_msg = self.analyze_problem(host, e) return error_type, error_msg, None except requests.exceptions.Timeout as e: if attempt < self.max_retries - 1: time.sleep(self.retry_delay) continue error_type, error_msg = self.analyze_problem(host, e) return error_type, error_msg, None except requests.exceptions.SSLError as e: if attempt < self.max_retries - 1: time.sleep(self.retry_delay) continue error_type, error_msg = self.analyze_problem(host, e) return error_type, error_msg, None except requests.exceptions.RequestException as e: if attempt < self.max_retries - 1: time.sleep(self.retry_delay) continue error_type, error_msg = self.analyze_problem(host, e) return error_type, error_msg, None return "MAX_RETRIES_EXCEEDED", f"Превышено максимальное количество попыток для {host}", None def process_monitor_callback(self, host: str, process: subprocess.Popen): """Callback для отслеживания завершения процесса""" def monitor(): try: stdout, stderr = process.communicate(timeout=300) if process.returncode == 0: self.logger.info(f"pt.exe успешно завершен для {host}") else: self.logger.error(f"pt.exe завершился с ошибкой для {host}. Код: {process.returncode}") if stderr: error_msg = stderr.decode('utf-8', errors='ignore').strip() if error_msg: self.logger.error(f"Ошибка pt.exe: {error_msg}") except subprocess.TimeoutExpired: self.logger.warning(f"pt.exe превысил время выполнения для {host}. Процесс будет завершен.") process.kill() finally: if host in self.running_processes: del self.running_processes[host] thread = threading.Thread(target=monitor, daemon=True) thread.start() def run_pt_executable(self, host: str, status: str, message: str, http_status: Optional[int] = None): """Асинхронный запуск .exe файла при проблемах""" if not self.pt_path or not os.path.exists(self.pt_path): self.logger.warning(f"Не удалось запустить pt.exe: файл не найден по пути {self.pt_path}") return if host in self.running_processes: self.logger.info(f"pt.exe уже запущен для {host}. Пропускаем повторный запуск.") return try: self.logger.info(f"Запуск pt.exe для хоста {host} со статусом {status}") args = [ self.pt_path, host, status, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), message ] if http_status: args.append(str(http_status)) process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.running_processes[host] = process self.process_monitor_callback(host, process) self.logger.info(f"pt.exe запущен для {host} (PID: {process.pid}). Мониторинг продолжается...") except Exception as e: self.logger.error(f"Ошибка запуска pt.exe для {host}: {e}") if host in self.running_processes: del self.running_processes[host] def should_trigger_alert(self, status: str) -> bool: """Определяем, нужно ли запускать .exe для данного статуса""" critical_errors = [ "LOCAL_NO_INTERNET", "DNS_ERROR", "HOST_UNREACHABLE", "SERVER_ERROR", "SERVICE_UNAVAILABLE", "BAD_GATEWAY", "GATEWAY_TIMEOUT", "CONNECTION_REFUSED", "TIMEOUT" ] return status in critical_errors def handle_problem(self, host: str, status: str, message: str, http_status: Optional[int] = None): """Обработка проблем с сайтом""" if status == "OK": self.logger.info(f"OK: {message}") elif status in ["REDIRECT", "CLIENT_ERROR"]: self.logger.warning(f"WARNING: {message}") else: self.logger.error(f"ERROR: {message}") if self.should_trigger_alert(status): self.run_pt_executable(host, status, message, http_status) def cleanup_processes(self): """Очистка завершенных процессов""" completed_hosts = [] for host, process in self.running_processes.items(): if process.poll() is not None: completed_hosts.append(host) for host in completed_hosts: del self.running_processes[host] def is_config_modified(self) -> bool: """Проверяет, был ли изменен конфигурационный файл""" try: current_time = time.time() if current_time - self.last_config_check < self.config_cache_seconds: return False self.last_config_check = current_time if not os.path.exists(self.config_path): return False mod_time = os.path.getmtime(self.config_path) if mod_time > self.last_config_mod_time: self.last_config_mod_time = mod_time return True return False except Exception as e: self.logger.error(f"Ошибка при проверке изменения конфига: {e}") return False def check_and_reload_config(self): """Проверяет и перезагружает конфиг если он изменился""" if self.is_config_modified(): self.logger.info("Обнаружены изменения в конфигурационном файле. Перезагружаем...") if self.load_config(force_reload=True): self.logger.info("Конфигурация перезагружена успешно!") # Применяем новую видимость окна self.setup_window_visibility() else: self.logger.error("Не удалось перезагрузить конфигурацию. Используются старые настройки.") def start_monitoring(self): """Запуск мониторинга""" self.logger.info("Запуск мониторинга сайтов...") self.logger.info("Для остановки нажмите Ctrl+C") self.logger.info("Изменения в config.ini будут применяться автоматически!") # Информация о режиме работы if not self.window_visible: self.logger.info("Программа работает в скрытом режиме. Логи сохраняются в website_monitor.log") try: cycle_count = 0 while True: cycle_count += 1 # Проверяем и перезагружаем конфиг если нужно self.check_and_reload_config() self.logger.info("--------------------------------------------------") self.logger.info(f"Цикл проверки #{cycle_count}: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") if not self.check_internet_connectivity(): self.logger.error("Нет интернета на локальной машине! Пропускаем проверку хостов.") time.sleep(self.check_interval) continue self.cleanup_processes() if self.running_processes: self.logger.info(f"Активные процессы pt.exe: {len(self.running_processes)}") all_ok = True for host in self.hosts: status, message, http_status = self.check_site(host) self.handle_problem(host, status, message, http_status) if status != "OK": all_ok = False time.sleep(1) if all_ok: self.logger.info("Все хосты доступны!") self.logger.info(f"Цикл проверки завершен. Ожидание {self.check_interval} секунд...") time.sleep(self.check_interval) except KeyboardInterrupt: self.logger.info("Мониторинг остановлен пользователем") self.cleanup_on_exit() except Exception as e: self.logger.error(f"Критическая ошибка в мониторинге: {e}") self.cleanup_on_exit() sys.exit(1) def cleanup_on_exit(self): """Очистка ресурсов при выходе""" self.logger.info("Очистка ресурсов...") if self.running_processes: self.logger.info(f"Завершение {len(self.running_processes)} активных процессов...") for host, process in self.running_processes.items(): try: if process.poll() is None: process.terminate() self.logger.info(f"Процесс для {host} завершен") except Exception as e: self.logger.error(f"Ошибка при завершении процесса для {host}: {e}") self.logger.info("Мониторинг полностью остановлен") def main(): """Основная функция""" try: monitor = WebsiteMonitor() monitor.start_monitoring() except Exception as e: print(f"Критическая ошибка при запуске: {e}") input("Нажмите Enter для выхода...") if __name__ == "__main__": main()