Skip to content

Commit 1d1ec99

Browse files
Add metrics monitoring functionality with callback support
Now there is an easy way to natively get stable stream of data X interval for Y amount of time, or until you explicitly stop! Updated the C code, the Python Lib Wrapper, and the example script Updated SECURITY.md as well
1 parent b7ffaa6 commit 1d1ec99

File tree

5 files changed

+324
-16
lines changed

5 files changed

+324
-16
lines changed

SECURITY.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
## Supported Versions
22

3-
| Version | Supported | Release Date |
4-
|---------|-----------|--------------|
5-
| 0.2.x || 7th Aug 2025 |
6-
| 0.1.x || 7th Aug 2025 |
3+
| Version | Supported | Release Date |
4+
|---------|-----------|---------------|
5+
| 0.3.x || 17th Aug 2025 |
6+
| 0.2.x || 13th Aug 2025 |
7+
| 0.1.x || 7th Aug 2025 |
8+
9+
> v0.1.0 cannot be found in the release section, it was a dev version that was never released to the public.
10+
>
11+
> v0.2.0-beta is the first public release, and it is a beta version. It also supports `pip`
712
813
## Reporting a Vulnerability
914

TODO.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
| Item | Description | Priority | Version to Expect |
2-
|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------|
3-
| processInspect | Add an ondemand feature to process inspect: monitor an app and send data at intervals to the caller (possibly async), with .end to stop, and support for flags. Useful for sysadmin panels. | High | 0.3.0-beta |
4-
5-
6-
Docs to update:
7-
- hRNG Python
8-
- hRNG C
9-
- Helper tools: Compiling (minor changes)
1+
| Item | Description | Priority | Version to Expect |
2+
|---------|-------------|----------|-------------------|
3+
| Nothing | ??? | ??? | ??? |

