16 Commits

Author SHA1 Message Date
edfd2cf043 Merge pull request 'feat/add-basic-telemetry' (#10) from feat/add-basic-telemetry into develop
Reviewed-on: #10
2026-03-08 19:42:40 +00:00
07b9ba6fdf refactor(client.py): improve CPU model retrivement on Windows 2026-03-08 20:30:00 +01:00
14e69fc406 build(uv.lock): update judas_protocol to 0.9.1 2026-03-05 22:29:32 +01:00
d6415712d9 feat(connector.py): send initial telemetry after HELLO 2026-03-05 22:09:05 +01:00
9360fe9f3c feat(connector.py): add self.client property 2026-03-05 22:08:53 +01:00
5235cef103 feat(client.py): add initial telemetry gathering 2026-03-05 22:08:20 +01:00
869763dde3 build(uv.lock): add depedency on psutil 2026-03-05 21:29:08 +01:00
dd5fccf8ff build(pyproject.toml): add depedency on psutil 2026-03-05 21:28:56 +01:00
a4a9051130 build(uv.lock): update judas_protocol to 0.9.0 2026-03-05 21:26:46 +01:00
github-actions[bot]
a6363f8591 chore(release): 0.4.4 2026-03-05 20:06:45 +00:00
f065d26a5f Merge pull request 'chore(release): 0.4.4' (#9) from release/0.4.4 into main
Reviewed-on: #9
2026-03-05 20:06:22 +00:00
0a46ac4527 build(uv.lock): update judas_protocol to 0.8.0 2026-03-03 19:34:50 +01:00
85553eefc5 refactor(connector.py): refactor calls to Message class constructors after protocol changes 2026-03-01 20:17:19 +01:00
f24f7c9b08 build(uv.lock): update judas_protocol to 0.7.0 2026-03-01 20:14:18 +01:00
github-actions[bot]
c3a8c2011c chore(release): 0.4.3 2026-03-01 18:38:46 +00:00
7099853125 fix(connector.py): fix no timeout on connection error 2026-03-01 19:37:24 +01:00
5 changed files with 240 additions and 10 deletions

View File

@@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.4.4] - 2026-03-05
### Refactor
- [`85553ee`](https://gitea.pufereq.pl/judas/judas_client/commit/85553eefc56e30f6d6ce40ff428fa46dc0c880fa) **connector.py**: refactor calls to Message class constructors after protocol changes
### Build
- [`0a46ac4`](https://gitea.pufereq.pl/judas/judas_client/commit/0a46ac45273d516ac061fd3ef2515281a3adb1de) **uv.lock**: update judas_protocol to 0.8.0
- [`f24f7c9`](https://gitea.pufereq.pl/judas/judas_client/commit/f24f7c9b08624a7ce3a765fb259d6f126dec82fa) **uv.lock**: update judas_protocol to 0.7.0
## [0.4.3] - 2026-03-01
### Bug Fixes
- [`7099853`](https://gitea.pufereq.pl/judas/judas_client/commit/709985312514f866b760610fe7d8374763d188b2) **connector.py**: fix no timeout on connection error
## [0.4.2] - 2026-03-01 ## [0.4.2] - 2026-03-01
### Bug Fixes ### Bug Fixes

View File

@@ -4,13 +4,14 @@ build-backend = "uv_build"
[project] [project]
name = "judas_client" name = "judas_client"
version = "0.4.2" version = "0.4.4"
description = "A client for judas, a remote PC fleet management system." description = "A client for judas, a remote PC fleet management system."
readme = "README.md" readme = "README.md"
authors = [] authors = []
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"judas-protocol", "judas-protocol",
"psutil>=7.2.2",
] ]
license = { text = "GPL-3.0+" } license = { text = "GPL-3.0+" }

View File

@@ -5,8 +5,12 @@ from __future__ import annotations
import logging as lg import logging as lg
import os import os
import platform
import subprocess
import uuid import uuid
from typing import Any
import psutil
from judas_protocol import Message from judas_protocol import Message
from judas_client.connector import Connector from judas_client.connector import Connector
@@ -41,12 +45,17 @@ class Client:
self.logger.debug(f"ID: {self.id}") self.logger.debug(f"ID: {self.id}")
self.connector: Connector = Connector( self.connector: Connector = Connector(
client=self,
mac_address=self.id, mac_address=self.id,
host=self.server_host, host=self.server_host,
port=self.server_port, port=self.server_port,
on_message=self.handle_message, on_message=self.handle_message,
) )
self.initial_telemetry: dict[str, Any] = (
self._gather_initial_telemetry()
)
def _get_mac_address(self) -> str: def _get_mac_address(self) -> str:
"""Get the MAC address of the client. """Get the MAC address of the client.
@@ -61,6 +70,144 @@ class Client:
) )
return mac_address return mac_address
def _get_cpu_model(self) -> str:
"""Get the CPU model name.
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."""
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": round(cpu_frequency.max, 2)
if cpu_frequency
else "Unknown",
"min_frequency_mhz": round(cpu_frequency.min, 2)
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: def handle_message(self, message: Message) -> None:
"""Handle incoming messages. """Handle incoming messages.

View File

@@ -5,16 +5,20 @@ import logging as lg
import selectors import selectors
import socket import socket
import time import time
from typing import Callable from typing import Callable, TYPE_CHECKING
from judas_protocol import Category, ControlAction, Message from judas_protocol import Category, ControlAction, Message
if TYPE_CHECKING:
from judas_client.client import Client
class Connector: class Connector:
"""Connector class for managing TCP connection and message exchange.""" """Connector class for managing TCP connection and message exchange."""
def __init__( def __init__(
self, self,
client: Client,
mac_address: str, mac_address: str,
host: str, host: str,
port: int, port: int,
@@ -34,6 +38,8 @@ class Connector:
) )
self.logger.debug("Initializing Connector...") self.logger.debug("Initializing Connector...")
self.client: Client = client
self.host: str = host self.host: str = host
self.port: int = port self.port: int = port
@@ -107,9 +113,17 @@ class Connector:
def send_hello(self) -> None: def send_hello(self) -> None:
"""Send a HELLO message to the server.""" """Send a HELLO message to the server."""
self.logger.debug("[*] Sending HELLO message...") self.logger.debug("[*] Sending HELLO message...")
hello_message: Message = Message.hello(self.mac_address) hello_message: Message = Message.Control.hello(self.mac_address)
self.send(hello_message) 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: def close(self) -> None:
"""Close the connection and clean up resources.""" """Close the connection and clean up resources."""
self.logger.debug("[*] Closing connection...") self.logger.debug("[*] Closing connection...")
@@ -145,8 +159,24 @@ class Connector:
self.socket.connect((self.host, self.port)) self.socket.connect((self.host, self.port))
connected = True connected = True
except BlockingIOError: except BlockingIOError:
# connection in progress # connection in progress, wait for socket to become writable
self.logger.debug(
"[.] Connection in progress, waiting for completion..."
)
events = self.selector.select(timeout=1)
for _, mask in events:
if mask & selectors.EVENT_WRITE:
err = self.socket.getsockopt(
socket.SOL_SOCKET, socket.SO_ERROR
)
if err == 0:
connected = True connected = True
else:
self.logger.error(
f"[!] Connection failed with error code: {err}"
)
if not connected:
continue
except socket.error as e: except socket.error as e:
self.logger.error(f"[!] Connection error: {e}") self.logger.error(f"[!] Connection error: {e}")
self.logger.debug(f"[.] Retrying in {delay} seconds...") self.logger.debug(f"[.] Retrying in {delay} seconds...")
@@ -155,6 +185,7 @@ class Connector:
self.logger.debug("[*] Connected, sending HELLO...") self.logger.debug("[*] Connected, sending HELLO...")
self.send_hello() self.send_hello()
self.send_initial_telemetry()
def run(self) -> None: def run(self) -> None:
"""Run the main event loop.""" """Run the main event loop."""
@@ -202,7 +233,9 @@ class Connector:
self.on_message(message) self.on_message(message)
if message.ack_required: if message.ack_required:
ack_message: Message = Message.ack(message.id) ack_message: Message = Message.Control.ack(
message.id
)
self.send(ack_message) self.send(ack_message)
self._send_outbound() self._send_outbound()
except Exception as e: except Exception as e:

40
uv.lock generated
View File

@@ -275,10 +275,11 @@ wheels = [
[[package]] [[package]]
name = "judas-client" name = "judas-client"
version = "0.4.2" version = "0.4.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "judas-protocol" }, { name = "judas-protocol" },
{ name = "psutil" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -298,7 +299,10 @@ test = [
] ]
[package.metadata] [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] [package.metadata.requires-dev]
bump = [ bump = [
@@ -318,8 +322,8 @@ test = [
[[package]] [[package]]
name = "judas-protocol" name = "judas-protocol"
version = "0.6.0" version = "0.9.1"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#d16c1914ba343aed300f1c5fae0201370c3274de" } source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" }
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
@@ -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" }, { 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"