- Add core module with SMART data parsing and health calculation - Add CLI with Rich-based terminal UI and health bar visualization - Add GUI with PyQt6 tabs for summary and detailed views - Support multiple health indicators (ID 231, 169, 233) for different SSD manufacturers - Add bilingual support (Russian/English) with auto-detection - Add GitHub Actions workflow for building binaries on Linux, Windows, macOS - Calculate health based on reallocated sectors, pending sectors, SSD life, and more Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
245 lines
8.4 KiB
Python
245 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Мониторинг здоровья дисков с S.M.A.R.T (аналог CrystalDiskInfo)
|
||
Требует: sudo smartctl (smartmontools)
|
||
"""
|
||
|
||
import subprocess
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
def check_smartctl():
|
||
"""Проверка наличия smartctl"""
|
||
try:
|
||
subprocess.run(['which', 'smartctl'], capture_output=True, check=True)
|
||
except subprocess.CalledProcessError:
|
||
print("❌ smartmontools не установлен!")
|
||
print("Установите: sudo pacman -S smartmontools")
|
||
sys.exit(1)
|
||
|
||
def get_disks():
|
||
"""Получить список дисков"""
|
||
try:
|
||
result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME'],
|
||
capture_output=True, text=True, check=True)
|
||
return [f"/dev/{disk}" for disk in result.stdout.strip().split('\n') if disk]
|
||
except:
|
||
return []
|
||
|
||
def get_disk_info(disk):
|
||
"""Получить информацию о диске"""
|
||
try:
|
||
model = subprocess.run(['lsblk', '-d', '-n', '-o', 'MODEL', disk],
|
||
capture_output=True, text=True).stdout.strip() or "Unknown"
|
||
size = subprocess.run(['lsblk', '-d', '-n', '-o', 'SIZE', disk],
|
||
capture_output=True, text=True).stdout.strip() or "Unknown"
|
||
return model, size
|
||
except:
|
||
return "Unknown", "Unknown"
|
||
|
||
def parse_smart_data(disk):
|
||
"""Парсить S.M.A.R.T данные"""
|
||
try:
|
||
result = subprocess.run(['sudo', 'smartctl', '-a', disk],
|
||
capture_output=True, text=True)
|
||
|
||
output = result.stdout
|
||
data = {
|
||
'status': 'GOOD' if 'PASSED' in output else ('BAD' if 'FAILED' in output else 'UNKNOWN'),
|
||
'temp': 'N/A',
|
||
'power_hours': 'N/A',
|
||
'power_cycles': 'N/A',
|
||
'reallocated': 0,
|
||
'pending': 0,
|
||
'uncorrectable': 0,
|
||
'attrs': {}
|
||
}
|
||
|
||
for line in output.split('\n'):
|
||
parts = line.split()
|
||
if len(parts) < 10:
|
||
continue
|
||
|
||
if '194' in parts[0] or 'Temperature_Celsius' in line:
|
||
try:
|
||
data['temp'] = f"{parts[9]}°C"
|
||
except:
|
||
pass
|
||
|
||
if '9' in parts[0] or 'Power_On_Hours' in line:
|
||
try:
|
||
hours = int(parts[9])
|
||
data['power_hours'] = f"{hours}h ({hours//24}d)"
|
||
except:
|
||
pass
|
||
|
||
if '12' in parts[0] or 'Power_Cycle_Count' in line:
|
||
try:
|
||
data['power_cycles'] = parts[9]
|
||
except:
|
||
pass
|
||
|
||
if '5' in parts[0] or 'Reallocated_Sector_Ct' in line:
|
||
try:
|
||
data['reallocated'] = int(parts[9])
|
||
except:
|
||
pass
|
||
|
||
if '197' in parts[0] or 'Current_Pending_Sect' in line:
|
||
try:
|
||
data['pending'] = int(parts[9])
|
||
except:
|
||
pass
|
||
|
||
if '198' in parts[0] or 'Offline_Uncorrectable' in line:
|
||
try:
|
||
data['uncorrectable'] = int(parts[9])
|
||
except:
|
||
pass
|
||
|
||
if parts and parts[0].isdigit() and len(parts) >= 10:
|
||
try:
|
||
attr_id = parts[0]
|
||
attr_name = parts[1] if len(parts) > 1 else 'Unknown'
|
||
data['attrs'][attr_id] = {
|
||
'name': attr_name,
|
||
'value': parts[3],
|
||
'worst': parts[4],
|
||
'threshold': parts[5],
|
||
'raw': parts[9]
|
||
}
|
||
except:
|
||
pass
|
||
|
||
return data
|
||
except:
|
||
return None
|
||
|
||
def calculate_health(smart_data):
|
||
"""Правильный расчёт здоровья диска"""
|
||
if not smart_data:
|
||
return 50, []
|
||
|
||
if smart_data['status'] == 'BAD':
|
||
return 5, ["🔴 S.M.A.R.T статус: BAD"]
|
||
|
||
health = 100
|
||
warnings = []
|
||
|
||
reallocated = smart_data['reallocated']
|
||
pending = smart_data['pending']
|
||
uncorrectable = smart_data['uncorrectable']
|
||
|
||
# === ПЕРЕНАЗНАЧЕННЫЕ СЕКТОРА ===
|
||
if reallocated > 0:
|
||
if reallocated > 500:
|
||
penalty = min(80, reallocated * 0.5)
|
||
health -= penalty
|
||
warnings.append(f"🔴 КРИТИЧНО: {reallocated} переназначенных секторов! Диск может отказать!")
|
||
elif reallocated > 100:
|
||
penalty = min(70, reallocated * 0.3)
|
||
health -= penalty
|
||
warnings.append(f"🟠 ВНИМАНИЕ: {reallocated} переназначенных секторов. Начните резервное копирование!")
|
||
elif reallocated > 10:
|
||
penalty = reallocated * 0.2
|
||
health -= penalty
|
||
warnings.append(f"🟡 ВНИМАНИЕ: {reallocated} переназначенных секторов")
|
||
else:
|
||
health -= reallocated * 0.1
|
||
|
||
# === ОЖИДАЮЩИЕ СЕКТОРА ===
|
||
if pending > 0:
|
||
health -= min(70, pending * 2)
|
||
warnings.append(f"🔴 КРИТИЧНО: {pending} ожидающих секторов!")
|
||
|
||
# === НЕИСПРАВИМЫЕ ОШИБКИ ===
|
||
if uncorrectable > 0:
|
||
health -= min(80, uncorrectable * 5)
|
||
warnings.append(f"🔴 КРИТИЧНО: {uncorrectable} неисправимых ошибок!")
|
||
|
||
return max(5, int(health)), warnings
|
||
|
||
def get_color(health):
|
||
"""Цвет в зависимости от процента"""
|
||
if health >= 85:
|
||
return '\033[92m' # Зеленый
|
||
elif health >= 60:
|
||
return '\033[93m' # Желтый
|
||
elif health >= 30:
|
||
return '\033[91m' # Красный
|
||
else:
|
||
return '\033[41m' # Красный фон
|
||
|
||
def get_icon(health):
|
||
"""Иконка здоровья"""
|
||
if health >= 85:
|
||
return "✓"
|
||
elif health >= 60:
|
||
return "⚠"
|
||
elif health >= 30:
|
||
return "✗"
|
||
else:
|
||
return "💥"
|
||
|
||
def print_disk(disk, model, size, health, warnings, smart_data):
|
||
"""Вывести информацию о диске"""
|
||
color = get_color(health)
|
||
icon = get_icon(health)
|
||
reset = '\033[0m'
|
||
|
||
print(f"\n{'='*75}")
|
||
print(f"Диск: {disk:15} Модель: {model:25} Размер: {size}")
|
||
|
||
bar_len = 40
|
||
filled = int(bar_len * health / 100)
|
||
bar = '█' * filled + '░' * (bar_len - filled)
|
||
print(f"Здоровье: {color}{icon} {bar} {health}%{reset}")
|
||
|
||
if warnings:
|
||
print("\n⚠️ ПРЕДУПРЕЖДЕНИЯ:")
|
||
for warning in warnings:
|
||
print(f" {warning}")
|
||
|
||
if not smart_data:
|
||
print("S.M.A.R.T: Не поддерживается")
|
||
return
|
||
|
||
print(f"\nСтатус: {smart_data['status']:8} | Температура: {smart_data['temp']:8} | "
|
||
f"Часов: {smart_data['power_hours']:15} | Циклов: {smart_data['power_cycles']}")
|
||
|
||
print(f"\nКритические атрибуты:")
|
||
print(f" • Переназначенные сектора: {smart_data['reallocated']}")
|
||
print(f" • Ожидающие сектора: {smart_data['pending']}")
|
||
print(f" • Неисправимые ошибки: {smart_data['uncorrectable']}")
|
||
|
||
def main():
|
||
"""Главная функция"""
|
||
check_smartctl()
|
||
|
||
print("\n" + "="*75)
|
||
print(f" МОНИТОРИНГ ЗДОРОВЬЯ ДИСКОВ (S.M.A.R.T)")
|
||
print(f" {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
|
||
print("="*75)
|
||
|
||
disks = get_disks()
|
||
|
||
if not disks:
|
||
print("❌ Диски не найдены")
|
||
return
|
||
|
||
for disk in disks:
|
||
model, size = get_disk_info(disk)
|
||
smart_data = parse_smart_data(disk)
|
||
health, warnings = calculate_health(smart_data)
|
||
print_disk(disk, model, size, health, warnings, smart_data)
|
||
|
||
print("\n" + "="*75 + "\n")
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except KeyboardInterrupt:
|
||
print("\n\nПрограмма прервана")
|
||
except Exception as e:
|
||
print(f"❌ Ошибка: {e}")
|