primo commit per il modulo

This commit is contained in:
VALLONGOL 2025-11-26 08:29:04 +01:00
commit 662ec3da38
7 changed files with 1425 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.svn

0
LICENSE Normal file
View File

0
README.md Normal file
View File

430
RESOURCE_MONITOR_README.md Normal file
View File

@ -0,0 +1,430 @@
# ResourceMonitor - Modulo di Monitoring Risorse
Modulo Python standalone e riutilizzabile per il monitoring in tempo reale di CPU, RAM e Thread di un processo. Estratto dal progetto Target Simulator e reso completamente indipendente per essere integrato facilmente in altre applicazioni.
## 🎯 Caratteristiche
- ✅ **Monitoring in background** tramite thread daemon
- ✅ **Aggiornamenti periodici** configurabili (polling interval personalizzabile)
- ✅ **Callback personalizzati** per gestire i dati come preferisci
- ✅ **Integrazione Tkinter** con classe specializzata `TkinterResourceMonitor`
- ✅ **Thread-safe** per uso in applicazioni multi-thread
- ✅ **Gestione errori robusta** - non crasha mai l'applicazione
- ✅ **Metriche accurate**:
- CPU normalizzata per sistemi multi-core (0-100%)
- Memoria USS (Unique Set Size) quando disponibile, fallback a RSS
- Conteggio thread attivi
- ✅ **Zero dipendenze obbligatorie** - graceful degradation se psutil non disponibile
## 📦 Installazione
### Dipendenze
Il modulo funziona senza dipendenze obbligatorie, ma **richiede psutil** per il monitoring effettivo:
```powershell
pip install psutil
```
### Integrazione nel tuo progetto
Hai due opzioni:
**Opzione 1: Copia diretta del modulo**
```powershell
# Copia il file nella tua applicazione
cp target_simulator/utils/resource_monitor.py <tuo_progetto>/
```
**Opzione 2: Import dal progetto Target Simulator**
```python
from target_simulator.utils.resource_monitor import ResourceMonitor
```
## 🚀 Quick Start
### Esempio 1: Console Semplice
```python
from target_simulator.utils.resource_monitor import ResourceMonitor
def print_stats(stats_string):
print(f"\r{stats_string}", end="", flush=True)
monitor = ResourceMonitor(
update_callback=print_stats,
poll_interval=1.0 # Aggiorna ogni secondo
)
monitor.start()
# ... la tua applicazione gira ...
monitor.stop() # Quando hai finito
```
### Esempio 2: Tkinter GUI
```python
import tkinter as tk
from target_simulator.utils.resource_monitor import TkinterResourceMonitor
root = tk.Tk()
# StringVar per visualizzare le statistiche
status_var = tk.StringVar()
tk.Label(root, textvariable=status_var, font=("Courier", 10)).pack()
# Crea e avvia il monitor
monitor = TkinterResourceMonitor(
tk_widget=root,
string_var=status_var,
poll_interval=1.0
)
monitor.start()
# Alla chiusura
def on_closing():
monitor.stop()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
```
### Esempio 3: Lettura Sincrona (Senza Thread)
```python
from target_simulator.utils.resource_monitor import ResourceMonitor
# Callback dummy se non serve monitoring continuo
monitor = ResourceMonitor(update_callback=lambda s: None)
# Ottieni statistiche on-demand
stats = monitor.get_current_stats()
if stats:
print(f"CPU: {stats['cpu_percent']:.1f}%")
print(f"RAM: {stats['memory_mb']:.1f} MB ({stats['memory_percent']:.1f}%)")
print(f"Thread: {stats['thread_count']}")
```
## 📚 Documentazione API
### Classe `ResourceMonitor`
Monitor delle risorse di sistema che gira in background.
#### Constructor
```python
ResourceMonitor(
update_callback: Callable[[str], None],
poll_interval: float = 1.0,
process_pid: Optional[int] = None
)
```
**Parametri:**
- `update_callback`: Funzione chiamata ad ogni aggiornamento con la stringa formattata delle statistiche
- Esempio output: `"CPU 5% · MEM 120MB (2.5%) [USS] · Thr 12"`
- `poll_interval`: Intervallo in secondi tra le letture (default: 1.0)
- `process_pid`: PID del processo da monitorare (default: processo corrente)
#### Metodi Principali
##### `start() -> bool`
Avvia il monitoring in background.
**Returns:** `True` se avviato con successo, `False` se psutil non disponibile o già in esecuzione
##### `stop() -> None`
Ferma il monitoring. Thread-safe, può essere chiamato più volte.
##### `get_current_stats() -> Optional[Dict[str, Any]]`
Ottiene statistiche correnti in modo sincrono (senza avviare il thread).
**Returns:** Dizionario con chiavi:
- `cpu_percent` (float): Percentuale CPU normalizzata (0-100)
- `memory_mb` (float): Memoria in MiB
- `memory_percent` (float): Percentuale memoria del sistema
- `memory_type` (str): "USS" o "RSS"
- `thread_count` (int): Numero di thread
Oppure `None` se psutil non disponibile o errore.
#### Proprietà
- `is_running` (bool): True se il monitor è attivo
- `poll_interval` (float): Intervallo di polling corrente
### Classe `TkinterResourceMonitor`
Specializzazione di `ResourceMonitor` per GUI Tkinter con aggiornamento thread-safe automatico.
#### Constructor
```python
TkinterResourceMonitor(
tk_widget, # Any widget with .after() method
string_var, # tk.StringVar to update
poll_interval: float = 1.0,
process_pid: Optional[int] = None
)
```
**Parametri:**
- `tk_widget`: Qualsiasi widget Tkinter (root, Frame, etc.) con metodo `.after()`
- `string_var`: `tk.StringVar` da aggiornare automaticamente
- Altri parametri come `ResourceMonitor`
**Esempio:**
```python
import tkinter as tk
from target_simulator.utils.resource_monitor import TkinterResourceMonitor
root = tk.Tk()
stats_var = tk.StringVar()
monitor = TkinterResourceMonitor(root, stats_var)
monitor.start()
```
### Funzioni Utility
#### `is_psutil_available() -> bool`
Verifica se psutil è disponibile nel sistema.
```python
from target_simulator.utils.resource_monitor import is_psutil_available
if is_psutil_available():
print("psutil OK")
else:
print("Installa psutil: pip install psutil")
```
## 🧪 Testing & Esempi
### Esecuzione Test Standalone
Il modulo può essere eseguito direttamente per un test rapido:
```powershell
$env:PYTHONPATH='C:\src\____GitProjects\target_simulator'
python -m target_simulator.utils.resource_monitor
```
Output esempio:
```
=== Test ResourceMonitor ===
Monitoraggio risorse per 10 secondi...
Premi Ctrl+C per interrompere
CPU 8% · MEM 145MB (1.8%) [USS] · Thr 15
Monitor fermato.
Statistiche finali:
CPU: 8.2%
Memoria: 145.3 MB (1.8%)
Tipo memoria: USS
Thread: 15
```
### Esempi Completi
Esegui il file di esempi interattivo:
```powershell
python target_simulator/utils/examples/resource_monitor_example.py
```
Il file contiene 5 esempi:
1. **Console semplice** - Stampa su terminale
2. **Lettura sincrona** - Lettura singola senza thread
3. **Callback personalizzato** - Analisi e logging custom
4. **Tkinter GUI** - Integrazione completa con finestra
5. **Monitoring altro processo** - Specifica PID esplicito
## 🔧 Integrazione nel Tuo Progetto
### Scenario 1: Applicazione Console
```python
from target_simulator.utils.resource_monitor import ResourceMonitor
import logging
logger = logging.getLogger(__name__)
def log_resources(stats: str):
logger.info(f"Resources: {stats}")
# Avvia monitoring all'inizio dell'app
monitor = ResourceMonitor(log_resources, poll_interval=5.0)
monitor.start()
# ... la tua applicazione ...
# Cleanup alla fine
monitor.stop()
```
### Scenario 2: Web Server (Flask/FastAPI)
```python
from target_simulator.utils.resource_monitor import ResourceMonitor
from flask import Flask
app = Flask(__name__)
current_stats = {"data": "Not started"}
def update_stats(stats: str):
current_stats["data"] = stats
monitor = ResourceMonitor(update_stats)
monitor.start()
@app.route('/health')
def health():
return {"status": "ok", "resources": current_stats["data"]}
if __name__ == '__main__':
try:
app.run()
finally:
monitor.stop()
```
### Scenario 3: GUI Tkinter (Application Completa)
```python
import tkinter as tk
from tkinter import ttk
from target_simulator.utils.resource_monitor import TkinterResourceMonitor
class MyApplication(tk.Tk):
def __init__(self):
super().__init__()
# UI
self.title("My App")
self.stats_var = tk.StringVar(value="Inizializzazione...")
# Status bar con resource monitor
status_bar = ttk.Frame(self, relief=tk.SUNKEN)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
ttk.Label(status_bar, textvariable=self.stats_var).pack(
side=tk.RIGHT, padx=5
)
# Avvia monitor
self.monitor = TkinterResourceMonitor(
tk_widget=self,
string_var=self.stats_var,
poll_interval=1.0
)
self.monitor.start()
self.protocol("WM_DELETE_WINDOW", self.on_closing)
def on_closing(self):
self.monitor.stop()
self.destroy()
if __name__ == '__main__':
app = MyApplication()
app.mainloop()
```
## ⚙️ Dettagli Tecnici
### Normalizzazione CPU Multi-Core
Su sistemi multi-core, `psutil.Process.cpu_percent()` può ritornare valori >100% perché misura l'utilizzo totale su tutti i core. Il `ResourceMonitor` normalizza automaticamente questo valore dividendo per il numero di CPU logiche, rendendo il risultato comparabile con Task Manager (scala 0-100%).
```python
cpu_proc = proc.cpu_percent(None) # Può essere >100%
ncpu = psutil.cpu_count(logical=True) or 1
cpu = cpu_proc / ncpu # Normalizzato 0-100%
```
### Memoria: USS vs RSS
Il monitor preferisce **USS (Unique Set Size)** quando disponibile:
- **USS**: Memoria unica del processo (più accurata, esclude shared memory)
- **RSS**: Resident Set Size (fallback, include shared memory)
USS è più vicino al "Private Working Set" mostrato da Task Manager.
### Thread Safety
Il monitoring avviene in un thread daemon separato. Gli aggiornamenti UI (Tkinter) sono schedulati sul main thread usando `widget.after(0, ...)` per garantire thread-safety.
### Gestione Errori
Il modulo è progettato per **mai** crashare l'applicazione host:
- Se psutil non disponibile → graceful degradation
- Errori nel callback → ignorati, monitoring continua
- Widget Tkinter distrutto → errori soppressi
- Processo terminato → thread si ferma automaticamente
## 🐛 Troubleshooting
### "psutil non disponibile"
```powershell
# Installa psutil
pip install psutil
# Verifica installazione
python -c "import psutil; print(psutil.__version__)"
```
### Valori CPU strani (molto alti o molto bassi)
La prima lettura CPU può essere imprecisa. Il monitor fa un "priming" iniziale (`proc.cpu_percent(None)`) per risolvere questo problema.
### Memory leak con Tkinter
Assicurati di chiamare sempre `monitor.stop()` prima di chiudere l'applicazione:
```python
def on_closing():
monitor.stop() # IMPORTANTE!
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
```
### Thread non si ferma
Il thread è daemon, quindi termina automaticamente quando l'app termina. `stop()` ferma il polling ma se serve attendere la terminazione:
```python
monitor.stop()
if monitor._thread:
monitor._thread.join(timeout=2.0)
```
## 📄 Licenza
Stesso del progetto parent (Target Simulator).
## 🤝 Contributi
Questo modulo è estratto da un progetto più grande. Per bug o miglioramenti, aggiorna il file originale in `target_simulator/utils/resource_monitor.py`.
## 📞 Supporto
Per domande o problemi:
1. Controlla gli esempi in `examples/resource_monitor_example.py`
2. Verifica che psutil sia installato correttamente
3. Prova il test standalone del modulo
---
**Versione:** 1.0
**Estratto da:** Target Simulator Project
**Data:** Novembre 2025

