From a4a9051130a304b6e2d6ed038c62f812dde2c6a4 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 21:26:46 +0100 Subject: [PATCH 1/8] build(uv.lock): update judas_protocol to 0.9.0 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 1b8c2c2..8e1e36f 100644 --- a/uv.lock +++ b/uv.lock @@ -318,8 +318,8 @@ test = [ [[package]] name = "judas-protocol" -version = "0.8.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" } +version = "0.9.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#3d5a1e95daa4cd99b51abdcaca9967fa8f921ec1" } [[package]] name = "markdown-it-py" -- 2.39.5 From dd5fccf8ff7ab214cec277ce6f8ccbc39acc85bb Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 21:28:56 +0100 Subject: [PATCH 2/8] build(pyproject.toml): add depedency on psutil --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f8c4abf..202fe5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ authors = [] requires-python = ">=3.13" dependencies = [ "judas-protocol", + "psutil>=7.2.2", ] license = { text = "GPL-3.0+" } -- 2.39.5 From 869763dde35d18522d4e84bb9436b1d5064364e8 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 21:29:08 +0100 Subject: [PATCH 3/8] build(uv.lock): add depedency on psutil --- uv.lock | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 8e1e36f..f2babcb 100644 --- a/uv.lock +++ b/uv.lock @@ -279,6 +279,7 @@ version = "0.4.4" source = { editable = "." } dependencies = [ { name = "judas-protocol" }, + { name = "psutil" }, ] [package.dev-dependencies] @@ -298,7 +299,10 @@ test = [ ] [package.metadata] -requires-dist = [{ name = "judas-protocol", git = "https://gitea.pufereq.pl/judas/judas_protocol.git" }] +requires-dist = [ + { name = "judas-protocol", git = "https://gitea.pufereq.pl/judas/judas_protocol.git" }, + { name = "psutil", specifier = ">=7.2.2" }, +] [package.metadata.requires-dev] bump = [ @@ -412,6 +416,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" -- 2.39.5 From 5235cef103a4b12f84fbc9def6bb68660041d8e1 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 22:08:20 +0100 Subject: [PATCH 4/8] feat(client.py): add initial telemetry gathering --- src/judas_client/client.py | 121 +++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/judas_client/client.py b/src/judas_client/client.py index f7e41ea..d9c8bb7 100644 --- a/src/judas_client/client.py +++ b/src/judas_client/client.py @@ -5,8 +5,13 @@ from __future__ import annotations import logging as lg import os +import platform +import re +import subprocess import uuid +from typing import Any +import psutil from judas_protocol import Message from judas_client.connector import Connector @@ -41,12 +46,18 @@ class Client: self.logger.debug(f"ID: {self.id}") self.connector: Connector = Connector( + client=self, mac_address=self.id, host=self.server_host, port=self.server_port, on_message=self.handle_message, ) + self.logger.debug("Gathering initial telemetry data...") + self.initial_telemetry: dict[str, Any] = ( + self._gather_initial_telemetry() + ) + def _get_mac_address(self) -> str: """Get the MAC address of the client. @@ -61,6 +72,116 @@ class Client: ) return mac_address + def _get_cpu_model(self) -> str: + + if platform.system() == "Windows": + return platform.processor() + elif platform.system() == "Darwin": + os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin" + command = "sysctl -n machdep.cpu.brand_string" + return str(subprocess.check_output(command).strip()) + elif platform.system() == "Linux": + command = "cat /proc/cpuinfo" + all_info = ( + subprocess.check_output(command, shell=True).decode().strip() + ) + for line in all_info.split("\n"): + if "model name" in line: + return re.sub(".*model name.*:", "", line, 1).strip() + return "Unknown" + + def _gather_initial_telemetry(self) -> dict[str, Any]: + """Gather initial telemetry data.""" + self.logger.debug("Gathering initial telemetry data...") + inventory: dict[str, Any] = {} + + # platform info + inventory["platform"] = { + "system": platform.system(), + "node": platform.node(), + "release": platform.release(), + "version": platform.version(), + "architecture": platform.architecture()[0], + } + + # CPU information + cpu_frequency = psutil.cpu_freq() + inventory["cpu"] = { + "model": self._get_cpu_model(), + "physical_cores": psutil.cpu_count(logical=False), + "threads": psutil.cpu_count(logical=True), + "max_frequency_mhz": cpu_frequency.max + if cpu_frequency + else "Unknown", + "min_frequency_mhz": cpu_frequency.min + if cpu_frequency + else "Unknown", + } + + # Memory information + virtual_memory = psutil.virtual_memory() + swap_memory = psutil.swap_memory() + inventory["memory"] = { + "total_ram": round(virtual_memory.total / (1024**2), 2), + "total_swap": round(swap_memory.total / (1024**2), 2), + } + + # Disk information + disk_partitions = psutil.disk_partitions() + inventory["disks"] = [] + for partition in disk_partitions: + try: + usage = psutil.disk_usage(partition.mountpoint) + inventory["disks"].append( + { + "device": partition.device, + "mountpoint": partition.mountpoint, + "filesystem": partition.fstype, + "total_size_gb": round(usage.total / (1024**3), 2), + } + ) + except (PermissionError, OSError) as e: + self.logger.warning( + f"Permission denied for disk partition: {partition.device}\n{e}" + ) + continue + + # Network information + network_interfaces = psutil.net_if_addrs() + inventory["network"] = [] + for interface_name, addresses in network_interfaces.items(): + interface_data = { + "interface": interface_name, + "ip_addresses": [], + "mac_address": None, + } + for address in addresses: + if address.family.name in ["AF_LINK", "AF_PACKET"]: + interface_data["mac_address"] = address.address + elif address.family.name == "AF_INET": + interface_data["ip_addresses"].append(address.address) + inventory["network"].append(interface_data) + + # User information + inventory["users"] = [] + for user in psutil.users(): + inventory["users"].append( + { + "name": user.name, + "terminal": user.terminal, + "host": user.host, + "started": user.started, + } + ) + + # Python environment information + inventory["python"] = { + "version": platform.python_version(), + "implementation": platform.python_implementation(), + } + + return inventory + def handle_message(self, message: Message) -> None: """Handle incoming messages. -- 2.39.5 From 9360fe9f3c600cd7c610e8fd6e2d80f91a8c341f Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 22:08:53 +0100 Subject: [PATCH 5/8] feat(connector.py): add `self.client` property --- src/judas_client/connector.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index 858d56e..04e564e 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -5,16 +5,20 @@ import logging as lg import selectors import socket import time -from typing import Callable +from typing import Callable, TYPE_CHECKING from judas_protocol import Category, ControlAction, Message +if TYPE_CHECKING: + from judas_client.client import Client + class Connector: """Connector class for managing TCP connection and message exchange.""" def __init__( self, + client: Client, mac_address: str, host: str, port: int, @@ -34,6 +38,8 @@ class Connector: ) self.logger.debug("Initializing Connector...") + self.client: Client = client + self.host: str = host self.port: int = port -- 2.39.5 From d6415712d97c27a1716a836e0cde440b486d9132 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 22:09:05 +0100 Subject: [PATCH 6/8] feat(connector.py): send initial telemetry after HELLO --- src/judas_client/connector.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index 04e564e..a7468cc 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -116,6 +116,14 @@ class Connector: hello_message: Message = Message.Control.hello(self.mac_address) self.send(hello_message) + def send_initial_telemetry(self) -> None: + """Send initial telemetry data to the server.""" + self.logger.debug("[*] Sending initial telemetry...") + telemetry_message: Message = Message.Telemetry.initial( + self.client.initial_telemetry + ) + self.send(telemetry_message) + def close(self) -> None: """Close the connection and clean up resources.""" self.logger.debug("[*] Closing connection...") @@ -177,6 +185,7 @@ class Connector: self.logger.debug("[*] Connected, sending HELLO...") self.send_hello() + self.send_initial_telemetry() def run(self) -> None: """Run the main event loop.""" -- 2.39.5 From 14e69fc40606961d3e3cabbda9119211c81fc972 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 22:29:32 +0100 Subject: [PATCH 7/8] build(uv.lock): update judas_protocol to 0.9.1 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index f2babcb..8807ed2 100644 --- a/uv.lock +++ b/uv.lock @@ -322,8 +322,8 @@ test = [ [[package]] name = "judas-protocol" -version = "0.9.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#3d5a1e95daa4cd99b51abdcaca9967fa8f921ec1" } +version = "0.9.1" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" } [[package]] name = "markdown-it-py" -- 2.39.5 From 07b9ba6fdf7194a06520357327048a33b309ed5b Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 8 Mar 2026 20:30:00 +0100 Subject: [PATCH 8/8] refactor(client.py): improve CPU model retrivement on Windows --- src/judas_client/client.py | 64 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/judas_client/client.py b/src/judas_client/client.py index d9c8bb7..8ec219f 100644 --- a/src/judas_client/client.py +++ b/src/judas_client/client.py @@ -6,7 +6,6 @@ from __future__ import annotations import logging as lg import os import platform -import re import subprocess import uuid from typing import Any @@ -53,7 +52,6 @@ class Client: on_message=self.handle_message, ) - self.logger.debug("Gathering initial telemetry data...") self.initial_telemetry: dict[str, Any] = ( self._gather_initial_telemetry() ) @@ -73,22 +71,50 @@ class Client: return mac_address def _get_cpu_model(self) -> str: + """Get the CPU model name. - if platform.system() == "Windows": - return platform.processor() - elif platform.system() == "Darwin": - os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin" - command = "sysctl -n machdep.cpu.brand_string" - return str(subprocess.check_output(command).strip()) - elif platform.system() == "Linux": - command = "cat /proc/cpuinfo" - all_info = ( - subprocess.check_output(command, shell=True).decode().strip() - ) - for line in all_info.split("\n"): - if "model name" in line: - return re.sub(".*model name.*:", "", line, 1).strip() - return "Unknown" + Returns: + str: The CPU model name, or "Unknown" if it cannot be determined. + """ + + system = platform.system() + + if system == "Windows": + try: + import winreg + + key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", + ) + model, _ = winreg.QueryValueEx(key, "ProcessorNameString") + winreg.CloseKey(key) + return model.strip() + except Exception: + return platform.processor() + + elif system == "Linux": + try: + with open("/proc/cpuinfo", "r") as f: + for line in f: + if "model name" in line: + return line.split(":")[1].strip() + except Exception: + return platform.processor() + + elif system == "Darwin": + try: + return ( + subprocess.check_output( + ["sysctl", "-n", "machdep.cpu.brand_string"] + ) + .decode() + .strip() + ) + except Exception: + return platform.processor() + + return platform.processor() def _gather_initial_telemetry(self) -> dict[str, Any]: """Gather initial telemetry data.""" @@ -110,10 +136,10 @@ class Client: "model": self._get_cpu_model(), "physical_cores": psutil.cpu_count(logical=False), "threads": psutil.cpu_count(logical=True), - "max_frequency_mhz": cpu_frequency.max + "max_frequency_mhz": round(cpu_frequency.max, 2) if cpu_frequency else "Unknown", - "min_frequency_mhz": cpu_frequency.min + "min_frequency_mhz": round(cpu_frequency.min, 2) if cpu_frequency else "Unknown", } -- 2.39.5