syscheck-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syscheck/__init__.py +2 -0
- syscheck/__main__.py +4 -0
- syscheck/cli.py +134 -0
- syscheck/cmdlang.py +109 -0
- syscheck/commands/__init__.py +8 -0
- syscheck/commands/cpu.py +45 -0
- syscheck/commands/disk.py +61 -0
- syscheck/commands/net.py +59 -0
- syscheck/commands/proc.py +35 -0
- syscheck/commands/ram.py +49 -0
- syscheck/commands/sys.py +43 -0
- syscheck/commands/temp.py +50 -0
- syscheck/commands/watch.py +115 -0
- syscheck/palette.py +30 -0
- syscheck/plugin.py +89 -0
- syscheck/plugins/__init__.py +1 -0
- syscheck/plugins/example.py +38 -0
- syscheck/providers.py +705 -0
- syscheck/screens.py +265 -0
- syscheck/shellguard.py +126 -0
- syscheck/tui.py +1086 -0
- syscheck/utils.py +111 -0
- syscheck_cli-0.2.0.dist-info/METADATA +14 -0
- syscheck_cli-0.2.0.dist-info/RECORD +27 -0
- syscheck_cli-0.2.0.dist-info/WHEEL +5 -0
- syscheck_cli-0.2.0.dist-info/entry_points.txt +2 -0
- syscheck_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Команда syscheck watch — live-мониторинг (дашборд)."""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.live import Live
|
|
6
|
+
from rich.layout import Layout
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from syscheck import providers
|
|
11
|
+
from syscheck.utils import console, bytes_to_human, seconds_to_human, make_bar, get_color_for_percent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cpu_panel() -> Panel:
|
|
15
|
+
d = providers.cpu_info()
|
|
16
|
+
colour = get_color_for_percent(d["percent"])
|
|
17
|
+
lines = [f"overall [{colour}]{d['percent']:5.1f}%[/] {make_bar(d['percent'], 18)}"]
|
|
18
|
+
if d["freq_current"]:
|
|
19
|
+
lines.append(f"freq [cyan]{d['freq_current']:.0f} MHz[/]")
|
|
20
|
+
parts = " ".join(f"[{get_color_for_percent(p)}]{p:3.0f}[/]" for p in (d["per_cpu"] or []))
|
|
21
|
+
lines.append(f"cores {parts}")
|
|
22
|
+
return Panel("\n".join(lines), title="cpu", border_style="dim")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ram_panel() -> Panel:
|
|
26
|
+
d = providers.ram_info()
|
|
27
|
+
colour = get_color_for_percent(d["percent"])
|
|
28
|
+
lines = [f"usage [{colour}]{d['percent']:5.1f}%[/] {make_bar(d['percent'], 18)}"]
|
|
29
|
+
lines.append(f"used [cyan]{bytes_to_human(d['used'])}[/] / {bytes_to_human(d['total'])}")
|
|
30
|
+
if d["swap_total"] > 0:
|
|
31
|
+
lines.append(f"swap {d['swap_percent']}%")
|
|
32
|
+
return Panel("\n".join(lines), title="ram", border_style="dim")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _disk_panel() -> Panel:
|
|
36
|
+
d = providers.disk_info()
|
|
37
|
+
lines = []
|
|
38
|
+
for disk in d["disks"]:
|
|
39
|
+
colour = get_color_for_percent(disk["percent"])
|
|
40
|
+
lines.append(
|
|
41
|
+
f"{disk['device'].ljust(6)} [{colour}]{disk['percent']:4.1f}%[/] {make_bar(disk['percent'], 12)} "
|
|
42
|
+
f"{bytes_to_human(disk['free'])} free"
|
|
43
|
+
)
|
|
44
|
+
return Panel("\n".join(lines) or "[dim]нет дисков[/]", title="disk", border_style="dim")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _net_panel() -> Panel:
|
|
48
|
+
d = providers.net_info()
|
|
49
|
+
lines = []
|
|
50
|
+
if d["io"]:
|
|
51
|
+
lines.append(f"sent [magenta]{bytes_to_human(d['io']['bytes_sent'])}[/]")
|
|
52
|
+
lines.append(f"recv [magenta]{bytes_to_human(d['io']['bytes_recv'])}[/]")
|
|
53
|
+
for i in d["interfaces"]:
|
|
54
|
+
if i["up"] and i["ipv4"]:
|
|
55
|
+
lines.append(f"[green]{i['name']}[/] {i['ipv4'][0]}")
|
|
56
|
+
return Panel("\n".join(lines), title="network", border_style="dim")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _proc_panel() -> Panel:
|
|
60
|
+
d = providers.processes(sort_by="cpu", limit=8)
|
|
61
|
+
lines = [f"{'pid':<7}{'process':<22}{'cpu':>6}{'ram':>6}"]
|
|
62
|
+
for p in d:
|
|
63
|
+
cpu = p.get("cpu_percent") or 0
|
|
64
|
+
ram = p.get("memory_percent") or 0
|
|
65
|
+
lines.append(
|
|
66
|
+
f"{p['pid']:<7}{(p['name'] or '?')[:20]:<22}"
|
|
67
|
+
f"{get_color_for_percent(cpu)}{cpu:5.1f}%[/]"
|
|
68
|
+
f"{get_color_for_percent(ram)}{ram:5.1f}%[/]"
|
|
69
|
+
)
|
|
70
|
+
return Panel("\n".join(lines), title="processes", border_style="dim")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_layout() -> Layout:
|
|
74
|
+
layout = Layout()
|
|
75
|
+
layout.split_column(Layout(name="header", size=1), Layout(name="body"))
|
|
76
|
+
layout["body"].split_column(Layout(name="top", ratio=1), Layout(name="bottom", ratio=1))
|
|
77
|
+
layout["top"].split_row(Layout(name="cpu", ratio=2), Layout(name="ram", ratio=1))
|
|
78
|
+
layout["bottom"].split_row(
|
|
79
|
+
Layout(name="disk", ratio=1), Layout(name="net", ratio=1),
|
|
80
|
+
Layout(name="procs", ratio=2), Layout(name="sys_info", ratio=1),
|
|
81
|
+
)
|
|
82
|
+
return layout
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def watch_cmd(
|
|
86
|
+
interval: int = typer.Option(2, "--interval", "-i", help="Интервал обновления (сек)"),
|
|
87
|
+
):
|
|
88
|
+
"""Live-мониторинг системы (Dashboard)."""
|
|
89
|
+
console.print("[dim]syscheck watch — ctrl+c для выхода[/]")
|
|
90
|
+
|
|
91
|
+
layout = _build_layout()
|
|
92
|
+
header = Panel(
|
|
93
|
+
Text("syscheck watch", justify="center", style="bold"),
|
|
94
|
+
style="dim",
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
with Live(layout, refresh_per_second=1, screen=True):
|
|
98
|
+
providers.cpu_info()
|
|
99
|
+
for _ in range(1200):
|
|
100
|
+
layout["header"].update(header)
|
|
101
|
+
layout["cpu"].update(_cpu_panel())
|
|
102
|
+
layout["ram"].update(_ram_panel())
|
|
103
|
+
layout["disk"].update(_disk_panel())
|
|
104
|
+
layout["net"].update(_net_panel())
|
|
105
|
+
layout["procs"].update(_proc_panel())
|
|
106
|
+
s = providers.system_info()
|
|
107
|
+
lines = [
|
|
108
|
+
f"uptime {seconds_to_human(s['uptime_seconds'])}",
|
|
109
|
+
f"node {s['node']}",
|
|
110
|
+
f"procs {len(providers.processes(sort_by='cpu', limit=1000))}",
|
|
111
|
+
]
|
|
112
|
+
layout["sys_info"].update(Panel("\n".join(lines), title="system", border_style="dim"))
|
|
113
|
+
time.sleep(interval)
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
console.print("\n[dim]мониторинг остановлен.[/]")
|
syscheck/palette.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Палитра WinMon и графические примитивы (bar)."""
|
|
2
|
+
|
|
3
|
+
BG = "#0b0d0f"
|
|
4
|
+
PANEL = "#101316"
|
|
5
|
+
BORDER = "#252a2f"
|
|
6
|
+
TEXT = "#e6e9ec"
|
|
7
|
+
MUTED = "#7d858e"
|
|
8
|
+
DIM = "#555d66"
|
|
9
|
+
ACCENT = "#8bd450"
|
|
10
|
+
WARN = "#e4b84c"
|
|
11
|
+
DANGER = "#e05b5b"
|
|
12
|
+
BLUE = "#62a8ff"
|
|
13
|
+
TRACK = "#20252a"
|
|
14
|
+
TERM_BG = "#080a0c"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def color_for(pct: float) -> str:
|
|
18
|
+
"""Цвет нагрузки: зелёный/жёлтый/красный по порогам 75/90."""
|
|
19
|
+
if pct >= 90:
|
|
20
|
+
return DANGER
|
|
21
|
+
if pct >= 75:
|
|
22
|
+
return WARN
|
|
23
|
+
return ACCENT
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bar_pct(pct: float, width: int = 26) -> str:
|
|
27
|
+
"""Горизонтальный бар █/░ с цветом нагрузки (markup)."""
|
|
28
|
+
w = int(width * min(max(pct, 0), 100) / 100)
|
|
29
|
+
c = color_for(pct)
|
|
30
|
+
return f"[{c}]{'█' * w}[/][{TRACK}]{'░' * (width - w)}[/]"
|
syscheck/plugin.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Система плагинов для syscheck."""
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional, Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
PLUGINS_DIR = Path(__file__).parent / "plugins"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PluginBase:
|
|
16
|
+
"""Базовый класс для плагинов. Наследуй от этого для создания своих команд."""
|
|
17
|
+
|
|
18
|
+
name: str = "unnamed"
|
|
19
|
+
description: str = "No description"
|
|
20
|
+
version: str = "0.1.0"
|
|
21
|
+
|
|
22
|
+
def execute(self, **kwargs) -> Any:
|
|
23
|
+
"""Выполняет команду плагина. Переопредели в дочернем классе."""
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_registry: Dict[str, PluginBase] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_plugin(plugin: PluginBase):
|
|
31
|
+
"""Регистрирует плагин в системе."""
|
|
32
|
+
_registry[plugin.name] = plugin
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_plugins() -> Dict[str, PluginBase]:
|
|
36
|
+
"""Возвращает все зарегистрированные плагины."""
|
|
37
|
+
_load_plugins_from_dir()
|
|
38
|
+
return dict(_registry)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_plugin(name: str) -> Optional[PluginBase]:
|
|
42
|
+
"""Возвращает плагин по имени."""
|
|
43
|
+
plugins = get_plugins()
|
|
44
|
+
return plugins.get(name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_plugins_from_dir():
|
|
48
|
+
"""Загружает все плагины из директории plugins/."""
|
|
49
|
+
if not PLUGINS_DIR.exists():
|
|
50
|
+
PLUGINS_DIR.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
for py_file in PLUGINS_DIR.glob("*.py"):
|
|
54
|
+
if py_file.name.startswith("_"):
|
|
55
|
+
continue
|
|
56
|
+
_load_module(py_file)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _load_module(module_path: Path):
|
|
60
|
+
"""Загружает один модуль-плагин."""
|
|
61
|
+
spec = importlib.util.spec_from_file_location(
|
|
62
|
+
f"syscheck.plugins.{module_path.stem}",
|
|
63
|
+
module_path,
|
|
64
|
+
)
|
|
65
|
+
if spec is None or spec.loader is None:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
module = importlib.util.module_from_spec(spec)
|
|
69
|
+
sys.modules[spec.name] = module
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
spec.loader.exec_module(module)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
console.print(f"[red]Plugin load error ({module_path.name}): {e}[/]")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Ищем классы-наследники PluginBase
|
|
78
|
+
for attr_name in dir(module):
|
|
79
|
+
attr = getattr(module, attr_name)
|
|
80
|
+
if (
|
|
81
|
+
isinstance(attr, type)
|
|
82
|
+
and issubclass(attr, PluginBase)
|
|
83
|
+
and attr is not PluginBase
|
|
84
|
+
):
|
|
85
|
+
try:
|
|
86
|
+
instance = attr()
|
|
87
|
+
register_plugin(instance)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
console.print(f"[red]Plugin init error ({attr_name}): {e}[/]")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Папка с пользовательскими плагинами."""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Пример плагина для syscheck.
|
|
2
|
+
|
|
3
|
+
Чтобы создать свой плагин:
|
|
4
|
+
1. Создай .py файл в папке syscheck/plugins/
|
|
5
|
+
2. Создай класс, наследующий от PluginBase
|
|
6
|
+
3. Реализуй метод execute()
|
|
7
|
+
4. Плагин автоматически подхватится при запуске syscheck
|
|
8
|
+
|
|
9
|
+
Пример:
|
|
10
|
+
syscheck plugins --list # покажет все плагины
|
|
11
|
+
syscheck plugins --run example # запустит этот плагин
|
|
12
|
+
"""
|
|
13
|
+
from syscheck.plugin import PluginBase, register_plugin
|
|
14
|
+
from syscheck.utils import console, print_value, print_ok
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExamplePlugin(PluginBase):
|
|
18
|
+
name = "example"
|
|
19
|
+
description = "Пример плагина — показывает информацию о библиотеках"
|
|
20
|
+
version = "0.1.0"
|
|
21
|
+
|
|
22
|
+
def execute(self, **kwargs):
|
|
23
|
+
import psutil
|
|
24
|
+
from importlib.metadata import version as _pkg_version
|
|
25
|
+
import typer
|
|
26
|
+
|
|
27
|
+
console.print("[dim]dependencies[/]")
|
|
28
|
+
print_value("psutil", psutil.__version__)
|
|
29
|
+
print_value("rich", _pkg_version("rich"))
|
|
30
|
+
print_value("typer", _pkg_version("typer"))
|
|
31
|
+
|
|
32
|
+
console.print()
|
|
33
|
+
print_ok("Создай свой плагин в syscheck/plugins/!")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Автоматическая регистрация при импорте
|
|
37
|
+
register_plugin(ExamplePlugin())
|
|
38
|
+
|