130 lines
4.6 KiB
Python
130 lines
4.6 KiB
Python
from datetime import datetime, UTC
|
|
from time import monotonic
|
|
import platform
|
|
import socket
|
|
import subprocess
|
|
|
|
try:
|
|
import psutil
|
|
except ModuleNotFoundError: # pragma: no cover - optional runtime dependency fallback
|
|
psutil = None
|
|
|
|
|
|
class SystemService:
|
|
_last_net_bytes_sent: int | None = None
|
|
_last_net_bytes_recv: int | None = None
|
|
_last_net_sample_at: float | None = None
|
|
|
|
def _get_network_rates(self) -> tuple[float, float]:
|
|
counters = psutil.net_io_counters()
|
|
now = monotonic()
|
|
|
|
if (
|
|
self.__class__._last_net_sample_at is None
|
|
or self.__class__._last_net_bytes_sent is None
|
|
or self.__class__._last_net_bytes_recv is None
|
|
):
|
|
self.__class__._last_net_bytes_sent = counters.bytes_sent
|
|
self.__class__._last_net_bytes_recv = counters.bytes_recv
|
|
self.__class__._last_net_sample_at = now
|
|
return 0.0, 0.0
|
|
|
|
elapsed = max(now - self.__class__._last_net_sample_at, 1e-6)
|
|
upload_bps = max(counters.bytes_sent - self.__class__._last_net_bytes_sent, 0) / elapsed
|
|
download_bps = max(counters.bytes_recv - self.__class__._last_net_bytes_recv, 0) / elapsed
|
|
|
|
self.__class__._last_net_bytes_sent = counters.bytes_sent
|
|
self.__class__._last_net_bytes_recv = counters.bytes_recv
|
|
self.__class__._last_net_sample_at = now
|
|
|
|
return round(upload_bps, 1), round(download_bps, 1)
|
|
|
|
def _get_gpu_status(self) -> dict:
|
|
empty = {
|
|
'gpu_name': None,
|
|
'gpu_memory_total_mb': None,
|
|
'gpu_memory_used_mb': None,
|
|
'gpu_util_percent': None,
|
|
}
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
'nvidia-smi',
|
|
'--query-gpu=name,memory.total,memory.used,utilization.gpu',
|
|
'--format=csv,noheader,nounits',
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding='utf-8',
|
|
timeout=2,
|
|
check=False,
|
|
)
|
|
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
|
return empty
|
|
|
|
if result.returncode != 0 or not result.stdout.strip():
|
|
return empty
|
|
|
|
first_line = result.stdout.strip().splitlines()[0]
|
|
parts = [part.strip() for part in first_line.split(',')]
|
|
if len(parts) < 4:
|
|
return empty
|
|
|
|
def parse_number(value: str) -> float | None:
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
return {
|
|
'gpu_name': parts[0] or None,
|
|
'gpu_memory_total_mb': parse_number(parts[1]),
|
|
'gpu_memory_used_mb': parse_number(parts[2]),
|
|
'gpu_util_percent': parse_number(parts[3]),
|
|
}
|
|
|
|
def get_status(self) -> dict:
|
|
if psutil is None:
|
|
return {
|
|
'cpu_percent': 0.0,
|
|
'memory_percent': 0.0,
|
|
'disk_percent': 0.0,
|
|
'disk_used_gb': 0.0,
|
|
'disk_total_gb': 0.0,
|
|
'network_upload_bps': 0.0,
|
|
'network_download_bps': 0.0,
|
|
'system_name': platform.system(),
|
|
'system_version': platform.version(),
|
|
'hostname': socket.gethostname(),
|
|
'uptime_seconds': 0.0,
|
|
'gpu_name': None,
|
|
'gpu_memory_total_mb': None,
|
|
'gpu_memory_used_mb': None,
|
|
'gpu_util_percent': None,
|
|
'timestamp': datetime.now(UTC).isoformat(),
|
|
}
|
|
|
|
cpu_percent = psutil.cpu_percent(interval=None)
|
|
memory = psutil.virtual_memory()
|
|
disk = psutil.disk_usage('/')
|
|
upload_bps, download_bps = self._get_network_rates()
|
|
gpu_status = self._get_gpu_status()
|
|
boot_time = psutil.boot_time()
|
|
now_ts = datetime.now(UTC).timestamp()
|
|
return {
|
|
'cpu_percent': round(cpu_percent, 1),
|
|
'memory_percent': round(memory.percent, 1),
|
|
'disk_percent': round(disk.percent, 1),
|
|
'disk_used_gb': round(disk.used / (1024 ** 3), 1),
|
|
'disk_total_gb': round(disk.total / (1024 ** 3), 1),
|
|
'network_upload_bps': upload_bps,
|
|
'network_download_bps': download_bps,
|
|
'system_name': platform.system(),
|
|
'system_version': platform.version(),
|
|
'hostname': socket.gethostname(),
|
|
'uptime_seconds': round(max(now_ts - boot_time, 0.0), 1),
|
|
**gpu_status,
|
|
'timestamp': datetime.now(UTC).isoformat(),
|
|
}
|