15 Commits

Author SHA1 Message Date
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
github-actions[bot]
a1afd261d5 chore(release): 0.4.2 2026-03-01 17:58:09 +00:00
29ed2c8e5d fix(connector.py): do not connect multiple times when BlockingIOError encountered in Connector.connect() 2026-03-01 18:56:44 +01:00
5 changed files with 220 additions and 10 deletions

View File

@@ -2,6 +2,29 @@
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
### Bug Fixes
- [`29ed2c8`](https://gitea.pufereq.pl/judas/judas_client/commit/29ed2c8e5d256aa7b75424e6d71e14463f8b9caa) **connector.py**: do not connect multiple times when BlockingIOError encountered in `Connector.connect()`
## [0.4.1] - 2026-02-28
### Bug Fixes

View File

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

View File

@@ -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.

View File

@@ -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
@@ -107,9 +113,17 @@ class Connector:
def send_hello(self) -> None:
"""Send a HELLO message to the server."""
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)
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...")
@@ -145,8 +159,24 @@ class Connector:
self.socket.connect((self.host, self.port))
connected = True
except BlockingIOError:
# Connection in progress
time.sleep(0.1)
# 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
else:
self.logger.error(
f"[!] Connection failed with error code: {err}"
)
if not connected:
continue
except socket.error as e:
self.logger.error(f"[!] Connection error: {e}")
self.logger.debug(f"[.] Retrying in {delay} seconds...")
@@ -155,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."""
@@ -202,7 +233,9 @@ class Connector:
self.on_message(message)
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_outbound()
except Exception as e:

40
uv.lock generated
View File

@@ -275,10 +275,11 @@ wheels = [
[[package]]
name = "judas-client"
version = "0.4.1"
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 = [
@@ -318,8 +322,8 @@ test = [
[[package]]
name = "judas-protocol"
version = "0.6.0"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#d16c1914ba343aed300f1c5fae0201370c3274de" }
version = "0.9.0"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#3d5a1e95daa4cd99b51abdcaca9967fa8f921ec1" }
[[package]]
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" },
]
[[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"