ndVpi 1.0.0b0__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.
- ndvpi/__init__.py +27 -0
- ndvpi/charm_enhance.py +136 -0
- ndvpi/cli.py +13 -0
- ndvpi/collquan_ext.py +111 -0
- ndvpi/critchers.py +184 -0
- ndvpi/diagnostics.py +75 -0
- ndvpi/history.py +58 -0
- ndvpi/menu.py +270 -0
- ndvpi/monitor.py +86 -0
- ndvpi/network_config.py +205 -0
- ndvpi/network_info.py +117 -0
- ndvpi/optimizer.py +113 -0
- ndvpi/polidium.py +69 -0
- ndvpi/version.py +1 -0
- ndvpi/wifi.py +126 -0
- ndvpi-1.0.0b0.dist-info/METADATA +34 -0
- ndvpi-1.0.0b0.dist-info/RECORD +20 -0
- ndvpi-1.0.0b0.dist-info/WHEEL +5 -0
- ndvpi-1.0.0b0.dist-info/entry_points.txt +2 -0
- ndvpi-1.0.0b0.dist-info/top_level.txt +1 -0
ndvpi/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""ndVpi — фоновая оптимизация сети и сканер Wi-Fi."""
|
|
2
|
+
from .version import __version__
|
|
3
|
+
from .wifi import WiFiScanner
|
|
4
|
+
from .network_info import NetworkInfo
|
|
5
|
+
from .polidium import Polidium
|
|
6
|
+
from .critchers import Critchers
|
|
7
|
+
from .network_config import NetworkConfig
|
|
8
|
+
from .monitor import NetworkMonitor
|
|
9
|
+
from .diagnostics import NetworkDiagnostics
|
|
10
|
+
from .history import SessionHistory
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"WiFiScanner",
|
|
15
|
+
"NetworkInfo",
|
|
16
|
+
"Polidium",
|
|
17
|
+
"Critchers",
|
|
18
|
+
"NetworkConfig",
|
|
19
|
+
"NetworkMonitor",
|
|
20
|
+
"NetworkDiagnostics",
|
|
21
|
+
"SessionHistory",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from . import collquan_ext # noqa: F401
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
ndvpi/charm_enhance.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Мягкая интеграция с CharmPkk: красиво, если есть; обычно, если нет."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
_charm = None
|
|
5
|
+
_c = None
|
|
6
|
+
_available = False
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from charmpkk import console as _c
|
|
10
|
+
_charm = __import__("charmpkk")
|
|
11
|
+
_available = True
|
|
12
|
+
except Exception:
|
|
13
|
+
_available = False
|
|
14
|
+
|
|
15
|
+
def has_charm(): return _available
|
|
16
|
+
def console(): return _c
|
|
17
|
+
def theme(name="tokyo_night"): return _c.get_theme(name) if _available else None
|
|
18
|
+
|
|
19
|
+
def _fg(color):
|
|
20
|
+
if not _available or not color: return None
|
|
21
|
+
if isinstance(color, str) and color.startswith("#"): return _c.fg_hex(color)
|
|
22
|
+
return color
|
|
23
|
+
|
|
24
|
+
def print_msg(text, color=None, style=None):
|
|
25
|
+
print(_c.colorize(text, fg=_fg(color), style=style)) if _available else print(text)
|
|
26
|
+
|
|
27
|
+
def banner(title, subtitle=None, color="#7aa2ff"):
|
|
28
|
+
if _available: return _c.banner(title, subtitle, fg=_fg(color))
|
|
29
|
+
return f"\n=== {title} ===\n{subtitle}\n" if subtitle else f"\n=== {title} ===\n"
|
|
30
|
+
|
|
31
|
+
def box(content, title=None, color="#7aa2ff"):
|
|
32
|
+
if _available: return _c.box(content, title=title, border_fg=_fg(color))
|
|
33
|
+
lines = content if isinstance(content, list) else [content]
|
|
34
|
+
return "\n".join([f"--- {title} ---" if title else "-"*20] + [str(l) for l in lines] + ["-"*20])
|
|
35
|
+
|
|
36
|
+
def table(headers, rows, header_color="#7aa2ff"):
|
|
37
|
+
if _available: return _c.Table(headers=headers, rows=rows, header_fg=_fg(header_color)).render()
|
|
38
|
+
all_rows = [list(headers)] + [list(r) for r in rows]
|
|
39
|
+
n_cols = len(headers)
|
|
40
|
+
widths = [max(len(str(r[i])) if i < len(r) else 0 for r in all_rows) for i in range(n_cols)]
|
|
41
|
+
lines = []
|
|
42
|
+
for i, row in enumerate(all_rows):
|
|
43
|
+
cells = [str(row[j]) if j < len(row) else "" for j in range(n_cols)]
|
|
44
|
+
lines.append(" | ".join(c.ljust(widths[j]) for j, c in enumerate(cells)))
|
|
45
|
+
if i == 0: lines.append("-+-".join("-" * w for w in widths))
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
def sparkline(values, fg="#7aa2ff"):
|
|
49
|
+
if _available: return _c.sparkline(values, fg=_fg(fg))
|
|
50
|
+
return "".join("▁▂▃▄▅▆▇█"[int(v/max(values or [1])*(7))] for v in values)
|
|
51
|
+
|
|
52
|
+
def key_value(data, key_color="#8b93a7"):
|
|
53
|
+
if _available: return _c.key_value(data, key_fg=_fg(key_color))
|
|
54
|
+
max_k = max((len(str(k)) for k in data), default=0)
|
|
55
|
+
return "\n".join(f" {str(k).ljust(max_k)}: {v}" for k, v in data.items())
|
|
56
|
+
|
|
57
|
+
def info(msg): print(_c.info(msg)) if _available else print(f"[i] {msg}")
|
|
58
|
+
def success(msg): print(_c.success(msg)) if _available else print(f"[+] {msg}")
|
|
59
|
+
def warning(msg): print(_c.warning(msg)) if _available else print(f"[!] {msg}")
|
|
60
|
+
def error(msg): print(_c.error(msg)) if _available else print(f"[x] {msg}")
|
|
61
|
+
|
|
62
|
+
def render_info(msg): return _c.info(msg) if _available else f"[i] {msg}"
|
|
63
|
+
def render_success(msg): return _c.success(msg) if _available else f"[+] {msg}"
|
|
64
|
+
def render_warning(msg): return _c.warning(msg) if _available else f"[!] {msg}"
|
|
65
|
+
def render_error(msg): return _c.error(msg) if _available else f"[x] {msg}"
|
|
66
|
+
|
|
67
|
+
def ask(prompt, default=None):
|
|
68
|
+
if _available: return _c.ask(prompt, default=default)
|
|
69
|
+
hint = f" [{default}]" if default is not None else ""
|
|
70
|
+
try: return input(f"? {prompt}{hint}: ").strip() or default
|
|
71
|
+
except (EOFError, KeyboardInterrupt): return default
|
|
72
|
+
|
|
73
|
+
def confirm(prompt, default=False):
|
|
74
|
+
if _available: return _c.confirm(prompt, default=default)
|
|
75
|
+
d = "Y/n" if default else "y/N"
|
|
76
|
+
try: v = input(f"? {prompt} ({d}): ").strip().lower()
|
|
77
|
+
except (EOFError, KeyboardInterrupt): return default
|
|
78
|
+
return v.startswith("y") if v else default
|
|
79
|
+
|
|
80
|
+
def select(title, options, color="#7aa2ff"):
|
|
81
|
+
if not options: return None
|
|
82
|
+
if _available: return _c.select(title, options, fg_cursor=_fg(color), fg_title=_fg(color))
|
|
83
|
+
print(title)
|
|
84
|
+
for i, opt in enumerate(options, 1): print(f" {i}. {opt}")
|
|
85
|
+
try:
|
|
86
|
+
idx = int(input("> ").strip()) - 1
|
|
87
|
+
if 0 <= idx < len(options): return options[idx]
|
|
88
|
+
except Exception: return None
|
|
89
|
+
|
|
90
|
+
class _NullProgress:
|
|
91
|
+
def __enter__(self): return self
|
|
92
|
+
def __exit__(self, *exc): print()
|
|
93
|
+
def update(self, current): sys.stdout.write(f"\r {current}%"); sys.stdout.flush()
|
|
94
|
+
def finish(self, msg=None): print(msg) if msg else None
|
|
95
|
+
|
|
96
|
+
def progress(total=100, label="", color="#7aa2ff"):
|
|
97
|
+
return _c.FancyProgressBar(total=total, label=label, fg=_fg(color)) if _available else _NullProgress()
|
|
98
|
+
|
|
99
|
+
class _NullSpinner:
|
|
100
|
+
def __enter__(self): return self
|
|
101
|
+
def __exit__(self, *exc): pass
|
|
102
|
+
def stop(self, final=None): print(final) if final else None
|
|
103
|
+
|
|
104
|
+
def spinner(text="Loading", color="#7aa2ff"):
|
|
105
|
+
if _available: return _c.Spinner(text, fg=_fg(color))
|
|
106
|
+
print(text + "...")
|
|
107
|
+
return _NullSpinner()
|
|
108
|
+
|
|
109
|
+
class LiveDashboard:
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self._live = None
|
|
112
|
+
self._running = False
|
|
113
|
+
self._last_lines = 0
|
|
114
|
+
def __enter__(self):
|
|
115
|
+
if _available: self._live = _c.Live(); self._live.__enter__()
|
|
116
|
+
self._running = True
|
|
117
|
+
return self
|
|
118
|
+
def update(self, content):
|
|
119
|
+
if not self._running: return
|
|
120
|
+
if _available and self._live: self._live.update(content)
|
|
121
|
+
else:
|
|
122
|
+
lines = content.split("\n")
|
|
123
|
+
if self._last_lines > 0: sys.stdout.write(f"\033[{self._last_lines}A")
|
|
124
|
+
max_lines = max(self._last_lines, len(lines))
|
|
125
|
+
for i in range(max_lines):
|
|
126
|
+
sys.stdout.write("\033[2K")
|
|
127
|
+
if i < len(lines): sys.stdout.write(lines[i])
|
|
128
|
+
sys.stdout.write("\n")
|
|
129
|
+
if max_lines > len(lines): sys.stdout.write(f"\033[{max_lines - len(lines)}A")
|
|
130
|
+
self._last_lines = len(lines)
|
|
131
|
+
sys.stdout.flush()
|
|
132
|
+
def __exit__(self, *exc):
|
|
133
|
+
self._running = False
|
|
134
|
+
if _available and self._live: self._live.__exit__(*exc)
|
|
135
|
+
else: sys.stdout.write("\n")
|
|
136
|
+
return False
|
ndvpi/cli.py
ADDED
ndvpi/collquan_ext.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Интеграция ndVpi с Collquan (CharmPkk)."""
|
|
2
|
+
try:
|
|
3
|
+
from charmpkk.terminals import CollquanShell
|
|
4
|
+
except Exception:
|
|
5
|
+
CollquanShell = None
|
|
6
|
+
|
|
7
|
+
from .wifi import WiFiScanner
|
|
8
|
+
from .network_info import NetworkInfo
|
|
9
|
+
from .polidium import Polidium
|
|
10
|
+
from .critchers import Critchers
|
|
11
|
+
from .network_config import NetworkConfig, _is_admin
|
|
12
|
+
|
|
13
|
+
_polidium = Polidium()
|
|
14
|
+
_critchers = Critchers()
|
|
15
|
+
_config = NetworkConfig()
|
|
16
|
+
|
|
17
|
+
def _cmd_scan(args):
|
|
18
|
+
data = WiFiScanner().current_network()
|
|
19
|
+
if not data: print("Нет данных о Wi-Fi."); return
|
|
20
|
+
for k, v in data.items(): print(f"{k}: {v}")
|
|
21
|
+
|
|
22
|
+
def _cmd_info(args):
|
|
23
|
+
info = NetworkInfo()
|
|
24
|
+
print(f"IP: {info.local_ip()}")
|
|
25
|
+
print(f"Шлюз: {info.gateway()}")
|
|
26
|
+
print(f"DNS: {', '.join(info.dns_servers()) or '—'}")
|
|
27
|
+
print(f"Ping: {info.ping() or '—'} ms")
|
|
28
|
+
|
|
29
|
+
def _cmd_poli(args):
|
|
30
|
+
if args and args[0] == "dns":
|
|
31
|
+
_polidium.flush_dns()
|
|
32
|
+
print("[+] DNS кэш очищен")
|
|
33
|
+
elif args and args[0] == "arp":
|
|
34
|
+
_polidium.flush_arp()
|
|
35
|
+
print("[+] ARP кэш очищен")
|
|
36
|
+
else:
|
|
37
|
+
_polidium.flush_dns()
|
|
38
|
+
_polidium.flush_arp()
|
|
39
|
+
print("[+] Polidium: кэши очищены")
|
|
40
|
+
|
|
41
|
+
def _cmd_crit(args):
|
|
42
|
+
if args and args[0] == "ram":
|
|
43
|
+
ok, msg = _critchers.clean_standby_list()
|
|
44
|
+
print(f"[{'x' if not ok else '+'}] {msg}")
|
|
45
|
+
elif args and args[0] == "tcp":
|
|
46
|
+
ok, msg = _critchers.apply_tcp_tweaks()
|
|
47
|
+
print(f"[{'x' if not ok else '+'}] {msg}")
|
|
48
|
+
elif args and args[0] == "restore":
|
|
49
|
+
ok, msg = _critchers.restore()
|
|
50
|
+
print(f"[{'x' if not ok else '+'}] {msg}")
|
|
51
|
+
else:
|
|
52
|
+
print("Использование: ndcrit <ram|tcp|restore>")
|
|
53
|
+
|
|
54
|
+
def _cmd_dns(args):
|
|
55
|
+
if not args or args[0] == "show":
|
|
56
|
+
servers = _config.get_dns()
|
|
57
|
+
print("DNS:", ", ".join(servers) if servers else "автоматически")
|
|
58
|
+
return
|
|
59
|
+
if args[0] == "set":
|
|
60
|
+
if len(args) < 2: print("Использование: nddns set <ip1> [ip2]..."); return
|
|
61
|
+
ok, msg = _config.set_dns(args[1:])
|
|
62
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
63
|
+
return
|
|
64
|
+
if args[0] == "reset":
|
|
65
|
+
ok, msg = _config.reset_dns()
|
|
66
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
67
|
+
return
|
|
68
|
+
print("Использование: nddns [show|set <ip>...|reset]")
|
|
69
|
+
|
|
70
|
+
def _cmd_pass(args):
|
|
71
|
+
if len(args) < 2: print("Использование: ndpass <ssid> <new_password>"); return
|
|
72
|
+
ssid, pwd = args[0], args[1]
|
|
73
|
+
print(f"[!] Внимание: смена пароля для '{ssid}' может разорвать соединение.")
|
|
74
|
+
if input("Продолжить? [y/N] ").strip().lower() != "y":
|
|
75
|
+
print("Отменено."); return
|
|
76
|
+
ok, msg = _config.change_wifi_password(ssid, pwd)
|
|
77
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
78
|
+
|
|
79
|
+
def _cmd_adapters(args):
|
|
80
|
+
if not args or args[0] == "list":
|
|
81
|
+
for a in _config.list_adapters(): print(f" {a.get('name', '?'):<30} {a}")
|
|
82
|
+
return
|
|
83
|
+
if args[0] in ("enable", "disable") and len(args) >= 2:
|
|
84
|
+
ok, msg = _config.set_adapter_state(args[1], enabled=(args[0] == "enable"))
|
|
85
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
86
|
+
return
|
|
87
|
+
print("Использование: ndadapters [list|enable|disable <name>]")
|
|
88
|
+
|
|
89
|
+
def _cmd_mtu(args):
|
|
90
|
+
if not args: print("Использование: ndmtu <interface> [new_mtu]"); return
|
|
91
|
+
iface = args[0]
|
|
92
|
+
if len(args) == 1:
|
|
93
|
+
mtu = _config.get_mtu(iface)
|
|
94
|
+
print(f"MTU {iface} = {mtu if mtu is not None else '—'}")
|
|
95
|
+
return
|
|
96
|
+
ok, msg = _config.set_mtu(iface, args[1])
|
|
97
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
98
|
+
|
|
99
|
+
def _cmd_admin(args):
|
|
100
|
+
print("Администратор:", "да" if _is_admin() else "нет")
|
|
101
|
+
|
|
102
|
+
if CollquanShell is not None:
|
|
103
|
+
CollquanShell.register("ndscan", "Сканировать Wi-Fi")(_cmd_scan)
|
|
104
|
+
CollquanShell.register("ndinfo", "Информация о сети")(_cmd_info)
|
|
105
|
+
CollquanShell.register("ndpoli", "Polidium: очистка DNS/ARP")(_cmd_poli)
|
|
106
|
+
CollquanShell.register("ndcrit", "Critchers: RAM/TCP буст")(_cmd_crit)
|
|
107
|
+
CollquanShell.register("nddns", "DNS: show/set/reset")(_cmd_dns)
|
|
108
|
+
CollquanShell.register("ndpass", "Сменить пароль Wi-Fi: <ssid> <pass>")(_cmd_pass)
|
|
109
|
+
CollquanShell.register("ndadapters", "Адаптеры: list/enable/disable")(_cmd_adapters)
|
|
110
|
+
CollquanShell.register("ndmtu", "MTU: <iface> [new_mtu]")(_cmd_mtu)
|
|
111
|
+
CollquanShell.register("ndadmin", "Проверить права администратора")(_cmd_admin)
|
ndvpi/critchers.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Critchers: Агрессивный сетевой бустер и очистка ресурсов (Без psutil)."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import ctypes
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
import atexit
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
BACKUP_FILE = Path.home() / ".ndvpi" / "critchers_backup.json"
|
|
12
|
+
|
|
13
|
+
def _is_windows():
|
|
14
|
+
return platform.system() == "Windows"
|
|
15
|
+
|
|
16
|
+
def _is_admin():
|
|
17
|
+
if not _is_windows(): return False
|
|
18
|
+
try:
|
|
19
|
+
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def _get_pids(process_name):
|
|
24
|
+
try:
|
|
25
|
+
cmd = f'tasklist /FI "IMAGENAME eq {process_name}" /FO CSV /NH'
|
|
26
|
+
out = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="cp866").stdout
|
|
27
|
+
pids = []
|
|
28
|
+
for line in out.splitlines():
|
|
29
|
+
if process_name.lower() in line.lower():
|
|
30
|
+
parts = line.split('","')
|
|
31
|
+
if len(parts) > 1:
|
|
32
|
+
try: pids.append(int(parts[1]))
|
|
33
|
+
except ValueError: pass
|
|
34
|
+
return pids
|
|
35
|
+
except Exception:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
def _suspend_process(pid):
|
|
39
|
+
try:
|
|
40
|
+
PROCESS_SUSPEND_RESUME = 0x0800
|
|
41
|
+
handle = ctypes.windll.kernel32.OpenProcess(PROCESS_SUSPEND_RESUME, False, pid)
|
|
42
|
+
if handle:
|
|
43
|
+
ctypes.windll.ntdll.NtSuspendProcess(handle)
|
|
44
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
45
|
+
return True
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def _resume_process(pid):
|
|
51
|
+
try:
|
|
52
|
+
PROCESS_SUSPEND_RESUME = 0x0800
|
|
53
|
+
handle = ctypes.windll.kernel32.OpenProcess(PROCESS_SUSPEND_RESUME, False, pid)
|
|
54
|
+
if handle:
|
|
55
|
+
ctypes.windll.ntdll.NtResumeProcess(handle)
|
|
56
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
57
|
+
return True
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
class Critchers:
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.backup_data = {}
|
|
65
|
+
self.suspended_pids = []
|
|
66
|
+
atexit.register(self.resume_background_downloads)
|
|
67
|
+
|
|
68
|
+
if BACKUP_FILE.exists():
|
|
69
|
+
try:
|
|
70
|
+
self.backup_data = json.loads(BACKUP_FILE.read_text("utf-8"))
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def _save_backup(self):
|
|
75
|
+
BACKUP_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
BACKUP_FILE.write_text(json.dumps(self.backup_data, indent=2), "utf-8")
|
|
77
|
+
|
|
78
|
+
def clean_standby_list(self):
|
|
79
|
+
if not _is_windows() or not _is_admin():
|
|
80
|
+
return False, "Требуются права администратора Windows"
|
|
81
|
+
try:
|
|
82
|
+
ctypes.windll.ntdll.NtSetSystemInformation(-1073744922, ctypes.byref(ctypes.c_int(4)), 4)
|
|
83
|
+
return True, "RAM Standby List очищен"
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return False, f"Ошибка RAM: {e}"
|
|
86
|
+
|
|
87
|
+
def apply_tcp_tweaks(self, dry_run=False):
|
|
88
|
+
if not _is_windows(): return False, "Только для Windows"
|
|
89
|
+
import winreg
|
|
90
|
+
tweaks = {
|
|
91
|
+
r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters": {
|
|
92
|
+
"NetworkThrottlingIndex": 0xffffffff,
|
|
93
|
+
"TcpAckFrequency": 1,
|
|
94
|
+
"TCPNoDelay": 1,
|
|
95
|
+
"TcpDelAckTicks": 0
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if dry_run:
|
|
99
|
+
return True, "DRY-RUN: Будет изменен реестр (TCP Acknowledgment, Throttling)"
|
|
100
|
+
if not _is_admin():
|
|
101
|
+
return False, "Требуются права администратора"
|
|
102
|
+
for path, values in tweaks.items():
|
|
103
|
+
try:
|
|
104
|
+
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_ALL_ACCESS)
|
|
105
|
+
for name, val in values.items():
|
|
106
|
+
try:
|
|
107
|
+
orig_val, _ = winreg.QueryValueEx(key, name)
|
|
108
|
+
self.backup_data[f"{path}\\{name}"] = orig_val
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
self.backup_data[f"{path}\\{name}"] = None
|
|
111
|
+
winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, val)
|
|
112
|
+
winreg.CloseKey(key)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return False, f"Ошибка реестра: {e}"
|
|
115
|
+
self._save_backup()
|
|
116
|
+
return True, "Твики TCP успешно применены"
|
|
117
|
+
|
|
118
|
+
def disable_wifi_power_saving(self, dry_run=False):
|
|
119
|
+
if not _is_windows(): return False, "Только для Windows"
|
|
120
|
+
if dry_run: return True, "DRY-RUN: Отключение энергосбережения Wi-Fi адаптера"
|
|
121
|
+
if not _is_admin(): return False, "Требуются права администратора"
|
|
122
|
+
ps_cmd = (
|
|
123
|
+
'Get-NetAdapter | Where-Object {$_.PhysicalMediaType -match "Native 802.11"} '
|
|
124
|
+
'| Disable-NetAdapterPowerManagement -ErrorAction SilentlyContinue'
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True)
|
|
128
|
+
return True, "Энергосбережение Wi-Fi отключено"
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return False, f"Ошибка: {e}"
|
|
131
|
+
|
|
132
|
+
def pause_background_downloads(self):
|
|
133
|
+
if not _is_windows() or not _is_admin():
|
|
134
|
+
return False, "Требуются права администратора"
|
|
135
|
+
targets = ["OneDrive.exe", "wuauclt.exe", "SysMain.exe"]
|
|
136
|
+
paused = []
|
|
137
|
+
for proc_name in targets:
|
|
138
|
+
pids = _get_pids(proc_name)
|
|
139
|
+
for pid in pids:
|
|
140
|
+
if _suspend_process(pid):
|
|
141
|
+
self.suspended_pids.append(pid)
|
|
142
|
+
paused.append(f"{proc_name} (PID: {pid})")
|
|
143
|
+
if paused:
|
|
144
|
+
return True, f"Приостановлены процессы: {', '.join(paused)}"
|
|
145
|
+
return True, "Фоновые качалки не найдены (или уже спят)"
|
|
146
|
+
|
|
147
|
+
def resume_background_downloads(self):
|
|
148
|
+
if not self.suspended_pids: return
|
|
149
|
+
resumed = []
|
|
150
|
+
for pid in self.suspended_pids:
|
|
151
|
+
if _resume_process(pid):
|
|
152
|
+
resumed.append(str(pid))
|
|
153
|
+
self.suspended_pids = []
|
|
154
|
+
if resumed:
|
|
155
|
+
print(f"[Critchers] Возобновлены процессы: {', '.join(resumed)}")
|
|
156
|
+
|
|
157
|
+
def restore(self):
|
|
158
|
+
if not _is_windows() or not _is_admin():
|
|
159
|
+
return False, "Требуются права администратора"
|
|
160
|
+
import winreg
|
|
161
|
+
restored = 0
|
|
162
|
+
for full_key, orig_val in self.backup_data.items():
|
|
163
|
+
path, name = full_key.rsplit("\\", 1)
|
|
164
|
+
try:
|
|
165
|
+
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_ALL_ACCESS)
|
|
166
|
+
if orig_val is None:
|
|
167
|
+
try: winreg.DeleteValue(key, name)
|
|
168
|
+
except FileNotFoundError: pass
|
|
169
|
+
else:
|
|
170
|
+
winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, orig_val)
|
|
171
|
+
winreg.CloseKey(key)
|
|
172
|
+
restored += 1
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
self.backup_data = {}
|
|
176
|
+
if BACKUP_FILE.exists():
|
|
177
|
+
BACKUP_FILE.unlink()
|
|
178
|
+
ps_cmd = (
|
|
179
|
+
'Get-NetAdapter | Where-Object {$_.PhysicalMediaType -match "Native 802.11"} '
|
|
180
|
+
'| Enable-NetAdapterPowerManagement -ErrorAction SilentlyContinue'
|
|
181
|
+
)
|
|
182
|
+
subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True)
|
|
183
|
+
self.resume_background_downloads()
|
|
184
|
+
return True, f"Восстановлено настроек: {restored}. Wi-Fi питание и процессы возвращены."
|
ndvpi/diagnostics.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Диагностика сети: выявление проблем и рекомендации."""
|
|
2
|
+
import socket
|
|
3
|
+
from .network_info import NetworkInfo
|
|
4
|
+
from .wifi import WiFiScanner
|
|
5
|
+
from .network_config import NetworkConfig
|
|
6
|
+
from . import charm_enhance as ui
|
|
7
|
+
|
|
8
|
+
class CheckResult:
|
|
9
|
+
def __init__(self, name, ok, details, recommendation=""):
|
|
10
|
+
self.name, self.ok, self.details, self.recommendation = name, ok, details, recommendation
|
|
11
|
+
|
|
12
|
+
class NetworkDiagnostics:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.info = NetworkInfo()
|
|
15
|
+
self.wifi = WiFiScanner()
|
|
16
|
+
self.config = NetworkConfig()
|
|
17
|
+
|
|
18
|
+
def run_checks(self):
|
|
19
|
+
results = []
|
|
20
|
+
adapters = self.config.list_adapters()
|
|
21
|
+
enabled = [a for a in adapters if a.get("state") in ("Connected", "Подключено", "up", "Подключен")]
|
|
22
|
+
if not adapters:
|
|
23
|
+
results.append(CheckResult("Адаптеры", False, "Не найдены сетевые адаптеры", "Проверьте драйверы"))
|
|
24
|
+
elif not enabled:
|
|
25
|
+
results.append(CheckResult("Адаптеры", False, "Все отключены", "Включите сетевой адаптер"))
|
|
26
|
+
else:
|
|
27
|
+
results.append(CheckResult("Адаптеры", True, f"Активен: {enabled[0].get('name', 'Неизвестно')}"))
|
|
28
|
+
|
|
29
|
+
wifi_data = self.wifi.current_network()
|
|
30
|
+
if wifi_data:
|
|
31
|
+
results.append(CheckResult("Wi-Fi", True, f"Подключено к {wifi_data.get('ssid', '?')}"))
|
|
32
|
+
else:
|
|
33
|
+
results.append(CheckResult("Wi-Fi", True, "Не активен (возможно Ethernet)"))
|
|
34
|
+
|
|
35
|
+
gw = self.info.gateway()
|
|
36
|
+
if not gw:
|
|
37
|
+
results.append(CheckResult("Шлюз", False, "Не найден", "Проверьте DHCP"))
|
|
38
|
+
else:
|
|
39
|
+
gw_ping = self.info.ping(gw, timeout=1)
|
|
40
|
+
if gw_ping is not None:
|
|
41
|
+
results.append(CheckResult("Шлюз", True, f"{gw} ({gw_ping:.0f} ms)"))
|
|
42
|
+
else:
|
|
43
|
+
results.append(CheckResult("Шлюз", False, f"{gw} не отвечает", "Перезагрузите роутер"))
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
socket.gethostbyname("google.com")
|
|
47
|
+
results.append(CheckResult("DNS", True, "Разрешение имён работает"))
|
|
48
|
+
except socket.gaierror:
|
|
49
|
+
results.append(CheckResult("DNS", False, "Домены не резолвятся", "Смените DNS на 1.1.1.1"))
|
|
50
|
+
|
|
51
|
+
ping_stats = self.info.ping_series("8.8.8.8", count=3, timeout=1)
|
|
52
|
+
if not ping_stats:
|
|
53
|
+
results.append(CheckResult("Интернет", False, "Нет доступа", "Проверьте кабель/провайдера"))
|
|
54
|
+
else:
|
|
55
|
+
if ping_stats["loss"] > 0:
|
|
56
|
+
results.append(CheckResult("Потери", False, f"Потери: {ping_stats['loss']:.1f}%", "Помехи или перегрузка канала"))
|
|
57
|
+
elif ping_stats["avg"] > 150:
|
|
58
|
+
results.append(CheckResult("Пинг", False, f"Высокий: {ping_stats['avg']:.0f} ms", "Попробуйте Critchers или Polidium"))
|
|
59
|
+
else:
|
|
60
|
+
results.append(CheckResult("Интернет", True, f"Стабильно ({ping_stats['avg']:.0f} ms)"))
|
|
61
|
+
return results
|
|
62
|
+
|
|
63
|
+
def get_report(self):
|
|
64
|
+
results = self.run_checks()
|
|
65
|
+
fails = [r for r in results if not r.ok]
|
|
66
|
+
if ui.has_charm():
|
|
67
|
+
rows = [[("✔ OK" if r.ok else "✖ ОШИБКА"), r.name, r.details or "", r.recommendation or ""] for r in results]
|
|
68
|
+
out = [ui.table(["Статус", "Проверка", "Детали", "Рекомендация"], rows), ""]
|
|
69
|
+
out.append(ui.render_warning("Обнаружены проблемы.") if fails else ui.render_success("Сеть работает стабильно."))
|
|
70
|
+
return "\n".join(out)
|
|
71
|
+
out = ["=== Диагностика сети ==="]
|
|
72
|
+
for r in results:
|
|
73
|
+
out.append(f"{'[+]' if r.ok else '[x]'} {r.name}: {r.details}")
|
|
74
|
+
if not r.ok and r.recommendation: out.append(f" -> {r.recommendation}")
|
|
75
|
+
return "\n".join(out)
|
ndvpi/history.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""История метрик сети за сессию и экспорт в JSON/CSV."""
|
|
2
|
+
import json
|
|
3
|
+
import csv
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
class SessionHistory:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.records = []
|
|
10
|
+
|
|
11
|
+
def add_ping(self, host, stats):
|
|
12
|
+
if stats:
|
|
13
|
+
self.records.append({"ts": time.time(), "type": "ping", "host": host, "data": stats})
|
|
14
|
+
|
|
15
|
+
def add_speed(self, download_mbps, upload_mbps):
|
|
16
|
+
self.records.append({"ts": time.time(), "type": "speed", "data": {"download": download_mbps, "upload": upload_mbps}})
|
|
17
|
+
|
|
18
|
+
def clear(self):
|
|
19
|
+
self.records.clear()
|
|
20
|
+
|
|
21
|
+
def summary(self):
|
|
22
|
+
pings = [r for r in self.records if r["type"] == "ping"]
|
|
23
|
+
speeds = [r for r in self.records if r["type"] == "speed"]
|
|
24
|
+
result = {"samples": len(self.records)}
|
|
25
|
+
if pings:
|
|
26
|
+
result.update({
|
|
27
|
+
"avg_ping_ms": round(sum(p["data"]["avg"] for p in pings) / len(pings), 2),
|
|
28
|
+
"avg_loss_pct": round(sum(p["data"]["loss"] for p in pings) / len(pings), 2),
|
|
29
|
+
"avg_jitter_ms": round(sum(p["data"]["jitter"] for p in pings) / len(pings), 2)
|
|
30
|
+
})
|
|
31
|
+
if speeds:
|
|
32
|
+
result.update({
|
|
33
|
+
"avg_download_mbps": round(sum(s["data"]["download"] for s in speeds) / len(speeds), 2),
|
|
34
|
+
"avg_upload_mbps": round(sum(s["data"]["upload"] for s in speeds) / len(speeds), 2)
|
|
35
|
+
})
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
def export_json(self, filepath):
|
|
39
|
+
path = Path(filepath)
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
42
|
+
json.dump(self.records, f, indent=2, ensure_ascii=False)
|
|
43
|
+
return str(path)
|
|
44
|
+
|
|
45
|
+
def export_csv(self, filepath):
|
|
46
|
+
path = Path(filepath)
|
|
47
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
49
|
+
writer = csv.writer(f)
|
|
50
|
+
writer.writerow(["Timestamp", "Type", "Min/Avg/Download", "Max/Upload", "Loss/Jitter"])
|
|
51
|
+
for r in self.records:
|
|
52
|
+
if r["type"] == "ping":
|
|
53
|
+
d = r["data"]
|
|
54
|
+
writer.writerow([time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(r["ts"])), "Ping", f"{d['avg']:.1f} ms", f"{d['max']:.1f} ms", f"{d['loss']:.1f}% / {d['jitter']:.1f} ms"])
|
|
55
|
+
elif r["type"] == "speed":
|
|
56
|
+
d = r["data"]
|
|
57
|
+
writer.writerow([time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(r["ts"])), "Speed", f"{d['download']:.1f} Mbps", f"{d['upload']:.1f} Mbps", "-"])
|
|
58
|
+
return str(path)
|