sysvis 1.1.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.
- sysvis/__init__.py +48 -0
- sysvis/__main__.py +12 -0
- sysvis/collectors/__init__.py +3 -0
- sysvis/collectors/system.py +507 -0
- sysvis/views/__init__.py +32 -0
- sysvis/views/_helpers.py +190 -0
- sysvis/views/cpu.py +105 -0
- sysvis/views/disk.py +71 -0
- sysvis/views/gpu.py +68 -0
- sysvis/views/health.py +153 -0
- sysvis/views/live.py +99 -0
- sysvis/views/memory.py +70 -0
- sysvis/views/menu.py +96 -0
- sysvis/views/network.py +99 -0
- sysvis/views/processes.py +89 -0
- sysvis/views/sysinfo.py +57 -0
- sysvis-1.1.0.dist-info/METADATA +241 -0
- sysvis-1.1.0.dist-info/RECORD +21 -0
- sysvis-1.1.0.dist-info/WHEEL +5 -0
- sysvis-1.1.0.dist-info/entry_points.txt +2 -0
- sysvis-1.1.0.dist-info/top_level.txt +1 -0
sysvis/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sysvis — Arrow-key terminal system monitor with actionable insights.
|
|
3
|
+
|
|
4
|
+
Quick start
|
|
5
|
+
-----------
|
|
6
|
+
import sysvis
|
|
7
|
+
sysvis.run() # launches arrow-key menu
|
|
8
|
+
|
|
9
|
+
# Use the collector directly:
|
|
10
|
+
from sysvis import SystemCollector
|
|
11
|
+
import time
|
|
12
|
+
c = SystemCollector()
|
|
13
|
+
time.sleep(1.5)
|
|
14
|
+
print(c.data["cpu"].percent_total)
|
|
15
|
+
c.stop()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .collectors.system import (
|
|
19
|
+
SystemCollector,
|
|
20
|
+
CPUMetrics,
|
|
21
|
+
MemoryMetrics,
|
|
22
|
+
DiskMetrics,
|
|
23
|
+
NetworkMetrics,
|
|
24
|
+
GPUMetrics,
|
|
25
|
+
ProcessMetrics,
|
|
26
|
+
SystemInfo,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__version__ = "1.1.0"
|
|
30
|
+
__author__ = "sysvis contributors"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SystemCollector",
|
|
34
|
+
"CPUMetrics",
|
|
35
|
+
"MemoryMetrics",
|
|
36
|
+
"DiskMetrics",
|
|
37
|
+
"NetworkMetrics",
|
|
38
|
+
"GPUMetrics",
|
|
39
|
+
"ProcessMetrics",
|
|
40
|
+
"SystemInfo",
|
|
41
|
+
"run",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run():
|
|
46
|
+
"""Launch the arrow-key interactive menu."""
|
|
47
|
+
from .views.menu import run as _run
|
|
48
|
+
_run()
|
sysvis/__main__.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sysvis/collectors/system.py
|
|
3
|
+
----------------------------
|
|
4
|
+
Non-blocking threaded system metrics collector.
|
|
5
|
+
Runs all psutil calls in a background thread — the menu/views
|
|
6
|
+
never block waiting for data.
|
|
7
|
+
|
|
8
|
+
CPU primed with interval=0.5 once at startup, then sampled
|
|
9
|
+
with interval=None (0 ms cost) on every subsequent call.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import socket
|
|
15
|
+
import platform
|
|
16
|
+
import subprocess
|
|
17
|
+
import time
|
|
18
|
+
import threading
|
|
19
|
+
import psutil
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import cpuinfo
|
|
24
|
+
HAS_CPUINFO = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
HAS_CPUINFO = False
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import GPUtil
|
|
30
|
+
HAS_GPUTIL = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_GPUTIL = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Dataclasses ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CPUMetrics:
|
|
39
|
+
percent_total: float = 0.0
|
|
40
|
+
percent_per_core: list = field(default_factory=list)
|
|
41
|
+
freq_current: float = 0.0
|
|
42
|
+
freq_max: float = 0.0
|
|
43
|
+
physical_cores: int = 0
|
|
44
|
+
logical_cores: int = 0
|
|
45
|
+
cpu_name: str = ""
|
|
46
|
+
load_avg_1: float = 0.0
|
|
47
|
+
load_avg_5: float = 0.0
|
|
48
|
+
load_avg_15: float = 0.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class MemoryMetrics:
|
|
53
|
+
ram_total: int = 0
|
|
54
|
+
ram_used: int = 0
|
|
55
|
+
ram_free: int = 0
|
|
56
|
+
ram_percent: float = 0.0
|
|
57
|
+
swap_total: int = 0
|
|
58
|
+
swap_used: int = 0
|
|
59
|
+
swap_percent: float = 0.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class DiskMetrics:
|
|
64
|
+
partitions: list = field(default_factory=list)
|
|
65
|
+
read_bytes: int = 0
|
|
66
|
+
write_bytes: int = 0
|
|
67
|
+
read_speed: float = 0.0
|
|
68
|
+
write_speed: float = 0.0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class NetworkMetrics:
|
|
73
|
+
interfaces: list = field(default_factory=list)
|
|
74
|
+
bytes_sent: int = 0
|
|
75
|
+
bytes_recv: int = 0
|
|
76
|
+
packets_sent: int = 0
|
|
77
|
+
packets_recv: int = 0
|
|
78
|
+
upload_speed: float = 0.0
|
|
79
|
+
download_speed: float = 0.0
|
|
80
|
+
hostname: str = ""
|
|
81
|
+
local_ip: str = ""
|
|
82
|
+
wifi_ssid: str = ""
|
|
83
|
+
wifi_ip: str = ""
|
|
84
|
+
ethernet_ip: str = ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class GPUMetrics:
|
|
89
|
+
available: bool = False
|
|
90
|
+
gpus: list = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class ProcessMetrics:
|
|
95
|
+
top_processes: list = field(default_factory=list)
|
|
96
|
+
total_processes: int = 0
|
|
97
|
+
running: int = 0
|
|
98
|
+
sleeping: int = 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SystemInfo:
|
|
103
|
+
os_name: str = ""
|
|
104
|
+
os_version: str = ""
|
|
105
|
+
os_release: str = ""
|
|
106
|
+
kernel: str = ""
|
|
107
|
+
architecture: str = ""
|
|
108
|
+
hostname: str = ""
|
|
109
|
+
uptime_seconds: float = 0.0
|
|
110
|
+
installed_apps_count: int = 0
|
|
111
|
+
installed_apps: list = field(default_factory=list)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── Collector ─────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
class SystemCollector:
|
|
117
|
+
"""
|
|
118
|
+
Continuously collects system metrics in a background thread.
|
|
119
|
+
|
|
120
|
+
Usage::
|
|
121
|
+
|
|
122
|
+
c = SystemCollector(interval=1.0)
|
|
123
|
+
time.sleep(1.5) # wait for first sample
|
|
124
|
+
data = c.data # always returns latest snapshot instantly
|
|
125
|
+
c.stop()
|
|
126
|
+
|
|
127
|
+
``data`` keys: "cpu", "memory", "disk", "network",
|
|
128
|
+
"gpu", "processes", "system"
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, interval: float = 1.0):
|
|
132
|
+
self.interval = interval
|
|
133
|
+
self._lock = threading.Lock()
|
|
134
|
+
|
|
135
|
+
# Static values resolved once
|
|
136
|
+
self._cpu_name = self._get_cpu_name()
|
|
137
|
+
self._physical_cores = psutil.cpu_count(logical=False) or 1
|
|
138
|
+
self._logical_cores = psutil.cpu_count(logical=True) or 1
|
|
139
|
+
self._hostname = socket.gethostname()
|
|
140
|
+
try:
|
|
141
|
+
self._local_ip = socket.gethostbyname(self._hostname)
|
|
142
|
+
except Exception:
|
|
143
|
+
self._local_ip = "127.0.0.1"
|
|
144
|
+
|
|
145
|
+
# Prime CPU sampler — must use a real interval once for accurate first read
|
|
146
|
+
psutil.cpu_percent(interval=0.5)
|
|
147
|
+
psutil.cpu_percent(interval=0.5, percpu=True)
|
|
148
|
+
|
|
149
|
+
# I/O baselines
|
|
150
|
+
self._prev_disk = psutil.disk_io_counters()
|
|
151
|
+
self._prev_net = psutil.net_io_counters()
|
|
152
|
+
self._prev_ts = time.monotonic()
|
|
153
|
+
|
|
154
|
+
# EMA-smoothed speeds (alpha=0.35)
|
|
155
|
+
self._ul = 0.0
|
|
156
|
+
self._dl = 0.0
|
|
157
|
+
self._rd = 0.0
|
|
158
|
+
self._wr = 0.0
|
|
159
|
+
self._EMA = 0.35
|
|
160
|
+
|
|
161
|
+
# Caches for slow operations
|
|
162
|
+
self._apps: list = []
|
|
163
|
+
self._apps_ts: float = 0.0
|
|
164
|
+
self._sys_cache: SystemInfo = SystemInfo()
|
|
165
|
+
self._sys_cache_ts: float = 0.0
|
|
166
|
+
self._wifi_ssid: str = "N/A"
|
|
167
|
+
self._wifi_ssid_ts: float = 0.0
|
|
168
|
+
|
|
169
|
+
# First blocking snapshot, then hand off to background thread
|
|
170
|
+
self._data: dict = self._collect()
|
|
171
|
+
|
|
172
|
+
self._running = True
|
|
173
|
+
self._thread = threading.Thread(target=self._loop, daemon=True)
|
|
174
|
+
self._thread.start()
|
|
175
|
+
|
|
176
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def data(self) -> dict:
|
|
180
|
+
"""Return the latest snapshot — never blocks."""
|
|
181
|
+
with self._lock:
|
|
182
|
+
return self._data
|
|
183
|
+
|
|
184
|
+
def stop(self):
|
|
185
|
+
"""Stop the background thread."""
|
|
186
|
+
self._running = False
|
|
187
|
+
|
|
188
|
+
# ── Background loop ───────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def _loop(self):
|
|
191
|
+
while self._running:
|
|
192
|
+
t0 = time.monotonic()
|
|
193
|
+
snapshot = self._collect()
|
|
194
|
+
with self._lock:
|
|
195
|
+
self._data = snapshot
|
|
196
|
+
time.sleep(max(0.0, self.interval - (time.monotonic() - t0)))
|
|
197
|
+
|
|
198
|
+
# ── Parallel collection ───────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def _collect(self) -> dict:
|
|
201
|
+
now = time.monotonic()
|
|
202
|
+
dt = max(now - self._prev_ts, 0.001)
|
|
203
|
+
self._prev_ts = now
|
|
204
|
+
|
|
205
|
+
results: dict = {}
|
|
206
|
+
|
|
207
|
+
def run(key, fn):
|
|
208
|
+
try:
|
|
209
|
+
results[key] = fn()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
threads = [
|
|
214
|
+
threading.Thread(target=run, args=(k, fn), daemon=True)
|
|
215
|
+
for k, fn in [
|
|
216
|
+
("cpu", self._collect_cpu),
|
|
217
|
+
("memory", self._collect_memory),
|
|
218
|
+
("disk", lambda: self._collect_disk(dt)),
|
|
219
|
+
("network", lambda: self._collect_network(dt)),
|
|
220
|
+
("gpu", self._collect_gpu),
|
|
221
|
+
("processes", self._collect_processes),
|
|
222
|
+
("system", self._collect_system),
|
|
223
|
+
]
|
|
224
|
+
]
|
|
225
|
+
for t in threads: t.start()
|
|
226
|
+
for t in threads: t.join(timeout=3.0)
|
|
227
|
+
|
|
228
|
+
# Fill any failed keys with empty dataclasses
|
|
229
|
+
defaults = {
|
|
230
|
+
"cpu": CPUMetrics(), "memory": MemoryMetrics(),
|
|
231
|
+
"disk": DiskMetrics(), "network": NetworkMetrics(),
|
|
232
|
+
"gpu": GPUMetrics(), "processes": ProcessMetrics(),
|
|
233
|
+
"system": SystemInfo(),
|
|
234
|
+
}
|
|
235
|
+
for k, v in defaults.items():
|
|
236
|
+
results.setdefault(k, v)
|
|
237
|
+
return results
|
|
238
|
+
|
|
239
|
+
# ── Individual collectors ─────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
def _collect_cpu(self) -> CPUMetrics:
|
|
242
|
+
m = CPUMetrics()
|
|
243
|
+
m.cpu_name = self._cpu_name
|
|
244
|
+
m.physical_cores = self._physical_cores
|
|
245
|
+
m.logical_cores = self._logical_cores
|
|
246
|
+
m.percent_total = psutil.cpu_percent(interval=0.1)
|
|
247
|
+
m.percent_per_core = psutil.cpu_percent(interval=0.1, percpu=True)
|
|
248
|
+
freq = psutil.cpu_freq()
|
|
249
|
+
if freq:
|
|
250
|
+
m.freq_current = round(freq.current, 1)
|
|
251
|
+
m.freq_max = round(freq.max, 1)
|
|
252
|
+
try:
|
|
253
|
+
la = os.getloadavg()
|
|
254
|
+
m.load_avg_1, m.load_avg_5, m.load_avg_15 = la
|
|
255
|
+
except (AttributeError, OSError):
|
|
256
|
+
pass # not available on Windows
|
|
257
|
+
return m
|
|
258
|
+
|
|
259
|
+
def _collect_memory(self) -> MemoryMetrics:
|
|
260
|
+
m = MemoryMetrics()
|
|
261
|
+
vm = psutil.virtual_memory()
|
|
262
|
+
sw = psutil.swap_memory()
|
|
263
|
+
m.ram_total, m.ram_used = vm.total, vm.used
|
|
264
|
+
m.ram_free, m.ram_percent = vm.available, vm.percent
|
|
265
|
+
m.swap_total, m.swap_used = sw.total, sw.used
|
|
266
|
+
m.swap_percent = sw.percent
|
|
267
|
+
return m
|
|
268
|
+
|
|
269
|
+
def _collect_disk(self, dt: float) -> DiskMetrics:
|
|
270
|
+
m = DiskMetrics()
|
|
271
|
+
parts = []
|
|
272
|
+
for p in psutil.disk_partitions(all=False):
|
|
273
|
+
try:
|
|
274
|
+
u = psutil.disk_usage(p.mountpoint)
|
|
275
|
+
parts.append({
|
|
276
|
+
"device": p.device, "mountpoint": p.mountpoint,
|
|
277
|
+
"fstype": p.fstype, "total": u.total,
|
|
278
|
+
"used": u.used, "free": u.free,
|
|
279
|
+
"percent": u.percent,
|
|
280
|
+
})
|
|
281
|
+
except (PermissionError, OSError):
|
|
282
|
+
continue
|
|
283
|
+
m.partitions = parts
|
|
284
|
+
curr = psutil.disk_io_counters()
|
|
285
|
+
if curr and self._prev_disk:
|
|
286
|
+
self._rd = self._ema(self._rd,
|
|
287
|
+
(curr.read_bytes - self._prev_disk.read_bytes) / dt)
|
|
288
|
+
self._wr = self._ema(self._wr,
|
|
289
|
+
(curr.write_bytes - self._prev_disk.write_bytes) / dt)
|
|
290
|
+
m.read_bytes = curr.read_bytes
|
|
291
|
+
m.write_bytes = curr.write_bytes
|
|
292
|
+
m.read_speed = self._rd
|
|
293
|
+
m.write_speed = self._wr
|
|
294
|
+
self._prev_disk = curr
|
|
295
|
+
return m
|
|
296
|
+
|
|
297
|
+
def _collect_network(self, dt: float) -> NetworkMetrics:
|
|
298
|
+
m = NetworkMetrics()
|
|
299
|
+
m.hostname = self._hostname
|
|
300
|
+
m.local_ip = self._local_ip
|
|
301
|
+
curr = psutil.net_io_counters()
|
|
302
|
+
self._ul = self._ema(self._ul,
|
|
303
|
+
(curr.bytes_sent - self._prev_net.bytes_sent) / dt)
|
|
304
|
+
self._dl = self._ema(self._dl,
|
|
305
|
+
(curr.bytes_recv - self._prev_net.bytes_recv) / dt)
|
|
306
|
+
m.upload_speed = self._ul
|
|
307
|
+
m.download_speed = self._dl
|
|
308
|
+
m.bytes_sent = curr.bytes_sent
|
|
309
|
+
m.bytes_recv = curr.bytes_recv
|
|
310
|
+
m.packets_sent = curr.packets_sent
|
|
311
|
+
m.packets_recv = curr.packets_recv
|
|
312
|
+
self._prev_net = curr
|
|
313
|
+
|
|
314
|
+
addrs = psutil.net_if_addrs()
|
|
315
|
+
stats = psutil.net_if_stats()
|
|
316
|
+
ifaces = []
|
|
317
|
+
for name, addr_list in addrs.items():
|
|
318
|
+
info = {"name": name, "ipv4": "", "ipv6": "",
|
|
319
|
+
"mac": "", "is_up": False, "speed": 0}
|
|
320
|
+
for a in addr_list:
|
|
321
|
+
if a.family == socket.AF_INET: info["ipv4"] = a.address
|
|
322
|
+
elif a.family == socket.AF_INET6: info["ipv6"] = a.address.split("%")[0]
|
|
323
|
+
elif a.family == psutil.AF_LINK: info["mac"] = a.address
|
|
324
|
+
if name in stats:
|
|
325
|
+
info["is_up"] = stats[name].isup
|
|
326
|
+
info["speed"] = stats[name].speed
|
|
327
|
+
ifaces.append(info)
|
|
328
|
+
lo = name.lower()
|
|
329
|
+
if any(k in lo for k in ("eth","en0","en1","enp","ethernet")):
|
|
330
|
+
m.ethernet_ip = info["ipv4"]
|
|
331
|
+
if any(k in lo for k in ("wlan","wi-fi","wifi","wlp","wlo")):
|
|
332
|
+
m.wifi_ip = info["ipv4"]
|
|
333
|
+
m.interfaces = ifaces
|
|
334
|
+
|
|
335
|
+
now = time.monotonic()
|
|
336
|
+
if now - self._wifi_ssid_ts > 15:
|
|
337
|
+
self._wifi_ssid = self._get_wifi_ssid()
|
|
338
|
+
self._wifi_ssid_ts = now
|
|
339
|
+
m.wifi_ssid = self._wifi_ssid
|
|
340
|
+
return m
|
|
341
|
+
|
|
342
|
+
def _collect_gpu(self) -> GPUMetrics:
|
|
343
|
+
m = GPUMetrics()
|
|
344
|
+
if not HAS_GPUTIL:
|
|
345
|
+
return m
|
|
346
|
+
try:
|
|
347
|
+
gpus = GPUtil.getGPUs()
|
|
348
|
+
m.available = bool(gpus)
|
|
349
|
+
m.gpus = [{
|
|
350
|
+
"id": g.id, "name": g.name,
|
|
351
|
+
"load": g.load * 100,
|
|
352
|
+
"mem_used": g.memoryUsed, "mem_total": g.memoryTotal,
|
|
353
|
+
"mem_percent": (g.memoryUsed / g.memoryTotal * 100)
|
|
354
|
+
if g.memoryTotal else 0,
|
|
355
|
+
"temperature": g.temperature,
|
|
356
|
+
} for g in gpus]
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
return m
|
|
360
|
+
|
|
361
|
+
def _collect_processes(self, top_n: int = 15) -> ProcessMetrics:
|
|
362
|
+
m = ProcessMetrics()
|
|
363
|
+
procs, running, sleeping = [], 0, 0
|
|
364
|
+
for p in psutil.process_iter(
|
|
365
|
+
["pid","name","cpu_percent","memory_percent","status","username"]):
|
|
366
|
+
try:
|
|
367
|
+
info = p.info
|
|
368
|
+
procs.append(info)
|
|
369
|
+
s = info.get("status", "")
|
|
370
|
+
if s == "running": running += 1
|
|
371
|
+
elif s in ("sleeping", "idle"): sleeping += 1
|
|
372
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
373
|
+
continue
|
|
374
|
+
m.total_processes = len(procs)
|
|
375
|
+
m.running = running
|
|
376
|
+
m.sleeping = sleeping
|
|
377
|
+
procs.sort(key=lambda x: x.get("cpu_percent") or 0, reverse=True)
|
|
378
|
+
m.top_processes = procs[:top_n]
|
|
379
|
+
return m
|
|
380
|
+
|
|
381
|
+
def _collect_system(self) -> SystemInfo:
|
|
382
|
+
now = time.monotonic()
|
|
383
|
+
if now - self._sys_cache_ts > 60:
|
|
384
|
+
s = SystemInfo()
|
|
385
|
+
s.os_name = platform.system()
|
|
386
|
+
s.os_version = platform.version()
|
|
387
|
+
s.os_release = platform.release()
|
|
388
|
+
s.kernel = platform.uname().version[:80]
|
|
389
|
+
s.architecture = platform.machine()
|
|
390
|
+
s.hostname = self._hostname
|
|
391
|
+
if now - self._apps_ts > 60:
|
|
392
|
+
self._apps = self._get_installed_apps()
|
|
393
|
+
self._apps_ts = now
|
|
394
|
+
s.installed_apps = self._apps
|
|
395
|
+
s.installed_apps_count = len(self._apps)
|
|
396
|
+
self._sys_cache = s
|
|
397
|
+
self._sys_cache_ts = now
|
|
398
|
+
self._sys_cache.uptime_seconds = time.time() - psutil.boot_time()
|
|
399
|
+
return self._sys_cache
|
|
400
|
+
|
|
401
|
+
# ── Utilities ─────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
def _ema(self, prev: float, new: float) -> float:
|
|
404
|
+
return self._EMA * new + (1 - self._EMA) * prev
|
|
405
|
+
|
|
406
|
+
def _get_cpu_name(self) -> str:
|
|
407
|
+
if HAS_CPUINFO:
|
|
408
|
+
try:
|
|
409
|
+
return cpuinfo.get_cpu_info().get("brand_raw", "") \
|
|
410
|
+
or platform.processor()
|
|
411
|
+
except Exception:
|
|
412
|
+
pass
|
|
413
|
+
return platform.processor()
|
|
414
|
+
|
|
415
|
+
def _get_wifi_ssid(self) -> str:
|
|
416
|
+
try:
|
|
417
|
+
if sys.platform == "win32":
|
|
418
|
+
out = subprocess.check_output(
|
|
419
|
+
["netsh","wlan","show","interfaces"],
|
|
420
|
+
stderr=subprocess.DEVNULL, timeout=3
|
|
421
|
+
).decode(errors="ignore")
|
|
422
|
+
for line in out.splitlines():
|
|
423
|
+
if "SSID" in line and "BSSID" not in line:
|
|
424
|
+
return line.split(":",1)[-1].strip()
|
|
425
|
+
elif sys.platform == "darwin":
|
|
426
|
+
out = subprocess.check_output(
|
|
427
|
+
["/System/Library/PrivateFrameworks/Apple80211.framework"
|
|
428
|
+
"/Versions/Current/Resources/airport","-I"],
|
|
429
|
+
stderr=subprocess.DEVNULL, timeout=3
|
|
430
|
+
).decode(errors="ignore")
|
|
431
|
+
for line in out.splitlines():
|
|
432
|
+
if " SSID:" in line:
|
|
433
|
+
return line.split(":",1)[-1].strip()
|
|
434
|
+
else:
|
|
435
|
+
for cmd in [["iwgetid","-r"],
|
|
436
|
+
["nmcli","-t","-f","active,ssid","dev","wifi"]]:
|
|
437
|
+
try:
|
|
438
|
+
out = subprocess.check_output(
|
|
439
|
+
cmd, stderr=subprocess.DEVNULL, timeout=3
|
|
440
|
+
).decode(errors="ignore").strip()
|
|
441
|
+
if out:
|
|
442
|
+
return out.split("yes:")[-1].strip() \
|
|
443
|
+
if "yes:" in out else out.splitlines()[0]
|
|
444
|
+
except FileNotFoundError:
|
|
445
|
+
continue
|
|
446
|
+
except Exception:
|
|
447
|
+
pass
|
|
448
|
+
return "N/A"
|
|
449
|
+
|
|
450
|
+
def _get_installed_apps(self) -> list:
|
|
451
|
+
apps = []
|
|
452
|
+
try:
|
|
453
|
+
if sys.platform == "win32":
|
|
454
|
+
import winreg
|
|
455
|
+
keys = [
|
|
456
|
+
(winreg.HKEY_LOCAL_MACHINE,
|
|
457
|
+
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
|
|
458
|
+
(winreg.HKEY_LOCAL_MACHINE,
|
|
459
|
+
r"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
|
|
460
|
+
(winreg.HKEY_CURRENT_USER,
|
|
461
|
+
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
|
|
462
|
+
]
|
|
463
|
+
for hive, sub in keys:
|
|
464
|
+
try:
|
|
465
|
+
key = winreg.OpenKey(hive, sub)
|
|
466
|
+
for i in range(winreg.QueryInfoKey(key)[0]):
|
|
467
|
+
try:
|
|
468
|
+
sk = winreg.OpenKey(key, winreg.EnumKey(key, i))
|
|
469
|
+
name = winreg.QueryValueEx(sk,"DisplayName")[0]
|
|
470
|
+
ver = ""
|
|
471
|
+
try: ver = winreg.QueryValueEx(sk,"DisplayVersion")[0]
|
|
472
|
+
except Exception: pass
|
|
473
|
+
if name: apps.append({"name": name, "version": ver})
|
|
474
|
+
except Exception: continue
|
|
475
|
+
except Exception: continue
|
|
476
|
+
elif sys.platform == "darwin":
|
|
477
|
+
out = subprocess.check_output(
|
|
478
|
+
["ls","/Applications"],
|
|
479
|
+
stderr=subprocess.DEVNULL, timeout=5
|
|
480
|
+
).decode(errors="ignore")
|
|
481
|
+
for line in out.strip().splitlines():
|
|
482
|
+
if line.endswith(".app"):
|
|
483
|
+
apps.append({"name": line[:-4], "version": ""})
|
|
484
|
+
else:
|
|
485
|
+
for cmd, parser in [
|
|
486
|
+
(["dpkg","--get-selections"],
|
|
487
|
+
lambda l: l.split()[0] if "install" in l else None),
|
|
488
|
+
(["rpm","-qa","--queryformat","%{NAME} %{VERSION}\n"],
|
|
489
|
+
lambda l: l.split()[0] if l.strip() else None),
|
|
490
|
+
(["pacman","-Q"],
|
|
491
|
+
lambda l: l.split()[0] if l.strip() else None),
|
|
492
|
+
]:
|
|
493
|
+
try:
|
|
494
|
+
out = subprocess.check_output(
|
|
495
|
+
cmd, stderr=subprocess.DEVNULL, timeout=10
|
|
496
|
+
).decode(errors="ignore")
|
|
497
|
+
for line in out.strip().splitlines():
|
|
498
|
+
name = parser(line)
|
|
499
|
+
if name: apps.append({"name": name, "version": ""})
|
|
500
|
+
break
|
|
501
|
+
except (FileNotFoundError,
|
|
502
|
+
subprocess.CalledProcessError,
|
|
503
|
+
subprocess.TimeoutExpired):
|
|
504
|
+
continue
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
return apps
|
sysvis/views/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sysvis.views — individual insight views (public API).
|
|
3
|
+
|
|
4
|
+
Each function takes a SystemCollector and runs until q/Esc.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
from sysvis import SystemCollector
|
|
9
|
+
from sysvis.views import cpu, health
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
c = SystemCollector()
|
|
13
|
+
time.sleep(1.5)
|
|
14
|
+
cpu(c) # live CPU view
|
|
15
|
+
health(c) # health report
|
|
16
|
+
c.stop()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .cpu import cpu
|
|
20
|
+
from .memory import memory
|
|
21
|
+
from .disk import disk
|
|
22
|
+
from .network import network
|
|
23
|
+
from .processes import processes
|
|
24
|
+
from .gpu import gpu
|
|
25
|
+
from .sysinfo import sysinfo
|
|
26
|
+
from .live import live
|
|
27
|
+
from .health import health
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"cpu", "memory", "disk", "network",
|
|
31
|
+
"processes", "gpu", "sysinfo", "live", "health",
|
|
32
|
+
]
|