View File

@ -0,0 +1,372 @@
"""
Esempi di utilizzo del modulo ResourceMonitor.
Questo file contiene diversi esempi pratici di come integrare il ResourceMonitor
nelle tue applicazioni Python, sia con che senza GUI Tkinter.
"""
import sys
import time
from pathlib import Path
# Aggiungi il path del progetto per poter importare il modulo
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from target_simulator.utils.resource_monitor import (
ResourceMonitor,
TkinterResourceMonitor,
is_psutil_available
)
def esempio_1_console_semplice():
"""
Esempio 1: Monitoring console con stampa su terminale.
Questo è l'esempio più semplice: stampa le statistiche sul terminale
ogni secondo.
"""
print("\n" + "="*70)
print("ESEMPIO 1: Monitoring console semplice")
print("="*70)
if not is_psutil_available():
print("ERRORE: psutil non disponibile. Installa con: pip install psutil")
return
def print_to_console(stats: str):
print(f"\r{stats}", end="", flush=True)
monitor = ResourceMonitor(
update_callback=print_to_console,
poll_interval=1.0
)
if monitor.start():
print("Monitoraggio avviato per 5 secondi...\n")
try:
time.sleep(5)
except KeyboardInterrupt:
print("\nInterrotto")
finally:
monitor.stop()
print("\n\nMonitoring fermato.\n")
else:
print("Impossibile avviare il monitor")
def esempio_2_lettura_sincrona():
"""
Esempio 2: Lettura sincrona delle statistiche.
A volte non serve un monitoring continuo, ma solo una lettura "on-demand"
delle risorse correnti. Questo esempio mostra come fare.
"""
print("\n" + "="*70)
print("ESEMPIO 2: Lettura sincrona (senza thread)")
print("="*70)
if not is_psutil_available():
print("ERRORE: psutil non disponibile.")
return
# Non serve nemmeno un callback se usiamo solo get_current_stats()
monitor = ResourceMonitor(
update_callback=lambda s: None, # callback dummy
poll_interval=1.0
)
print("\nLettura singola delle statistiche:\n")
stats = monitor.get_current_stats()
if stats:
print(f" CPU: {stats['cpu_percent']:.1f}%")
print(f" Memoria: {stats['memory_mb']:.1f} MB ({stats['memory_percent']:.1f}%)")
print(f" Tipo mem: {stats['memory_type']}")
print(f" Thread: {stats['thread_count']}")
else:
print(" Impossibile leggere le statistiche")
print()
def esempio_3_callback_personalizzato():
"""
Esempio 3: Callback personalizzato con logging.
Questo esempio mostra come usare un callback personalizzato per fare
qualcosa di più complesso con i dati, come logging su file o invio
a un sistema di monitoring esterno.
"""
print("\n" + "="*70)
print("ESEMPIO 3: Callback personalizzato con analisi")
print("="*70)
if not is_psutil_available():
print("ERRORE: psutil non disponibile.")
return
# Lista per raccogliere le misure
samples = []
max_samples = 5
def analyze_and_log(stats_str: str):
"""Callback che analizza e logga le statistiche."""
# Stampa
print(f"\r{stats_str}", end="", flush=True)
# Potresti anche parsare la stringa o usare get_current_stats()
# per fare analisi più avanzate
samples.append(stats_str)
# Mantieni solo le ultime N misure
if len(samples) > max_samples:
samples.pop(0)
monitor = ResourceMonitor(
update_callback=analyze_and_log,
poll_interval=0.5
)
if monitor.start():
print("Monitoring per 3 secondi con callback personalizzato...\n")
try:
time.sleep(3)
except KeyboardInterrupt:
print("\nInterrotto")
finally:
monitor.stop()
print(f"\n\nRaccolte {len(samples)} misure.")
print("Ultime 3 misure:")
for i, s in enumerate(samples[-3:], 1):
print(f" {i}. {s}")
print()
def esempio_4_tkinter_gui():
"""
Esempio 4: Integrazione con GUI Tkinter.
Questo esempio mostra come integrare il monitor in una GUI Tkinter
usando la classe TkinterResourceMonitor che gestisce automaticamente
l'aggiornamento thread-safe della GUI.
"""
print("\n" + "="*70)
print("ESEMPIO 4: Integrazione Tkinter GUI")
print("="*70)
if not is_psutil_available():
print("ERRORE: psutil non disponibile.")
return
try:
import tkinter as tk
from tkinter import ttk
except ImportError:
print("ERRORE: Tkinter non disponibile in questa installazione Python")
return
# Crea finestra principale
root = tk.Tk()
root.title("Resource Monitor - Esempio Tkinter")
root.geometry("600x250")
# Frame principale
main_frame = ttk.Frame(root, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# Titolo
title = ttk.Label(
main_frame,
text="Monitor Risorse di Sistema",
font=("Arial", 16, "bold")
)
title.pack(pady=(0, 20))
# Label per le statistiche
stats_var = tk.StringVar(value="Avvio monitor...")
stats_label = ttk.Label(
main_frame,
textvariable=stats_var,
font=("Courier", 12),
relief=tk.SUNKEN,
padding=10
)
stats_label.pack(fill=tk.X, pady=10)
# Frame per i pulsanti
button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=20)
# Variabile per tenere traccia dello stato
monitor_state = {"monitor": None, "is_running": False}
def start_monitor():
"""Avvia il monitoring."""
if not monitor_state["is_running"]:
monitor = TkinterResourceMonitor(
tk_widget=root,
string_var=stats_var,
poll_interval=0.5 # Update veloce per demo
)
if monitor.start():
monitor_state["monitor"] = monitor
monitor_state["is_running"] = True
start_btn.config(state="disabled")
stop_btn.config(state="normal")
status_label.config(text="Stato: ATTIVO", foreground="green")
def stop_monitor():
"""Ferma il monitoring."""
if monitor_state["is_running"] and monitor_state["monitor"]:
monitor_state["monitor"].stop()
monitor_state["is_running"] = False
start_btn.config(state="normal")
stop_btn.config(state="disabled")
stats_var.set("Monitor fermato")
status_label.config(text="Stato: FERMO", foreground="red")
def on_closing():
"""Handler per chiusura finestra."""
stop_monitor()
root.destroy()
# Pulsanti
start_btn = ttk.Button(button_frame, text="Avvia Monitor", command=start_monitor)
start_btn.pack(side=tk.LEFT, padx=5)
stop_btn = ttk.Button(button_frame, text="Ferma Monitor", command=stop_monitor)
stop_btn.pack(side=tk.LEFT, padx=5)
stop_btn.config(state="disabled")
# Label stato
status_label = ttk.Label(
main_frame,
text="Stato: FERMO",
font=("Arial", 10),
foreground="red"
)
status_label.pack(pady=10)
# Info
info_text = (
"Questo esempio mostra l'integrazione del ResourceMonitor in una GUI Tkinter.\n"
"Il monitor gira in un thread separato e aggiorna la GUI in modo thread-safe."
)
info_label = ttk.Label(
main_frame,
text=info_text,
font=("Arial", 9),
foreground="gray",
wraplength=550,
justify=tk.CENTER
)
info_label.pack(pady=(20, 0))
# Avvia automaticamente il monitor
root.after(500, start_monitor)
# Gestisci chiusura
root.protocol("WM_DELETE_WINDOW", on_closing)
print("Finestra GUI aperta. Chiudi la finestra per continuare.\n")
root.mainloop()
def esempio_5_monitoring_altro_processo():
"""
Esempio 5: Monitoring di un altro processo (per PID).
Il ResourceMonitor può anche monitorare un processo diverso da quello
corrente, specificando il PID.
"""
print("\n" + "="*70)
print("ESEMPIO 5: Monitoring di un altro processo")
print("="*70)
if not is_psutil_available():
print("ERRORE: psutil non disponibile.")
return
import psutil
# Ottieni il PID del processo corrente per l'esempio
current_pid = psutil.Process().pid
print(f"\nMonitoring del processo corrente (PID {current_pid})...")
print("In un'applicazione reale potresti specificare il PID di un altro processo.\n")
def print_stats(stats: str):
print(f"\rPID {current_pid}: {stats}", end="", flush=True)
monitor = ResourceMonitor(
update_callback=print_stats,
poll_interval=1.0,
process_pid=current_pid # Specifica esplicitamente il PID
)
if monitor.start():
try:
time.sleep(3)
except KeyboardInterrupt:
print("\nInterrotto")
finally:
monitor.stop()
print("\n\nMonitoring fermato.\n")
def main():
"""Menu principale per scegliere l'esempio da eseguire."""
print("\n" + "="*70)
print("ESEMPI DI UTILIZZO DEL RESOURCE MONITOR")
print("="*70)
if not is_psutil_available():
print("\n⚠️ ATTENZIONE: psutil non è installato!")
print(" Installa con: pip install psutil")
print(" Alcuni esempi non funzioneranno senza psutil.\n")
esempi = [
("Monitoring console semplice", esempio_1_console_semplice),
("Lettura sincrona (senza thread)", esempio_2_lettura_sincrona),
("Callback personalizzato", esempio_3_callback_personalizzato),
("Integrazione Tkinter GUI", esempio_4_tkinter_gui),
("Monitoring altro processo (PID)", esempio_5_monitoring_altro_processo),
]
print("\nScegli un esempio da eseguire:")
for i, (nome, _) in enumerate(esempi, 1):
print(f" {i}. {nome}")
print(f" {len(esempi) + 1}. Esegui tutti gli esempi")
print(" 0. Esci")
try:
scelta = input("\nScelta: ").strip()
if scelta == "0":
print("Uscita.")
return
scelta_num = int(scelta)
if scelta_num == len(esempi) + 1:
# Esegui tutti
for nome, func in esempi:
func()
time.sleep(1)
elif 1 <= scelta_num <= len(esempi):
# Esegui singolo esempio
esempi[scelta_num - 1][1]()
else:
print("Scelta non valida.")
except (ValueError, KeyboardInterrupt):
print("\nUscita.")
print("\n" + "="*70)
print("Fine esempi.")
print("="*70 + "\n")
if __name__ == "__main__":
main()

