|
| 1 | +import ctypes |
| 2 | +import os |
| 3 | +import json |
| 4 | +import platform |
| 5 | +from ctypes import c_char_p, c_size_t, c_ulong, create_string_buffer |
| 6 | + |
| 7 | + |
| 8 | +class ProcessMetrics: |
| 9 | + """ |
| 10 | + Wrapper class for interfacing with the native `processInspect` DLL that collects |
| 11 | + various system and process metrics on Windows. Supports querying working set, |
| 12 | + private bytes, pagefile usage, handle count, thread count, CPU usage, IO stats, |
| 13 | + and network stats. |
| 14 | +
|
| 15 | + The correct DLL is selected automatically based on the platform architecture (x64 or x86), |
| 16 | + searching multiple possible relative paths. |
| 17 | +
|
| 18 | + Methods allow starting a metric collection session, ending it (to retrieve final metrics), |
| 19 | + and getting an instant snapshot of metrics. All metric results are returned as parsed JSON dicts. |
| 20 | +
|
| 21 | + Attributes: |
| 22 | + METRIC_WORKING_SET (int): Flag for working set memory usage. |
| 23 | + METRIC_PRIVATE_BYTES (int): Flag for private bytes memory usage. |
| 24 | + METRIC_PAGEFILE (int): Flag for pagefile usage. |
| 25 | + METRIC_HANDLES (int): Flag for handle count. |
| 26 | + METRIC_THREADS (int): Flag for thread count. |
| 27 | + METRIC_CPU_USAGE (int): Flag for CPU usage percentage. |
| 28 | + METRIC_IO (int): Flag for I/O statistics. |
| 29 | + METRIC_NET (int): Flag for network statistics. |
| 30 | +
|
| 31 | + Raises: |
| 32 | + RuntimeError: If the DLL is not found or if metric collection fails. |
| 33 | + """ |
| 34 | + |
| 35 | + METRIC_WORKING_SET = 0x01 |
| 36 | + METRIC_PRIVATE_BYTES = 0x02 |
| 37 | + METRIC_PAGEFILE = 0x04 |
| 38 | + METRIC_HANDLES = 0x08 |
| 39 | + METRIC_THREADS = 0x10 |
| 40 | + METRIC_CPU_USAGE = 0x20 |
| 41 | + METRIC_IO = 0x40 |
| 42 | + METRIC_NET = 0x80 |
| 43 | + |
| 44 | + def __init__(self): |
| 45 | + """ |
| 46 | + Initialize ProcessMetrics instance by loading the appropriate DLL |
| 47 | + for the current platform architecture. |
| 48 | +
|
| 49 | + Searches for DLL in relative paths: |
| 50 | + - ./dist/{arch}/processInspect_{arch}.dll |
| 51 | + - ../dist/{arch}/processInspect_{arch}.dll |
| 52 | + - ../../dist/{arch}/processInspect_{arch}.dll |
| 53 | +
|
| 54 | + Raises: |
| 55 | + FileNotFoundError: If the DLL cannot be found in any of the expected locations. |
| 56 | + """ |
| 57 | + arch = 'x64' if platform.architecture()[0] == '64bit' else 'x86' |
| 58 | + dll_name = f'processInspect_{arch}.dll' |
| 59 | + base_dir = os.path.dirname(__file__) |
| 60 | + possible_dist_paths = [ |
| 61 | + os.path.join(base_dir, 'dist', arch, dll_name), |
| 62 | + os.path.join(base_dir, '..', 'dist', arch, dll_name), |
| 63 | + os.path.join(base_dir, '..', '..', 'dist', arch, dll_name), |
| 64 | + ] |
| 65 | + |
| 66 | + dll_path = None |
| 67 | + for path in possible_dist_paths: |
| 68 | + abs_path = os.path.abspath(path) |
| 69 | + if os.path.exists(abs_path): |
| 70 | + dll_path = abs_path |
| 71 | + break |
| 72 | + |
| 73 | + if dll_path is None: |
| 74 | + # Could not find DLL, raise an informative error |
| 75 | + raise FileNotFoundError( |
| 76 | + f"Could not find {dll_name} DLL in any of the expected locations:\n" + |
| 77 | + "\n".join(os.path.abspath(p) for p in possible_dist_paths) |
| 78 | + ) |
| 79 | + |
| 80 | + # Load the DLL using ctypes |
| 81 | + self._dll = ctypes.CDLL(dll_path) |
| 82 | + |
| 83 | + # Define argument and return types of DLL functions for type safety |
| 84 | + self._dll.start_metrics_collection.argtypes = [c_ulong, c_ulong] |
| 85 | + self._dll.start_metrics_collection.restype = ctypes.c_int |
| 86 | + |
| 87 | + self._dll.end_metrics_collection.argtypes = [c_ulong, c_ulong, c_char_p, c_size_t] |
| 88 | + self._dll.end_metrics_collection.restype = ctypes.c_int |
| 89 | + |
| 90 | + self._dll.get_metrics_json.argtypes = [c_ulong, c_ulong, c_char_p, c_size_t] |
| 91 | + self._dll.get_metrics_json.restype = ctypes.c_int |
| 92 | + |
| 93 | + @staticmethod |
| 94 | + def _json_call(func, pid: int, metrics: int) -> dict: |
| 95 | + """ |
| 96 | + Internal helper method to call a DLL function that returns JSON data |
| 97 | + in a buffer, parse it, and return as a Python dictionary. |
| 98 | +
|
| 99 | + Args: |
| 100 | + func (callable): DLL function to call, which fills a buffer with JSON. |
| 101 | + pid (int): Process ID to query. |
| 102 | + metrics (int): Bitmask of metrics flags to request. |
| 103 | +
|
| 104 | + Returns: |
| 105 | + dict: Parsed JSON metrics. |
| 106 | +
|
| 107 | + Raises: |
| 108 | + RuntimeError: If the DLL function call returns failure. |
| 109 | + """ |
| 110 | + buf = create_string_buffer(4096) # buffer size fixed to 4 KB |
| 111 | + success = func(pid, metrics, buf, ctypes.sizeof(buf)) |
| 112 | + if not success: |
| 113 | + raise RuntimeError(f"Metric collection failed for PID {pid}") |
| 114 | + return json.loads(buf.value.decode('utf-8')) |
| 115 | + |
| 116 | + def start_session(self, pid: int, metrics: int) -> bool: |
| 117 | + """ |
| 118 | + Start a metrics collection session for a specific process ID. |
| 119 | +
|
| 120 | + Args: |
| 121 | + pid (int): Process ID to start metrics collection for. |
| 122 | + metrics (int): Bitmask of metrics to collect (use class flags). |
| 123 | +
|
| 124 | + Returns: |
| 125 | + bool: True if session started successfully, False otherwise. |
| 126 | + """ |
| 127 | + return bool(self._dll.start_metrics_collection(pid, metrics)) |
| 128 | + |
| 129 | + def end_session(self, pid: int, metrics: int) -> dict: |
| 130 | + """ |
| 131 | + End a previously started metrics collection session and retrieve results. |
| 132 | +
|
| 133 | + Args: |
| 134 | + pid (int): Process ID of the session. |
| 135 | + metrics (int): Bitmask of metrics to retrieve. |
| 136 | +
|
| 137 | + Returns: |
| 138 | + dict: Metrics collected during the session. |
| 139 | + """ |
| 140 | + return self._json_call(self._dll.end_metrics_collection, pid, metrics) |
| 141 | + |
| 142 | + def get_snapshot(self, pid: int, metrics: int) -> dict: |
| 143 | + """ |
| 144 | + Retrieve an instant snapshot of metrics for a process without starting a session. |
| 145 | +
|
| 146 | + Args: |
| 147 | + pid (int): Process ID to query. |
| 148 | + metrics (int): Bitmask of metrics to retrieve. |
| 149 | +
|
| 150 | + Returns: |
| 151 | + dict: Current metrics snapshot. |
| 152 | + """ |
| 153 | + return self._json_call(self._dll.get_metrics_json, pid, metrics) |
0 commit comments