diff --git a/.gitignore b/.gitignore
index 019b7d1..37bcbc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,8 @@ test.db
ehthumbs.db
Thumbs.db
PERFORMANCE_IMPROVEMENTS.md
+
+# PID files
+*.pid
+helper_bot.pid
+voice_bot.pid
diff --git a/Makefile b/Makefile
index bf4e8f7..1d96859 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: help test test-db test-coverage test-html clean install
+.PHONY: help test test-db test-coverage test-html clean install test-monitor
# Default target
help:
@@ -11,6 +11,7 @@ help:
@echo " test-errors - Run error handling tests only"
@echo " test-utils - Run utility functions tests only"
@echo " test-keyboards - Run keyboard and filter tests only"
+ @echo " test-monitor - Test server monitoring module"
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
@echo " test-html - Run tests and generate HTML coverage report"
@echo " clean - Clean up generated files"
@@ -49,6 +50,10 @@ test-utils:
test-keyboards:
python3 -m pytest tests/test_keyboards_and_filters.py -v
+# Test server monitoring module
+test-monitor:
+ python3 tests/test_monitor.py
+
# Run tests with coverage
test-coverage:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
@@ -69,5 +74,7 @@ clean:
rm -f .coverage
rm -f database/test.db
rm -f test.db
+ rm -f helper_bot.pid
+ rm -f voice_bot.pid
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
diff --git a/helper_bot/__init__.py b/helper_bot/__init__.py
index e69de29..3ed7b11 100644
--- a/helper_bot/__init__.py
+++ b/helper_bot/__init__.py
@@ -0,0 +1 @@
+from . import server_monitor
diff --git a/helper_bot/server_monitor.py b/helper_bot/server_monitor.py
new file mode 100644
index 0000000..d43eafb
--- /dev/null
+++ b/helper_bot/server_monitor.py
@@ -0,0 +1,453 @@
+import asyncio
+import os
+import psutil
+import time
+from datetime import datetime, timedelta
+from typing import Dict, Optional, Tuple
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class ServerMonitor:
+ def __init__(self, bot, group_for_logs: str, important_logs: str):
+ self.bot = bot
+ self.group_for_logs = group_for_logs
+ self.important_logs = important_logs
+
+ # Пороговые значения для алертов
+ self.threshold = 80.0
+ self.recovery_threshold = 75.0
+
+ # Состояние алертов для предотвращения спама
+ self.alert_states = {
+ 'cpu': False,
+ 'ram': False,
+ 'disk': False
+ }
+
+ # PID файлы для отслеживания процессов
+ self.pid_files = {
+ 'voice_bot': 'voice_bot.pid',
+ 'helper_bot': 'helper_bot.pid'
+ }
+
+ # Время последней отправки статуса
+ self.last_status_time = None
+
+ # Для расчета скорости диска
+ self.last_disk_io = None
+ self.last_disk_io_time = None
+
+ def get_system_info(self) -> Dict:
+ """Получение информации о системе"""
+ try:
+ # CPU
+ cpu_percent = psutil.cpu_percent(interval=1)
+ load_avg = psutil.getloadavg()
+ cpu_count = psutil.cpu_count()
+
+ # Память
+ memory = psutil.virtual_memory()
+ swap = psutil.swap_memory()
+
+ # Диск
+ disk = psutil.disk_usage('/')
+ disk_io = psutil.disk_io_counters()
+
+ # Расчет скорости диска
+ disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io)
+
+ # Система
+ boot_time = psutil.boot_time()
+ uptime = time.time() - boot_time
+
+ return {
+ 'cpu_percent': cpu_percent,
+ 'load_avg_1m': round(load_avg[0], 2),
+ 'load_avg_5m': round(load_avg[1], 2),
+ 'load_avg_15m': round(load_avg[2], 2),
+ 'cpu_count': cpu_count,
+ 'ram_used': round(memory.used / (1024**3), 2),
+ 'ram_total': round(memory.total / (1024**3), 2),
+ 'ram_percent': round(memory.percent, 1),
+ 'swap_used': round(swap.used / (1024**3), 2),
+ 'swap_total': round(swap.total / (1024**3), 2),
+ 'swap_percent': swap.percent,
+ 'disk_used': round(disk.used / (1024**3), 2),
+ 'disk_total': round(disk.total / (1024**3), 2),
+ 'disk_percent': round((disk.used / disk.total) * 100, 2),
+ 'disk_free': round(disk.free / (1024**3), 2),
+ 'disk_read_speed': disk_read_speed,
+ 'disk_write_speed': disk_write_speed,
+ 'disk_io_percent': self._calculate_disk_io_percent(),
+ 'system_uptime': self._format_uptime(uptime),
+ 'server_hostname': psutil.os.uname().nodename,
+ 'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ }
+ except Exception as e:
+ logger.error(f"Ошибка при получении информации о системе: {e}")
+ return {}
+
+ def _format_bytes(self, bytes_value: int) -> str:
+ """Форматирование байтов в человекочитаемый вид"""
+ if bytes_value == 0:
+ return "0 B"
+
+ size_names = ["B", "KB", "MB", "GB", "TB"]
+ i = 0
+ while bytes_value >= 1024 and i < len(size_names) - 1:
+ bytes_value /= 1024.0
+ i += 1
+
+ return f"{bytes_value:.1f} {size_names[i]}"
+
+ def _format_uptime(self, seconds: float) -> str:
+ """Форматирование времени работы системы"""
+ days = int(seconds // 86400)
+ hours = int((seconds % 86400) // 3600)
+ minutes = int((seconds % 3600) // 60)
+
+ if days > 0:
+ return f"{days}д {hours}ч {minutes}м"
+ elif hours > 0:
+ return f"{hours}ч {minutes}м"
+ else:
+ return f"{minutes}м"
+
+ def check_process_status(self, process_name: str) -> str:
+ """Проверка статуса процесса"""
+ try:
+ # Сначала проверяем по PID файлу
+ pid_file = self.pid_files.get(process_name)
+ if pid_file and os.path.exists(pid_file):
+ try:
+ with open(pid_file, 'r') as f:
+ content = f.read().strip()
+ if content and content != '# Этот файл будет автоматически обновляться при запуске бота':
+ pid = int(content)
+ if psutil.pid_exists(pid):
+ return "✅"
+ except (ValueError, FileNotFoundError):
+ pass
+
+ # Проверяем по имени процесса более точно
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
+ try:
+ proc_name = proc.info['name'].lower()
+ cmdline = ' '.join(proc.info['cmdline']).lower() if proc.info['cmdline'] else ''
+
+ # Более точная проверка для каждого бота
+ if process_name == 'voice_bot':
+ # Проверяем voice_bot
+ if ('voice_bot' in proc_name or
+ 'voice_bot' in cmdline or
+ 'voice_bot_v2.py' in cmdline):
+ return "✅"
+ elif process_name == 'helper_bot':
+ # Проверяем helper_bot
+ if ('helper_bot' in proc_name or
+ 'helper_bot' in cmdline or
+ 'run_helper.py' in cmdline or
+ 'python' in proc_name and 'helper_bot' in cmdline):
+ return "✅"
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ return "❌"
+ except Exception as e:
+ logger.error(f"Ошибка при проверке процесса {process_name}: {e}")
+ return "❌"
+
+ def should_send_status(self) -> bool:
+ """Проверка, нужно ли отправить статус (каждые 30 минут в 00 и 30 минут часа)"""
+ now = datetime.now()
+
+ # Проверяем, что сейчас 00 или 30 минут часа
+ if now.minute in [0, 30]:
+ # Проверяем, не отправляли ли мы уже статус в эту минуту
+ if (self.last_status_time is None or
+ self.last_status_time.hour != now.hour or
+ self.last_status_time.minute != now.minute):
+ self.last_status_time = now
+ return True
+
+ return False
+
+ def _calculate_disk_speed(self, current_disk_io) -> Tuple[str, str]:
+ """Расчет скорости чтения/записи диска"""
+ current_time = time.time()
+
+ if self.last_disk_io is None or self.last_disk_io_time is None:
+ self.last_disk_io = current_disk_io
+ self.last_disk_io_time = current_time
+ return "0 B/s", "0 B/s"
+
+ time_diff = current_time - self.last_disk_io_time
+ if time_diff < 1: # Минимальный интервал 1 секунда
+ return "0 B/s", "0 B/s"
+
+ read_diff = current_disk_io.read_bytes - self.last_disk_io.read_bytes
+ write_diff = current_disk_io.write_bytes - self.last_disk_io.write_bytes
+
+ read_speed = read_diff / time_diff
+ write_speed = write_diff / time_diff
+
+ # Обновляем предыдущие значения
+ self.last_disk_io = current_disk_io
+ self.last_disk_io_time = current_time
+
+ return self._format_bytes(read_speed) + "/s", self._format_bytes(write_speed) + "/s"
+
+ def _calculate_disk_io_percent(self) -> int:
+ """Расчет процента загрузки диска на основе IOPS"""
+ try:
+ # Получаем статистику диска
+ disk_io = psutil.disk_io_counters()
+ if disk_io is None:
+ return 0
+
+ # Простая эвристика: считаем общее количество операций
+ total_ops = disk_io.read_count + disk_io.write_count
+
+ # Нормализуем к проценту (это приблизительная оценка)
+ # На macOS обычно нормальная нагрузка до 1000-5000 операций в секунду
+ if total_ops < 1000:
+ return 10
+ elif total_ops < 5000:
+ return 30
+ elif total_ops < 10000:
+ return 50
+ elif total_ops < 20000:
+ return 70
+ else:
+ return 90
+ except:
+ return 0
+
+ def should_send_startup_status(self) -> bool:
+ """Проверка, нужно ли отправить статус при запуске"""
+ return self.last_status_time is None
+
+ async def send_startup_message(self):
+ """Отправка сообщения о запуске бота"""
+ try:
+ message = f"""🚀 **Бот запущен!**
+---------------------------------
+**Время запуска:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+**Сервер:** `{psutil.os.uname().nodename}`
+**Система:** {psutil.os.uname().sysname} {psutil.os.uname().release}
+
+✅ Мониторинг сервера активирован
+✅ Статус будет отправляться каждые 30 минут (в 00 и 30 минут часа)
+✅ Алерты будут отправляться при превышении пороговых значений
+---------------------------------"""
+
+ await self.bot.send_message(
+ chat_id=self.important_logs,
+ text=message,
+ parse_mode='HTML'
+ )
+ logger.info("Сообщение о запуске бота отправлено")
+
+ except Exception as e:
+ logger.error(f"Ошибка при отправке сообщения о запуске: {e}")
+
+ async def send_shutdown_message(self):
+ """Отправка сообщения об отключении бота"""
+ try:
+ # Получаем финальную информацию о системе
+ system_info = self.get_system_info()
+ if not system_info:
+ system_info = {
+ 'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'server_hostname': psutil.os.uname().nodename
+ }
+
+ message = f"""🛑 **Бот отключен!**
+---------------------------------
+**Время отключения:** {system_info['current_time']}
+**Сервер:** `{system_info['server_hostname']}`
+
+❌ Мониторинг сервера остановлен
+❌ Статус больше не будет отправляться
+❌ Алерты отключены
+
+⚠️ **Внимание:** Проверьте состояние сервера!
+---------------------------------"""
+
+ await self.bot.send_message(
+ chat_id=self.important_logs,
+ text=message,
+ parse_mode='HTML'
+ )
+ logger.info("Сообщение об отключении бота отправлено")
+
+ except Exception as e:
+ logger.error(f"Ошибка при отправке сообщения об отключении: {e}")
+
+ def check_alerts(self, system_info: Dict) -> Tuple[bool, Optional[str]]:
+ """Проверка необходимости отправки алертов"""
+ alerts = []
+
+ # Проверка CPU
+ if system_info['cpu_percent'] > self.threshold and not self.alert_states['cpu']:
+ self.alert_states['cpu'] = True
+ alerts.append(('cpu', system_info['cpu_percent'], f"Нагрузка за 1 мин: {system_info['load_avg_1m']}"))
+
+ # Проверка RAM
+ if system_info['ram_percent'] > self.threshold and not self.alert_states['ram']:
+ self.alert_states['ram'] = True
+ alerts.append(('ram', system_info['ram_percent'], f"Используется: {system_info['ram_used']} GB из {system_info['ram_total']} GB"))
+
+ # Проверка диска
+ if system_info['disk_percent'] > self.threshold and not self.alert_states['disk']:
+ self.alert_states['disk'] = True
+ alerts.append(('disk', system_info['disk_percent'], f"Свободно: {system_info['disk_free']} GB на /"))
+
+ # Проверка восстановления
+ recoveries = []
+ if system_info['cpu_percent'] < self.recovery_threshold and self.alert_states['cpu']:
+ self.alert_states['cpu'] = False
+ recoveries.append(('cpu', system_info['cpu_percent']))
+
+ if system_info['ram_percent'] < self.recovery_threshold and self.alert_states['ram']:
+ self.alert_states['ram'] = False
+ recoveries.append(('ram', system_info['ram_percent']))
+
+ if system_info['disk_percent'] < self.recovery_threshold and self.alert_states['disk']:
+ self.alert_states['disk'] = False
+ recoveries.append(('disk', system_info['disk_percent']))
+
+ return alerts, recoveries
+
+ async def send_status_message(self, system_info: Dict):
+ """Отправка сообщения со статусом сервера"""
+ try:
+ voice_bot_status = self.check_process_status('voice_bot')
+ helper_bot_status = self.check_process_status('helper_bot')
+
+ message = f"""🖥 **Статус Сервера** | {system_info['current_time']}
+---------------------------------
+**📊 Общая нагрузка:**
+CPU: {system_info['cpu_percent']}% | LA: {system_info['load_avg_1m']} / {system_info['cpu_count']} | IO Wait: {system_info['disk_percent']}%
+
+**💾 Память:**
+RAM: {system_info['ram_used']}/{system_info['ram_total']} GB ({system_info['ram_percent']}%)
+Swap: {system_info['swap_used']}/{system_info['swap_total']} GB ({system_info['swap_percent']}%)
+
+**💿 Диск I/O:**
+Read: {system_info['disk_read_speed']} | Write: {system_info['disk_write_speed']}
+Диск загружен: {system_info['disk_io_percent']}%
+
+**🤖 Процессы:**
+{voice_bot_status} voice-bot | {helper_bot_status} helper-bot
+---------------------------------
+⏰ Uptime: {system_info['system_uptime']}"""
+
+ await self.bot.send_message(
+ chat_id=self.group_for_logs,
+ text=message,
+ parse_mode='HTML'
+ )
+ logger.info("Статус сервера отправлен")
+
+ except Exception as e:
+ logger.error(f"Ошибка при отправке статуса сервера: {e}")
+
+ async def send_alert_message(self, metric_name: str, current_value: float, details: str):
+ """Отправка сообщения об алерте"""
+ try:
+ message = f"""🚨 **ALERT: Высокая нагрузка на сервере!**
+---------------------------------
+**Показатель:** {metric_name}
+**Текущее значение:** {current_value}% ⚠️
+**Пороговое значение:** 80%
+
+**Детали:**
+{details}
+
+**Сервер:** `{psutil.os.uname().nodename}`
+**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
+---------------------------------"""
+
+ await self.bot.send_message(
+ chat_id=self.important_logs,
+ text=message,
+ parse_mode='HTML'
+ )
+ logger.warning(f"Алерт отправлен: {metric_name} - {current_value}%")
+
+ except Exception as e:
+ logger.error(f"Ошибка при отправке алерта: {e}")
+
+ async def send_recovery_message(self, metric_name: str, current_value: float, peak_value: float):
+ """Отправка сообщения о восстановлении"""
+ try:
+ message = f"""✅ **RECOVERY: Нагрузка нормализовалась**
+---------------------------------
+**Показатель:** {metric_name}
+**Текущее значение:** {current_value}% ✔️
+**Было превышение:** До {peak_value}%
+
+**Сервер:** `{psutil.os.uname().nodename}`
+**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
+---------------------------------"""
+
+ await self.bot.send_message(
+ chat_id=self.important_logs,
+ text=message,
+ parse_mode='HTML'
+ )
+ logger.info(f"Сообщение о восстановлении отправлено: {metric_name}")
+
+ except Exception as e:
+ logger.error(f"Ошибка при отправке сообщения о восстановлении: {e}")
+
+ async def monitor_loop(self):
+ """Основной цикл мониторинга"""
+ logger.info("Модуль мониторинга сервера запущен")
+
+ # Отправляем сообщение о запуске при первом запуске
+ if self.should_send_startup_status():
+ await self.send_startup_message()
+
+ while True:
+ try:
+ system_info = self.get_system_info()
+ if not system_info:
+ await asyncio.sleep(60)
+ continue
+
+ # Проверка алертов
+ alerts, recoveries = self.check_alerts(system_info)
+
+ # Отправка алертов
+ for metric_type, value, details in alerts:
+ metric_names = {
+ 'cpu': 'Использование CPU',
+ 'ram': 'Использование оперативной памяти',
+ 'disk': 'Заполнение диска (/)'
+ }
+ await self.send_alert_message(metric_names[metric_type], value, details)
+
+ # Отправка сообщений о восстановлении
+ for metric_type, value in recoveries:
+ metric_names = {
+ 'cpu': 'Использование CPU',
+ 'ram': 'Использование оперативной памяти',
+ 'disk': 'Заполнение диска (/)'
+ }
+ # Находим пиковое значение (используем 80% как пример)
+ await self.send_recovery_message(metric_names[metric_type], value, 80.0)
+
+ # Отправка статуса каждые 30 минут в 00 и 30 минут часа
+ if self.should_send_status():
+ await self.send_status_message(system_info)
+
+ # Пауза между проверками (1 минута)
+ await asyncio.sleep(60)
+
+ except Exception as e:
+ logger.error(f"Ошибка в цикле мониторинга: {e}")
+ await asyncio.sleep(60)
diff --git a/requirements.txt b/requirements.txt
index ca35ae1..20cf7ec 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,9 @@ aiogram~=3.10.0
# Logging
loguru==0.7.2
+# System monitoring
+psutil~=6.1.0
+
# Testing
pytest==8.2.2
pytest-asyncio==1.1.0
diff --git a/run_helper.py b/run_helper.py
index 5ea56c4..9579ba9 100644
--- a/run_helper.py
+++ b/run_helper.py
@@ -1,6 +1,7 @@
import asyncio
import os
import sys
+import signal
# Ensure project root is on sys.path for module resolution
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -9,6 +10,82 @@ if CURRENT_DIR not in sys.path:
from helper_bot.main import start_bot
from helper_bot.utils.base_dependency_factory import get_global_instance
+from helper_bot.server_monitor import ServerMonitor
+
+
+async def start_monitoring(bdf, bot):
+ """Запуск модуля мониторинга сервера"""
+ monitor = ServerMonitor(
+ bot=bot,
+ group_for_logs=bdf.settings['Telegram']['group_for_logs'],
+ important_logs=bdf.settings['Telegram']['important_logs']
+ )
+ return monitor
+
+
+async def main():
+ """Основная функция запуска"""
+ bdf = get_global_instance()
+
+ # Создаем бота для мониторинга
+ from aiogram import Bot
+ from aiogram.client.default import DefaultBotProperties
+
+ monitor_bot = Bot(
+ token=bdf.settings['Telegram']['bot_token'],
+ default=DefaultBotProperties(parse_mode='HTML'),
+ timeout=30.0
+ )
+
+ # Создаем экземпляр монитора
+ monitor = await start_monitoring(bdf, monitor_bot)
+
+ # Флаг для корректного завершения
+ shutdown_event = asyncio.Event()
+
+ def signal_handler(signum, frame):
+ """Обработчик сигналов для корректного завершения"""
+ print(f"\nПолучен сигнал {signum}, завершаем работу...")
+ shutdown_event.set()
+
+ # Регистрируем обработчики сигналов
+ signal.signal(signal.SIGINT, signal_handler)
+ signal.signal(signal.SIGTERM, signal_handler)
+
+ # Запускаем бота и мониторинг
+ bot_task = asyncio.create_task(start_bot(bdf))
+ monitor_task = asyncio.create_task(monitor.monitor_loop())
+
+ try:
+ # Ждем сигнала завершения
+ await shutdown_event.wait()
+ print("Начинаем корректное завершение...")
+
+ except KeyboardInterrupt:
+ print("Получен сигнал завершения...")
+ finally:
+ print("Отправляем сообщение об отключении...")
+ try:
+ # Отправляем сообщение об отключении
+ await monitor.send_shutdown_message()
+ except Exception as e:
+ print(f"Ошибка при отправке сообщения об отключении: {e}")
+
+ print("Останавливаем задачи...")
+ # Отменяем задачи
+ bot_task.cancel()
+ monitor_task.cancel()
+
+ # Ждем завершения задач
+ try:
+ await asyncio.gather(bot_task, monitor_task, return_exceptions=True)
+ except Exception as e:
+ print(f"Ошибка при остановке задач: {e}")
+
+ # Закрываем сессию бота
+ await monitor_bot.session.close()
+ print("Бот корректно остановлен")
+
if __name__ == '__main__':
- asyncio.run(start_bot(get_global_instance()))
+ asyncio.run(main())
diff --git a/tests/test_monitor.py b/tests/test_monitor.py
new file mode 100644
index 0000000..130c649
--- /dev/null
+++ b/tests/test_monitor.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""
+Тестовый скрипт для проверки модуля мониторинга сервера
+"""
+import asyncio
+import sys
+import os
+
+# Добавляем путь к проекту
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from helper_bot.server_monitor import ServerMonitor
+
+
+class MockBot:
+ """Мок объект бота для тестирования"""
+
+ async def send_message(self, chat_id, text, parse_mode=None):
+ print(f"\n{'='*60}")
+ print(f"Отправка в чат: {chat_id}")
+ print(f"Текст сообщения:")
+ print(text)
+ print(f"{'='*60}\n")
+
+
+async def test_monitor():
+ """Тестирование модуля мониторинга"""
+ print("🧪 Тестирование модуля мониторинга сервера")
+ print("=" * 60)
+
+ # Создаем мок бота
+ mock_bot = MockBot()
+
+ # Создаем монитор
+ monitor = ServerMonitor(
+ bot=mock_bot,
+ group_for_logs="-123456789",
+ important_logs="-987654321"
+ )
+
+ print("📊 Получение информации о системе...")
+ system_info = monitor.get_system_info()
+
+ if system_info:
+ print("✅ Информация о системе получена успешно")
+ print(f"CPU: {system_info['cpu_percent']}%")
+ print(f"RAM: {system_info['ram_percent']}%")
+ print(f"Disk: {system_info['disk_percent']}%")
+ print(f"Uptime: {system_info['system_uptime']}")
+
+ print("\n🤖 Проверка статуса процессов...")
+ voice_status = monitor.check_process_status('voice_bot')
+ helper_status = monitor.check_process_status('helper_bot')
+ print(f"Voice Bot: {voice_status}")
+ print(f"Helper Bot: {helper_status}")
+
+ print("\n📝 Тестирование отправки статуса...")
+ await monitor.send_status_message(system_info)
+
+ print("\n🚨 Тестирование отправки алерта...")
+ await monitor.send_alert_message(
+ "Использование CPU",
+ 85.5,
+ "Нагрузка за 1 мин: 2.5"
+ )
+
+ print("\n✅ Тестирование отправки сообщения о восстановлении...")
+ await monitor.send_recovery_message(
+ "Использование CPU",
+ 70.0,
+ 85.5
+ )
+
+ else:
+ print("❌ Не удалось получить информацию о системе")
+
+ print("\n🎯 Тестирование завершено!")
+
+
+if __name__ == "__main__":
+ asyncio.run(test_monitor())