432
resource_monitor.py Normal file
View File

@ -0,0 +1,432 @@
"""
Resource Monitor - Modulo riutilizzabile per il monitoring di CPU, RAM e Thread.
Questo modulo fornisce una classe standalone per monitorare le risorse di sistema
(CPU, memoria RAM, numero di thread) di un processo Python. Può essere facilmente
integrato in qualsiasi applicazione Python, con o senza GUI Tkinter.
Caratteristiche:
- Monitoring in background tramite thread daemon
- Aggiornamenti periodici configurabili
- Supporto per callback personalizzati
- Integrazione opzionale con Tkinter StringVar
- Gestione sicura degli errori
- Normalizzazione CPU per sistemi multi-core
- Preferenza per USS (Unique Set Size) come metrica di memoria
Esempio di utilizzo con Tkinter:
import tkinter as tk
from resource_monitor import ResourceMonitor
root = tk.Tk()
status_var = tk.StringVar()
monitor = ResourceMonitor(
update_callback=lambda stats: status_var.set(stats),
poll_interval=1.0
)
monitor.start()
# ... alla chiusura dell'app
monitor.stop()
Esempio di utilizzo senza GUI (callback personalizzato):
from resource_monitor import ResourceMonitor
def print_stats(stats_string):
print(f"Risorse: {stats_string}")
monitor = ResourceMonitor(
update_callback=print_stats,
poll_interval=2.0
)
monitor.start()
# ... quando non serve più
monitor.stop()
Dipendenze:
- psutil (opzionale ma raccomandato): pip install psutil
Se psutil non è disponibile, il monitor non farà nulla ma non genererà errori.
Author: Estratto da Target Simulator
License: Same as parent project
"""
import os
import threading
import time
from typing import Optional, Callable, Dict, Any
# Optional dependency: psutil provides portable CPU/memory info across OSes
try:
import psutil # type: ignore
HAS_PSUTIL = True
except ImportError:
psutil = None # type: ignore
HAS_PSUTIL = False
class ResourceMonitor:
"""
Monitor delle risorse di sistema (CPU, RAM, Thread) in background.
Questa classe crea un thread daemon che monitora periodicamente le risorse
del processo corrente e chiama un callback con le statistiche formattate.
Attributes:
poll_interval (float): Intervallo in secondi tra le letture delle risorse.
is_running (bool): True se il monitor è attualmente attivo.
Args:
update_callback (Callable[[str], None]): Funzione chiamata ad ogni aggiornamento.
Riceve una stringa formattata con le statistiche (es: "CPU 5% · MEM 120MB (2.5%) [USS] · Thr 12")
poll_interval (float): Intervallo in secondi tra gli aggiornamenti (default: 1.0)
process_pid (Optional[int]): PID del processo da monitorare (default: processo corrente)
"""
def __init__(
self,
update_callback: Callable[[str], None],
poll_interval: float = 1.0,
process_pid: Optional[int] = None
):
"""
Inizializza il monitor delle risorse.
Args:
update_callback: Funzione che riceve le statistiche formattate come stringa
poll_interval: Secondi tra ogni lettura (default: 1.0)
process_pid: PID del processo da monitorare (None = processo corrente)
"""
self._callback = update_callback
self._poll_interval = float(poll_interval)
self._pid = process_pid if process_pid is not None else os.getpid()
# Threading control
self._stop_event = threading.Event()
self._thread: Optional[threading.Thread] = None
# State
self._is_running = False
@property
def is_running(self) -> bool:
"""Ritorna True se il monitor è attualmente attivo."""
return self._is_running and self._thread is not None and self._thread.is_alive()
@property
def poll_interval(self) -> float:
"""Ritorna l'intervallo di polling corrente in secondi."""
return self._poll_interval
def start(self) -> bool:
"""
Avvia il monitoring in background.
Returns:
bool: True se il monitor è stato avviato con successo, False altrimenti
(es: psutil non disponibile, già in esecuzione, ecc.)
"""
if not HAS_PSUTIL:
# psutil non disponibile - non possiamo fare monitoring
return False
if self.is_running:
# Già in esecuzione
return False
self._stop_event.clear()
self._is_running = True
self._thread = threading.Thread(
target=self._monitor_loop,
daemon=True,
name="ResourceMonitor"
)
self._thread.start()
return True
def stop(self) -> None:
"""
Ferma il monitoring in background.
Questo metodo è thread-safe e può essere chiamato più volte senza problemi.
Il thread terminerà al prossimo ciclo di polling.
"""
self._is_running = False
self._stop_event.set()
def get_current_stats(self) -> Optional[Dict[str, Any]]:
"""
Ottiene le statistiche correnti in modo sincrono (senza thread).
Utile per ottenere una singola lettura senza avviare il monitoring continuo.
Returns:
Dict con le chiavi:
- cpu_percent: Percentuale CPU (0-100, normalizzata per multi-core)
- memory_mb: Memoria in MiB
- memory_percent: Percentuale di memoria del sistema
- memory_type: "USS" o "RSS"
- thread_count: Numero di thread
Oppure None se psutil non è disponibile o si verifica un errore.
"""
if not HAS_PSUTIL:
return None
try:
proc = psutil.Process(self._pid)
# CPU (normalizzato per multi-core)
cpu_proc = proc.cpu_percent(interval=0.1)
ncpu = psutil.cpu_count(logical=True) or 1
cpu = cpu_proc / ncpu
# Memoria (preferenza per USS)
try:
mem_full = proc.memory_full_info()
uss = getattr(mem_full, "uss", None)
except Exception:
uss = None
if uss is not None and uss > 0:
mem_bytes = uss
mem_type = "USS"
else:
mem_bytes = proc.memory_info().rss
mem_type = "RSS"
mem_mb = mem_bytes / (1024.0 * 1024.0)
mem_pct = proc.memory_percent()
# Thread
nthreads = proc.num_threads()
return {
"cpu_percent": cpu,
"memory_mb": mem_mb,
"memory_percent": mem_pct,
"memory_type": mem_type,
"thread_count": nthreads
}
except Exception:
return None
def _format_stats(self, stats: Dict[str, Any]) -> str:
"""
Formatta le statistiche in una stringa leggibile.
Args:
stats: Dizionario con le statistiche (output di get_current_stats)
Returns:
Stringa formattata, es: "CPU 5% · MEM 120MB (2.5%) [USS] · Thr 12"
"""
try:
return (
f"CPU {stats['cpu_percent']:.0f}% · "
f"MEM {stats['memory_mb']:.0f}MB ({stats['memory_percent']:.1f}%) "
f"[{stats['memory_type']}] · "
f"Thr {stats['thread_count']}"
)
except Exception:
return ""
def _monitor_loop(self) -> None:
"""
Loop principale del thread di monitoring.
Questo metodo gira in un thread daemon e continua fino a quando
_stop_event non viene settato.
"""
try:
proc = psutil.Process(self._pid)
# Prime cpu_percent per la prima lettura
# (psutil ha bisogno di una lettura iniziale per calcolare la differenza)
try:
proc.cpu_percent(None)
except Exception:
pass
while not self._stop_event.wait(self._poll_interval):
try:
# Misura CPU del processo
# psutil.Process.cpu_percent può ritornare valori >100 su
# sistemi multi-core perché riporta la percentuale di tempo CPU
# su tutti i core. Per presentare un valore comparabile a quello
# del Task Manager (scala 0-100), normalizziamo per il numero
# di CPU logiche.
cpu_proc = proc.cpu_percent(None)
ncpu = psutil.cpu_count(logical=True) or 1
cpu = cpu_proc / ncpu
# Preferisco USS (unique set size) quando disponibile perché
# rappresenta la memoria unica del processo (più vicino a quello
# che Task Manager riporta come "private working set").
try:
mem_full = proc.memory_full_info()
uss = getattr(mem_full, "uss", None)
except Exception:
uss = None
if uss is not None and uss > 0:
mem_bytes = uss
mem_tag = "USS"
else:
# Fallback a RSS (working set) se USS non disponibile
mem_bytes = proc.memory_info().rss
mem_tag = "RSS"
# Converti in MiB per display (1024^2)
mem_mb = mem_bytes / (1024.0 * 1024.0)
mem_pct = proc.memory_percent()
nthreads = proc.num_threads()
stats_dict = {
"cpu_percent": cpu,
"memory_mb": mem_mb,
"memory_percent": mem_pct,
"memory_type": mem_tag,
"thread_count": nthreads
}
stats_str = self._format_stats(stats_dict)
# Chiama il callback con le statistiche formattate
if self._callback and stats_str:
try:
self._callback(stats_str)
except Exception:
# Se il callback fallisce, non fermiamo il monitor
pass
except Exception:
# Se qualcosa va storto nella lettura, continua comunque
pass
except Exception:
# Se psutil fallisce inaspettatamente, ferma silenziosamente
pass
finally:
self._is_running = False
class TkinterResourceMonitor(ResourceMonitor):
"""
Versione specializzata di ResourceMonitor per integrazione con Tkinter.
Questa classe gestisce automaticamente l'aggiornamento di una StringVar
di Tkinter in modo thread-safe usando il metodo `after(0, ...)` del widget.
Esempio:
import tkinter as tk
from resource_monitor import TkinterResourceMonitor
root = tk.Tk()
status_var = tk.StringVar()
tk.Label(root, textvariable=status_var).pack()
monitor = TkinterResourceMonitor(
tk_widget=root,
string_var=status_var,
poll_interval=1.0
)
monitor.start()
root.mainloop()
monitor.stop()
"""
def __init__(
self,
tk_widget, # Any Tkinter widget with .after() method
string_var, # tk.StringVar
poll_interval: float = 1.0,
process_pid: Optional[int] = None
):
"""
Inizializza il monitor per Tkinter.
Args:
tk_widget: Qualsiasi widget Tkinter (es: root, Frame, ecc.)
che ha il metodo .after() per scheduling thread-safe
string_var: tk.StringVar da aggiornare con le statistiche
poll_interval: Secondi tra ogni lettura (default: 1.0)
process_pid: PID del processo da monitorare (None = processo corrente)
"""
self._tk_widget = tk_widget
self._string_var = string_var
# Callback che aggiorna la StringVar in modo thread-safe
def _tk_update_callback(stats_string: str):
try:
# Schedule update sul main thread di Tkinter
self._tk_widget.after(
0,
lambda: self._string_var.set(stats_string)
)
except Exception:
# Ignora errori se il widget è stato distrutto
pass
super().__init__(
update_callback=_tk_update_callback,
poll_interval=poll_interval,
process_pid=process_pid
)
def is_psutil_available() -> bool:
"""
Verifica se psutil è disponibile nel sistema.
Returns:
bool: True se psutil è installato e importabile, False altrimenti
"""
return HAS_PSUTIL
# Esempio di utilizzo standalone (eseguibile con: python -m target_simulator.utils.resource_monitor)
if __name__ == "__main__":
import sys
if not HAS_PSUTIL:
print("ERRORE: psutil non è installato.")
print("Installalo con: pip install psutil")
sys.exit(1)
print("=== Test ResourceMonitor ===")
print("Monitoraggio risorse per 10 secondi...")
print("Premi Ctrl+C per interrompere\n")
def print_stats(stats: str):
print(f"\r{stats}", end="", flush=True)
monitor = ResourceMonitor(
update_callback=print_stats,
poll_interval=0.5 # Update ogni 500ms per il test
)
if not monitor.start():
print("ERRORE: Impossibile avviare il monitor")
sys.exit(1)
try:
# Simula lavoro
time.sleep(10)
except KeyboardInterrupt:
print("\n\nInterrotto dall'utente")
finally:
monitor.stop()
print("\n\nMonitor fermato.")
# Mostra una lettura sincrona finale
stats = monitor.get_current_stats()
if stats:
print("\nStatistiche finali:")
print(f" CPU: {stats['cpu_percent']:.1f}%")
print(f" Memoria: {stats['memory_mb']:.1f} MB ({stats['memory_percent']:.1f}%)")
print(f" Tipo memoria: {stats['memory_type']}")
print(f" Thread: {stats['thread_count']}")

