-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
374 lines (303 loc) · 14.7 KB
/
main.py
File metadata and controls
374 lines (303 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#info: Block 1 - Imports and Configuration
# We import necessary libraries here:
# - psutil: used to get all the system data (CPU, RAM, Disk).
# - tkinter: (Graphical User Interface) library. We use it to create the windows, buttons, and progress bars.
# - logging: helps save what happens in the application to a file, so we can check it later.
# - threading: It allows our system monitoring code to run in the background without freezing the GUI window.
import psutil
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import logging
import threading
import time
import os
from datetime import datetime
# Configure logging
# This tells Python to create a file named 'system_monitor.log' and save all INFO level messages there.
logging.basicConfig(
filename='system_monitor.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
#info: Block 2 - Backend Logic
# This class handles the "Heavy Lifting" or the brain of the application.
class SystemMonitorBackend:
def __init__(self, cpu_threshold=80, ram_threshold=80, disk_threshold=90):
# We set the thresholds here. If usage goes above these numbers, we will warn the user.
self.cpu_threshold = cpu_threshold
self.ram_threshold = ram_threshold
self.disk_threshold = disk_threshold
self.running = False
self._alert_shown = False # Prevent popup spam (keep track if we already alerted)
def get_cpu_usage(self):
# Asks the OS: "How busy is the CPU right now?"
return psutil.cpu_percent(interval=1)
def get_ram_usage(self):
# Asks the OS: "How full is the RAM memory?"
return psutil.virtual_memory().percent
def get_disk_usage(self):
# Global Usage (typically the C: drive on Windows or / on Linux)
return psutil.disk_usage('/').percent
def get_partitions_usage(self):
# This function looks for ALL drives connected to the computer (USB, D:, E:, etc.)
partitions_data = []
try:
partitions = psutil.disk_partitions()
for part in partitions:
# specific filters
if os.name == 'nt':
# On Windows, we skip CD-ROM drives or empty card reader slots
if 'cdrom' in part.opts or part.fstype == '':
continue
else:
# Linux/Unix specific: skip loop devices (snaps) which are virtual drives that clutter the view
if '/loop' in part.device:
continue
try:
# Get stats for this specific partition
usage = psutil.disk_usage(part.mountpoint)
partitions_data.append({
'device': part.device,
'mountpoint': part.mountpoint,
'percent': usage.percent
})
except (PermissionError, OSError):
continue
except Exception:
pass
return partitions_data
def get_top_processes(self, limit=5):
# This function finds which programs are eating up the CPU.
# It iterates over all running processes and sorts them by CPU usage.
procs = []
for p in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
try:
p.info['cpu_percent']
procs.append(p.info)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
# Sort by CPU usage DESC (High to Low)
sorted_procs = sorted(procs, key=lambda p: p['cpu_percent'] or 0.0, reverse=True)
return sorted_procs[:limit] # Return only the top 5
def check_alerts(self, cpu, ram, disk):
# Checks if current usage is higher than our safety limits.
# If yes, it creates a warning message.
alerts = []
if cpu > self.cpu_threshold:
msg = f"High CPU Usage: {cpu}%"
logging.warning(msg)
alerts.append(msg)
if ram > self.ram_threshold:
msg = f"High RAM Usage: {ram}%"
logging.warning(msg)
alerts.append(msg)
if disk > self.disk_threshold:
msg = f"High Disk Usage: {disk}%"
logging.warning(msg)
alerts.append(msg)
return alerts
def log_status(self, cpu, ram, disk):
# Writes the current health status to the log file.
logging.info(f"Status - CPU: {cpu}%, RAM: {ram}%, Disk: {disk}%")
#info: Block 3 - Helper Windows (Log Viewer)
# This class creates the secondary window that pops up when you click "Check Logs" or "Errors".
# It can show everything or just errors, depending on the 'only_errors' flag.
class LogViewerWindow(tk.Toplevel):
def __init__(self, parent, title="Real-time Logs", only_errors=False):
super().__init__(parent)
self.title(title)
self.geometry("600x400")
self.only_errors = only_errors
# A ScrolledText widget
self.text_area = scrolledtext.ScrolledText(self, state='disabled', font=("Consolas", 10))
self.text_area.pack(expand=True, fill='both', padx=10, pady=10)
# Threading again.. We read logs in a background thread so the window doesn't freeze while waiting for file updates.
self.stop_event = threading.Event()
self.log_thread = threading.Thread(target=self.read_logs, daemon=True)
self.log_thread.start()
self.protocol("WM_DELETE_WINDOW", self.on_close)
def read_logs(self):
filename = 'system_monitor.log'
if not os.path.exists(filename):
self.append_text("Log file not found yet.")
return
with open(filename, 'r') as f:
# 1. Read existing content
lines = f.readlines()
for line in lines:
if self.should_display(line):
self.append_text(line)
# 2. "Tail" the file (keep watching for new lines)
while not self.stop_event.is_set():
where = f.tell()
line = f.readline()
if not line:
time.sleep(1) # Wait a bit before checking again
f.seek(where)
else:
if self.should_display(line):
self.append_text(line)
def should_display(self, line):
# If we are in "Errors" mode, we only show lines with WARNING or ERROR.
if not self.only_errors:
return True
return "WARNING" in line or "ERROR" in line
def append_text(self, text):
# Safely add text to the text area.
try:
self.text_area.config(state='normal') # Unlock to edit
self.text_area.insert(tk.END, text) # Add text
self.text_area.see(tk.END) # Scroll to bottom
self.text_area.config(state='disabled') # Lock again so user can't type
except Exception:
pass # Window might closed
def on_close(self):
# Clean up the thread when window is closed
self.stop_event.set()
self.destroy()
#info: Block 4 - Main Application GUI
# It creates the main window and organizes all the visual elements.
class SystemMonitorApp:
def __init__(self, root):
self.root = root
self.root.title("Python System Monitor Pro")
self.root.geometry("600x750")
self.backend = SystemMonitorBackend()
# Styles allow us to make things look pretty and consistent
self.style = ttk.Style()
self.style.configure("TLabel", font=("Helvetica", 10))
self.style.configure("Header.TLabel", font=("Helvetica", 12, "bold"))
self.style.configure("TButton", font=("Helvetica", 10))
self.create_widgets()
# Start the monitoring loop in a separate thread
# daemon=True means this thread will die automatically when the main program closes
self.running = True
self.monitor_thread = threading.Thread(target=self.update_stats, daemon=True)
self.monitor_thread.start()
def create_widgets(self):
# --- Top Action Bar ---
top_frame = ttk.Frame(self.root)
top_frame.pack(fill="x", padx=10, pady=5)
title_label = ttk.Label(top_frame, text="System Monitor", font=("Helvetica", 16, "bold"))
title_label.pack(side=tk.LEFT)
# Buttons for Logs and Errors
errors_btn = ttk.Button(top_frame, text="Errors", command=self.open_error_viewer)
errors_btn.pack(side=tk.RIGHT, padx=(5, 0))
log_btn = ttk.Button(top_frame, text="Check Logs", command=self.open_log_viewer)
log_btn.pack(side=tk.RIGHT)
# --- Global Stats ---
self.global_frame = ttk.LabelFrame(self.root, text="Global Resources")
self.global_frame.pack(fill="x", padx=10, pady=5)
# CPU Global Bar
self.cpu_label = ttk.Label(self.global_frame, text="CPU: 0%")
self.cpu_label.pack(anchor="w", padx=10)
self.cpu_bar = ttk.Progressbar(self.global_frame, orient="horizontal", mode="determinate")
self.cpu_bar.pack(fill="x", padx=10, pady=(0, 5))
# RAM Global Bar
self.ram_label = ttk.Label(self.global_frame, text="RAM: 0%")
self.ram_label.pack(anchor="w", padx=10)
self.ram_bar = ttk.Progressbar(self.global_frame, orient="horizontal", mode="determinate")
self.ram_bar.pack(fill="x", padx=10, pady=(0, 5))
# --- Disk Partitions ---
self.disk_frame = ttk.LabelFrame(self.root, text="Disk Partitions")
self.disk_frame.pack(fill="x", padx=10, pady=5)
# We will dynamically add/update disk widgets here because drives can change
self.partition_widgets = {} # Map: mountpoint -> {label, bar}
# --- Top Processes ---
self.procs_frame = ttk.LabelFrame(self.root, text="Top Heavy Processes (CPU)")
self.procs_frame.pack(fill="both", expand=True, padx=10, pady=5)
# Treeview is a table widget
columns = ("pid", "name", "cpu", "mem")
self.tree = ttk.Treeview(self.procs_frame, columns=columns, show="headings", height=8)
self.tree.heading("pid", text="PID")
self.tree.heading("name", text="Name")
self.tree.heading("cpu", text="CPU %")
self.tree.heading("mem", text="Mem %")
self.tree.column("pid", width=60)
self.tree.column("name", width=150)
self.tree.column("cpu", width=80)
self.tree.column("mem", width=80)
self.tree.pack(fill="both", expand=True, padx=5, pady=5)
def open_log_viewer(self):
LogViewerWindow(self.root, title="Real-time Logs")
def open_error_viewer(self):
LogViewerWindow(self.root, title="System Errors (Alerts)", only_errors=True)
def update_stats(self):
# This is the heartbeat of the app. It runs every 2 seconds.
while self.running:
try:
# 1. Fetch data from backend
cpu = self.backend.get_cpu_usage()
ram = self.backend.get_ram_usage()
disk = self.backend.get_disk_usage()
parts_data = self.backend.get_partitions_usage()
top_procs = self.backend.get_top_processes(5)
# 2. Schedule GUI update on the main thread
# (You shouldn't update GUI directly from a background thread!)
self.root.after(0, self.update_gui, cpu, ram, disk, parts_data, top_procs)
# 3. Check for alerts and log
self.backend.log_status(cpu, ram, disk)
alerts = self.backend.check_alerts(cpu, ram, disk)
if alerts:
# Logic for displaying popup alerts could go here
pass
time.sleep(2) # Wait for 2 seconds
except Exception as e:
print(f"Error in monitoring loop: {e}")
time.sleep(2)
def update_gui(self, cpu, ram, disk, parts_data, top_procs):
# Update Global Stats
self.cpu_label.config(text=f"CPU Total: {cpu}%")
self.cpu_bar["value"] = cpu
self.ram_label.config(text=f"RAM Total: {ram}%")
self.ram_bar["value"] = ram
# Update Partitions (Dynamic!)
# Check if we have widgets for each partition found
current_mounts = set(p['mountpoint'] for p in parts_data)
existing_mounts = set(self.partition_widgets.keys())
# Remove widgets for drives that are no longer there (unplugged USBs)
for m in (existing_mounts - current_mounts):
widgets = self.partition_widgets.pop(m)
widgets['frame'].destroy()
# Add new widgets for newly discovered drives
for p in parts_data:
m = p['mountpoint']
if m not in self.partition_widgets:
frame = ttk.Frame(self.disk_frame)
frame.pack(fill="x", padx=5, pady=2)
lbl = ttk.Label(frame, text=f"{p['device']} ({m})", width=20, anchor="w")
lbl.pack(side=tk.LEFT)
bar = ttk.Progressbar(frame, orient="horizontal", mode="determinate", length=200)
bar.pack(side=tk.LEFT, fill="x", expand=True, padx=5)
perc_lbl = ttk.Label(frame, text="0%", width=8)
perc_lbl.pack(side=tk.RIGHT)
self.partition_widgets[m] = {'frame': frame, 'label': lbl, 'bar': bar, 'perc': perc_lbl}
# Update the progress bar values
w = self.partition_widgets[m]
w['bar']["value"] = p['percent']
w['perc'].config(text=f"{p['percent']}%")
# Update Top Processes
# Clear the old list
for item in self.tree.get_children():
self.tree.delete(item)
# Add the new top 5
for p in top_procs:
self.tree.insert("", "end", values=(
p['pid'],
p['name'],
f"{p['cpu_percent']:.1f}",
f"{p['memory_percent']:.1f}"
))
def show_alert(self, alerts):
messagebox.showwarning("Resource Alert", "\n".join(alerts))
def on_closing(self):
self.running = False
self.root.destroy()
#info: Block 5 - Entry Point
# Script starts
if __name__ == "__main__":
root = tk.Tk()
app = SystemMonitorApp(root)
# Ensure cleaner exit when 'X' is clicked
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()