Windows desktop system monitor (PyQt6). CPU, memory, network, multi-GPU (NVIDIA + AMD), per-process GPU memory and network traffic.
uv sync
uv run python -m sysmonitor # or python main.py
.\build.ps1 # Nuitka one-file exe → SysMonitor.exe- Python 3.13+ only. Use
uv, not pip. - No lint, typecheck, or test tooling — do not run them.
- Windows-only; all non-portable APIs (pywin32, pythonnet, ctypes Win32, PDH, WMI).
- Run without admin for most features; PawnIO driver requires admin for per-core CPU temp/freq via MSR.
sysmonitor/
├── __main__.py # python -m sysmonitor entry
├── core/ # UI entry & window
│ ├── main.py # QApp → single-instance → splash → init → MonitorWindow → app.exec
│ ├── window.py # MonitorWindow (700px wide, dynamic height, Mica, connects MonitorData signals)
│ ├── splash.py # SplashWindow (frameless loading progress during init)
│ └── single_instance.py # Named mutex + QLocalServer IPC
├── data/ # Data & monitor backends
│ ├── monitor_data.py # MonitorData (QObject): owns QTimers, background threads, emits Qt signals
│ └── monitors/
│ ├── gpu.py # GpuBackend, GpuProcMem (NVML → LHM AMD → WMI → simulation)
│ ├── network.py # NetworkETW (PDH \Process(*)\IO Read Bytes/sec)
│ └── cpu_sensors.py # CpuSensors daemon thread (PDH power/freq + LHM temp)
├── ui/
│ └── widgets.py # MeterRow, Sparkline, CoreGrid
└── utils/
├── config.py # ThemeConfig global class + resolve_colors
├── elevation.py # is_admin, try_elevate (unused in current main flow)
├── pawnio.py # PawnIO kernel driver detection + silent install
└── utils.py # res_path, fmt_bytes, cpu_name, level_color, bar_style
Root main.py is from sysmonitor.core.main import main; main().
QApplicationinit →acquire_single_instance()(named mutexLocal\SysMonitor_SingleInstance_Mutex)- If second instance:
notify_existing_instance()(QLocalSocket) then exit setQuitOnLastWindowClosed(False)for traySplashWindowshown (frameless loading window, centered, progress bar + status label)ensure_pawnio()→CpuSensors()→GpuBackend()→NetworkETW()MonitorData(gpu, net_etw, cpu_sensors)createdMonitorWindow(data)→start_single_instance_server()→ splash closed →show()app.aboutToQuit.connect(uninstall_pawnio)
No UAC auto-elevation in current entry flow. elevation.py exists but is not called from main. The --elevated flag handling mentioned in older docs was removed.
MonitorData is the central coordinator (QObject with 4 pyqtSignals):
| Signal | Emitted by | Trigger | Contents |
|---|---|---|---|
cpu_updated |
_cpu_worker thread |
_timer_main (100-1000ms) |
CpuData (total%, per-core%, freqs, power, temp, procs) |
mem_updated |
_mem_worker thread |
_timer_mem (1000ms) |
MemData (mem/swap pct, gpu_mem list, freq, procs) |
net_updated |
_net_worker thread |
_timer_net (1000ms) |
NetData (up/down bytes/sec, procs_text) |
gpu_updated |
_cpu_worker thread |
same as cpu | list[GpuItem] (util, temp, power, clock, enc/dec, PCIe, procs) |
MonitorWindowconnects signals in constructor:data.cpu_updated.connect(self._on_cpu), etc.- Workers use
threading.Event+ QTimer timeout for wake-up — no Qt signals from threads. _proc_workerruns continuously (1s sleep), pollspsutil.process_iterfor top memory & CPU processes.CpuSensors._loopruns in its own daemon thread (1s interval).GpuBackend._bg_poll_loopruns for NVML backend only (daemon thread, configurable interval).- Cross-thread data flow: instance attributes, no explicit locking for simple values (power, temp, freq).
NVML → WMI probe (has AMD?) → LHM (LibreHardwareMonitor .NET) → WMI fallback → Simulation
| Backend | Requires | Data |
|---|---|---|
| NVML | NVIDIA GPU + nvidia-ml-py |
Full: util, mem, temp, power, clock, enc/dec, PCIe, per-process |
| LHM AMD | LibreHardwareMonitorLib.dll (in libs/LHM/) + pythonnet |
Util, temp, power, clock, VRAM |
| WMI | pywin32 | Name + VRAM total only |
| Simulation | SYS_AMD_SIMULATE=1 env |
2 fake AMD GPUs |
Per-process GPU memory (GpuProcMem): PDH \GPU Process Memory(*)\Dedicated Usage, works for any WDDM GPU.
Sensors fmt_bytes_short is defined locally in gpu.py (avoids circular import with utils.py).
Three modes cycled via _cycle_theme(): system → dark → light → ...
ThemeConfigis a global mutable class, mutated byresolve_colors(mode). No QPalette mutation.- "system" mode reads
QApplication.palette()live and trackscolorSchemeChanged/PaletteChangeevent for live Windows dark/light changes. _applying_themeflag prevents re-entrantsetStyleSheetloops.
Mica (Windows 11): applied in showEvent via DwmExtendFrameIntoClientArea + DWMWA_SYSTEMBACKDROP_TYPE=2. Must re-apply after any setWindowFlag() (recreates HWND). Requires transparent widget backgrounds.
Always use utils.res_path(*parts); works in source, Nuitka onefile (sys._MEIPASS), and PyInstaller modes. Walks up from utils/ dir to find libs/.
Key assets: libs/logo.ico, libs/LHM/*.dll, libs/PawnIO_setup.exe.
CpuSensors._init_lhm() and GpuBackend._find_libs() do their own path resolution for LHM DLLs (appending to sys.path and PATH).
pawnio.py detects and silently installs the PawnIO kernel driver via PawnIO_setup.exe -silent. Required for LHM to read CPU temperature/frequency via MSR. Auto-installed in core/main.py via ensure_pawnio() if admin; graceful fallback if not. Uninstalled on app.aboutToQuit.
Binary dependencies (scripts/get-deps.ps1): LHM DLLs and PawnIO_setup.exe are not in git. Run before any build — downloads from GitHub releases. build.ps1 calls it automatically.
Nuitka (primary): .\build.ps1 runs get-deps.ps1, uv sync --group dev, then:
uv run python -m nuitka --standalone --onefile --windows-console-mode=disable --windows-uac-admin --plugin-enable=pyqt6 --include-raw-dir=libs=libs --windows-icon-from-ico=libs/logo.ico --clean-cache=all -o SysMonitor.exe main.py
PyInstaller (alternative): pyinstaller SysMonitor.spec (note: SysMonitor.spec explicitly collects pythonnet + clr_loader hidden imports for CpuSensors LHM support).
GitHub Actions (.github/workflows/build.yml):
- Uses
setup-uv@v8.2.0(pinned) - Nuitka step uses
shell: cmd— pwsh swallows long flags github.server_url == 'https://github.com'guard prevents Forgejo from picking it up
Forgejo CI (.forgejo/workflows/build.yml):
- Self-hosted Windows runner (label
windows, no pwsh — usespowershell5.1) - Release asset upload via internal API directly (
http://debian.lan:3257/api/v1) - Uses
>multiline syntax for Nuitka command - No
--clean-cache=allin CI (reuses compile cache)
- All external calls (PDH, WMI, NVML, LHM) wrapped in try/except with graceful degradation.
- History storage:
deque(maxlen=600)in Sparkline. MeterRow.set_value()only callssetStyleSheetwhen color tier changes (optimization).psutil.cpu_percent(percpu=True)called once atMonitorDatainit to calibrate.- Memory frequency queried once via PowerShell subprocess (
Get-CimInstance Win32_PhysicalMemory). cpu_name()readsProcessorNameStringfrom registry (matches Task Manager).- All Python code in
sysmonitor/package (rootmain.pyis justfrom sysmonitor.core.main import main; main()).