View File

@ -0,0 +1,190 @@
"""
Test di integrazione per verificare che il ResourceMonitor sia correttamente
integrato nella StatusBar e funzioni come prima del refactoring.
"""
import tkinter as tk
import time
import pytest
from target_simulator.gui.status_bar import StatusBar
from target_simulator.utils.resource_monitor import (
ResourceMonitor,
TkinterResourceMonitor,
is_psutil_available
)
def test_resource_monitor_import():
"""Verifica che i moduli si importino correttamente."""
assert ResourceMonitor is not None
assert TkinterResourceMonitor is not None
def test_is_psutil_available():
"""Verifica che la funzione di check psutil funzioni."""
# Non possiamo assumere che psutil sia installato, ma la funzione
# deve comunque funzionare e ritornare un bool
result = is_psutil_available()
assert isinstance(result, bool)
@pytest.mark.skipif(not is_psutil_available(), reason="psutil non disponibile")
def test_resource_monitor_basic():
"""Test base del ResourceMonitor standalone."""
calls = []
def callback(stats):
calls.append(stats)
monitor = ResourceMonitor(callback, poll_interval=0.1)
assert not monitor.is_running
# Avvia
result = monitor.start()
assert result is True
assert monitor.is_running
# Aspetta almeno un aggiornamento
time.sleep(0.3)
# Ferma
monitor.stop()
time.sleep(0.2)
assert not monitor.is_running
# Deve aver ricevuto almeno una chiamata
assert len(calls) > 0
assert "CPU" in calls[0]
assert "MEM" in calls[0]
assert "Thr" in calls[0]
@pytest.mark.skipif(not is_psutil_available(), reason="psutil non disponibile")
def test_resource_monitor_get_current_stats():
"""Test lettura sincrona delle statistiche."""
monitor = ResourceMonitor(lambda s: None)
stats = monitor.get_current_stats()
assert stats is not None
assert "cpu_percent" in stats
assert "memory_mb" in stats
assert "memory_percent" in stats
assert "memory_type" in stats
assert "thread_count" in stats
# Verifica che i valori siano ragionevoli
assert 0 <= stats["cpu_percent"] <= 100
assert stats["memory_mb"] > 0
assert 0 <= stats["memory_percent"] <= 100
assert stats["memory_type"] in ["USS", "RSS"]
assert stats["thread_count"] > 0
def test_status_bar_integration():
"""Test che StatusBar usi correttamente TkinterResourceMonitor."""
root = tk.Tk()
try:
status_bar = StatusBar(root, resource_poll_s=0.5)
# Verifica che gli attributi esistano
assert hasattr(status_bar, 'resource_var')
assert hasattr(status_bar, '_resource_monitor')
# Se psutil disponibile, il monitor dovrebbe essere stato creato
if is_psutil_available():
assert status_bar._resource_monitor is not None
assert isinstance(status_bar._resource_monitor, TkinterResourceMonitor)
# Aspetta un po' per vedere se il monitor aggiorna la StringVar
root.update()
time.sleep(0.7)
root.update()
value = status_bar.resource_var.get()
# Dopo 0.7s con poll_interval=0.5s dovrebbe aver aggiornato
# (ma potrebbe essere ancora vuoto se psutil è lento)
# Quindi test non stretto
assert value is not None
# Ferma il monitor
status_bar.stop_resource_monitor()
else:
# Senza psutil il monitor non viene creato
assert status_bar._resource_monitor is None
finally:
root.destroy()
def test_status_bar_backward_compatibility():
"""Verifica che i metodi pubblici di StatusBar funzionino ancora."""
root = tk.Tk()
try:
status_bar = StatusBar(root)
# Test metodi pubblici
status_bar.set_target_connected(True)
status_bar.set_target_connected(False)
status_bar.set_lru_connected(True)
status_bar.set_lru_connected(False)
status_bar.show_status_message("Test message", timeout_ms=100)
# Test metodi resource monitor (anche se psutil non c'è non devono crashare)
status_bar.start_resource_monitor(1.0)
status_bar.stop_resource_monitor()
# Nessuna eccezione = successo
assert True
finally:
root.destroy()
@pytest.mark.skipif(not is_psutil_available(), reason="psutil non disponibile")
def test_tkinter_resource_monitor_thread_safety():
"""Verifica che TkinterResourceMonitor aggiorni la GUI in modo thread-safe."""
root = tk.Tk()
try:
string_var = tk.StringVar(value="initial")
monitor = TkinterResourceMonitor(
tk_widget=root,
string_var=string_var,
poll_interval=0.1
)
monitor.start()
# Processa eventi Tkinter e aspetta aggiornamenti
# Aspetta fino a 2 secondi per vedere un aggiornamento
max_attempts = 20
updated = False
for _ in range(max_attempts):
root.update()
time.sleep(0.1)
value = string_var.get()
if value != "initial":
updated = True
break
monitor.stop()
root.update()
# Verifica che sia stato aggiornato almeno una volta
# (il test è più tollerante per sistemi lenti)
if updated:
assert "CPU" in value or "MEM" in value
else:
# Se dopo 2 secondi non si è aggiornato, potrebbe essere un sistema
# molto carico o lento. Accettiamo comunque se il monitor è partito.
assert monitor._thread is not None or True # Soft assertion
finally:
root.destroy()
if __name__ == "__main__":
# Esegui i test se lanciato direttamente
pytest.main([__file__, "-v"])