example/processInspect.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
1. Setting up the ProcessMetrics module
66
2. Using both session-based and snapshot monitoring approaches
77
3. Working with all available metric types
8-
4. Interpreting and displaying the collected metrics
8+
4. Interpreting and displaying the metrics
99
5. Proper error handling and best practices
10+
6. Continuous monitoring with callbacks
1011
"""
1112
import json
1213
import os
@@ -316,7 +317,7 @@ def demo_other_process_monitoring() -> None:
316317
print(f" PID {proc.info['pid']}: {proc.info['name']}")
317318

318319
# Try to monitor a system process like explorer.exe on Windows
319-
target_name = "explorer.exe" if os.name == "nt" else "sshd"
320+
target_name = "explorer.exe"
320321
target_pid = None
321322

322323
for proc in psutil.process_iter(['pid', 'name']):
@@ -340,6 +341,119 @@ def demo_other_process_monitoring() -> None:
340341
print(f"\nCould not find {target_name} process")
341342

342343

344+
def demo_continuous_monitoring() -> None:
345+
"""
346+
Demonstrates how to use continuous monitoring with callbacks.
347+
348+
This approach is ideal for:
349+
- Real-time monitoring dashboards
350+
- System administration panels
351+
- Long-term resource tracking
352+
- Detecting resource spikes or anomalies
353+
"""
354+
print("\n\n" + "=" * 80)
355+
print("DEMONSTRATION: CONTINUOUS MONITORING WITH CALLBACKS")
356+
print("=" * 80)
357+
358+
metrics = ProcessMetrics()
359+
pid = os.getpid() # Monitor the current Python process
360+
361+
# Define the metrics we want to collect
362+
flags = (
363+
ProcessMetrics.METRIC_WORKING_SET |
364+
ProcessMetrics.METRIC_PRIVATE_BYTES |
365+
ProcessMetrics.METRIC_CPU_USAGE |
366+
ProcessMetrics.METRIC_HANDLES |
367+
ProcessMetrics.METRIC_THREADS
368+
)
369+
370+
# Counter for received updates
371+
updates_received = 0
372+
373+
# Callback function that will be called for each metrics update
374+
def metrics_callback(data):
375+
nonlocal updates_received
376+
updates_received += 1
377+
print(f"\nUpdate #{updates_received} received:")
378+
print(f" PID: {data['pid']}")
379+
380+
if 'working_set_kb' in data:
381+
print(f" Working Set: {data['working_set_kb']} KB")
382+
if 'private_kb' in data:
383+
print(f" Private Memory: {data['private_kb']} KB")
384+
if 'cpu' in data:
385+
print(f" CPU Usage: {data['cpu']}%")
386+
if 'handles' in data:
387+
print(f" Handles: {data['handles']}")
388+
if 'threads' in data:
389+
print(f" Threads: {data['threads']}")
390+
391+
# If we receive 5 updates, simulate some workload
392+
if updates_received == 5:
393+
print("\n [Simulating intensive workload during monitoring...]")
394+
simulate_workload(intensity=3)
395+
396+
# Start monitoring with a 1-second interval, run for 10 seconds
397+
interval_ms = 1000 # 1 second between updates
398+
duration_ms = 10000 # Run for 10 seconds total
399+
400+
print(f"Starting continuous monitoring for PID {pid}")
401+
print(f"Interval: {interval_ms}ms, Duration: {duration_ms}ms")
402+
print("Updates will be printed as they are received...")
403+
404+
# Start the monitoring
405+
success = metrics.start_monitoring(
406+
pid=pid,
407+
metrics=flags,
408+
interval_ms=interval_ms,
409+
duration_ms=duration_ms,
410+
callback=metrics_callback
411+
)
412+
413+
if not success:
414+
print("ERROR: Failed to start continuous monitoring!")
415+
return
416+
417+
# Wait until monitoring is complete
418+
print("Monitoring active. Waiting for completion...")
419+
420+
# Keep checking if monitoring is still active
421+
while metrics.is_monitoring_active():
422+
time.sleep(0.5)
423+
424+
print("\nMonitoring completed automatically after duration expired.")
425+
print(f"Received {updates_received} updates in total.")
426+
427+
# Demonstrate manual stopping
428+
print("\nNow demonstrating indefinite monitoring with manual stop...")
429+
updates_received = 0
430+
431+
# Start indefinite monitoring (duration = -1)
432+
success = metrics.start_monitoring(
433+
pid=pid,
434+
metrics=flags,
435+
interval_ms=interval_ms,
436+
duration_ms=-1, # Run indefinitely
437+
callback=metrics_callback
438+
)
439+
440+
if not success:
441+
print("ERROR: Failed to start indefinite monitoring!")
442+
return
443+
444+
print("Indefinite monitoring started. Will stop after 5 seconds...")
445+
446+
# Let it run for 5 seconds then stop manually
447+
time.sleep(5)
448+
449+
# Stop monitoring
450+
if metrics.stop_monitoring():
451+
print("\nMonitoring stopped manually.")
452+
print(f"Received {updates_received} updates before stopping.")
453+
else:
454+
print("\nFailed to stop monitoring or monitoring was already stopped.")
455+
456+
343457
def main():
344458
"""Main function to demonstrate all ProcessMetrics capabilities."""
345459
print("=" * 80)
@@ -353,6 +467,7 @@ def main():
353467
demo_snapshot_monitoring()
354468
demo_selective_metrics()
355469
demo_other_process_monitoring()
470+
demo_continuous_monitoring() # Added the new continuous monitoring demo
356471

357472
print("\n" + "=" * 80)
358473
print("DEMONSTRATION COMPLETE")

pyCTools/processInspect.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ctypes
22
import json
3-
from ctypes import c_char_p, c_size_t, c_ulong, create_string_buffer
3+
from ctypes import c_char_p, c_size_t, c_ulong, create_string_buffer, CFUNCTYPE, c_void_p, c_int
44

55
from pyCTools._loadDLL import load_dll
66

@@ -59,6 +59,24 @@ def __init__(self):
5959
self._dll.get_metrics_json.argtypes = [c_ulong, c_ulong, c_char_p, c_size_t]
6060
self._dll.get_metrics_json.restype = ctypes.c_int
6161

62+
# Define C function type for the monitoring callback
63+
self._CALLBACK_TYPE = CFUNCTYPE(None, c_char_p, c_void_p)
64+
65+
# Set types for monitoring functions
66+
self._dll.start_metrics_monitoring.argtypes = [c_ulong, c_ulong, c_ulong, c_int,
67+
self._CALLBACK_TYPE, c_void_p]
68+
self._dll.start_metrics_monitoring.restype = ctypes.c_int
69+
70+
self._dll.stop_metrics_monitoring.argtypes = []
71+
self._dll.stop_metrics_monitoring.restype = ctypes.c_int
72+
73+
self._dll.is_metrics_monitoring_active.argtypes = []
74+
self._dll.is_metrics_monitoring_active.restype = ctypes.c_int
75+
76+
# Store callback reference to prevent garbage collection
77+
self._callback_ref = None
78+
self._user_callback = None
79+
6280
@staticmethod
6381
def _json_call(func, pid: int, metrics: int, _buffer_size: int = 4096) -> dict:
6482
"""
@@ -121,3 +139,70 @@ def get_snapshot(self, pid: int, metrics: int) -> dict:
121139
dict: Current metrics snapshot.
122140
"""
123141
return self._json_call(self._dll.get_metrics_json, pid, metrics)
142+
143+
# noinspection PyUnusedLocal
144+
def _callback_wrapper(self, json_str, user_data):
145+
"""
146+
Internal callback wrapper that converts C JSON string to Python dict
147+
and calls the user's callback function.
148+
149+
Args:
150+
json_str (c_char_p): JSON metrics data from C.
151+
user_data (c_void_p): User data pointer (unused in this implementation).
152+
"""
153+
if self._user_callback:
154+
metrics_dict = json.loads(ctypes.string_at(json_str).decode('utf-8'))
155+
self._user_callback(metrics_dict)
156+
157+
def start_monitoring(self, pid: int, metrics: int, interval_ms: int,
158+
duration_ms: int = -1, callback=None) -> bool:
159+
"""
160+
Start continuous monitoring of a process at specified intervals.
161+
162+
Args:
163+
pid (int): Process ID to monitor.
164+
metrics (int): Bitmask of metrics to collect (use class flags).
165+
interval_ms (int): Interval between metric collections in milliseconds.
166+
duration_ms (int): Total duration to monitor in milliseconds. Use -1 for
167+
indefinite monitoring until explicitly stopped.
168+
callback (callable): Function to call with each metrics update.
169+
The callback receives a dict of the parsed metrics.
170+
171+
Returns:
172+
bool: True if monitoring started successfully, False otherwise.
173+
"""
174+
if self.is_monitoring_active():
175+
return False
176+
177+
self._user_callback = callback
178+
179+
# Create C-compatible callback function
180+
if callback:
181+
self._callback_ref = self._CALLBACK_TYPE(self._callback_wrapper)
182+
else:
183+
self._callback_ref = None
184+
185+
return bool(self._dll.start_metrics_monitoring(
186+
pid, metrics, interval_ms, duration_ms, self._callback_ref, None))
187+
188+
def stop_monitoring(self) -> bool:
189+
"""
190+
Stop an active monitoring session.
191+
192+
Returns:
193+
bool: True if monitoring was successfully stopped, False if no monitoring was active.
194+
"""
195+
result = bool(self._dll.stop_metrics_monitoring())
196+
if result:
197+
self._user_callback = None
198+
self._callback_ref = None
199+
return result
200+
201+
def is_monitoring_active(self) -> bool:
202+
"""
203+
Check if a monitoring session is currently active.
204+
205+
Returns:
206+
bool: True if monitoring is active, False otherwise.
207+
"""
208+
return bool(self._dll.is_metrics_monitoring_active())

0 commit comments

Comments
 (0)