From 88bba5c44972c1a7fb2a6ca554d1148f45c842c3 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 19:42:52 +0200 Subject: [PATCH 01/12] build(pyproject.toml): add depedency on judas_protocol --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a04f6a3..486fe01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,9 @@ description = "A client for judas, a remote PC fleet management system." readme = "README.md" authors = [] requires-python = ">=3.13" -dependencies = [] +dependencies = [ + "judas-protocol", +] license = { text = "GPL-3.0+" } [dependency-groups] @@ -81,3 +83,6 @@ allowed_tags = [ minor_tags = ["feat"] patch_tags = ["fix", "perf"] default_bump_level = 0 + +[tool.uv.sources] +judas-protocol = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git" } From 84d4b9821435857fa49c2d05e1ae8366d010a64d Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 19:43:14 +0200 Subject: [PATCH 02/12] build(uv.lock): add depedency on judas_protocol --- uv.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/uv.lock b/uv.lock index e23c460..74934f4 100644 --- a/uv.lock +++ b/uv.lock @@ -242,6 +242,9 @@ wheels = [ name = "judas-client" version = "0.1.0" source = { editable = "." } +dependencies = [ + { name = "judas-protocol" }, +] [package.dev-dependencies] bump = [ @@ -256,6 +259,7 @@ test = [ ] [package.metadata] +requires-dist = [{ name = "judas-protocol", git = "https://gitea.pufereq.pl/judas/judas_protocol.git" }] [package.metadata.requires-dev] bump = [ @@ -269,6 +273,11 @@ test = [ { name = "pytest-mock", specifier = ">=3.14.1" }, ] +[[package]] +name = "judas-protocol" +version = "0.1.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#fd070b176347a0f7b81f937b189d8f50736f3514" } + [[package]] name = "markdown-it-py" version = "4.0.0" From eec14b91fdd1f254afd13c5247cff229b629b8b1 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 19:45:00 +0200 Subject: [PATCH 03/12] feat(__main__.py): add `__main__.py` --- src/judas_client/__main__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/judas_client/__main__.py diff --git a/src/judas_client/__main__.py b/src/judas_client/__main__.py new file mode 100644 index 0000000..e67d856 --- /dev/null +++ b/src/judas_client/__main__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + + +import logging as lg + +from judas_client.connector import Connector + +if __name__ == "__main__": + lg.basicConfig( + level=lg.DEBUG, + format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s", + ) + logger = lg.getLogger(__name__) + + connector = Connector("127.0.0.1", 3692) + connector.run() From 8c30c4328dffe2064019f468a6138d66f31c2d89 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 19:45:20 +0200 Subject: [PATCH 04/12] feat(connector.py): add `Connector` class --- src/judas_client/connector.py | 155 ++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/judas_client/connector.py diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py new file mode 100644 index 0000000..56deb23 --- /dev/null +++ b/src/judas_client/connector.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging as lg + +import socket +import uuid +import time + +from judas_protocol import Message + + +class Connector: + def __init__( + self, + host: str, + port: int, + connect_timeout: float = 5.0, + ack_timeout: float = None, + ) -> None: + self.logger: lg.Logger = lg.getLogger( + f"{__name__}.{self.__class__.__name__}" + ) + self.logger.debug("Initializing Connector...") + + self.host: str = host + self.port: int = port + self.socket_timeout: None = None + self.connect_timeout: float = connect_timeout + self.ack_timeout: float = ack_timeout + + self.socket: socket.socket = socket.socket( + socket.AF_INET, socket.SOCK_STREAM + ) + + self.mac_address: str = self._get_mac_address() + + def _get_mac_address(self) -> str: + mac_address: str = ":".join( + [ + "{:02x}".format((uuid.getnode() >> ele) & 0xFF) + for ele in range(0, 8 * 6, 8) + ][::-1] + ) + self.logger.debug(f"MAC address: {mac_address}") + return mac_address + + def _send_ack(self) -> None: + self.logger.debug("[>] Sending ACK...") + try: + self.socket.sendall(Message.ack().to_bytes()) + self.logger.debug("[<] ACK sent") + except socket.error as e: + self.logger.error(f"[!] Failed to send ACK: {e}") + + def _check_ack(self) -> bool: + self.logger.debug("[.] Waiting for ACK...") + try: + self.socket.settimeout(self.ack_timeout) + ack: bytes = self.socket.recv(1024) + self.socket.settimeout(self.socket_timeout) + + if ack == Message.ack().to_bytes(): + self.logger.debug("[<] ACK received") + return True + else: + self.logger.error(f"[!] Invalid ACK received: {ack}") + + except TimeoutError as e: + self.logger.error(f"[!] ACK timeout: {e}") + + except socket.error as e: + self.logger.error(f"[!] Failed to receive ACK: {e}") + + return False + + def connect(self, retry_interval: int = 1) -> None: + self.logger.debug( + f"Connecting to {self.host}:{self.port} with timeout {self.connect_timeout}s..." + ) + try: + self.socket.settimeout(self.connect_timeout) + self.socket.connect((self.host, self.port)) + self.socket.settimeout(self.socket_timeout) + self.logger.info(f"[+] Connected to {self.host}:{self.port}") + except ( + socket.timeout, + ConnectionRefusedError, + ConnectionAbortedError, + ) as e: + self.logger.error( + f"[!] Connection to {self.host}:{self.port} failed: {e}" + ) + self.logger.info( + f"[.] Retrying connection in {retry_interval} s..." + ) + time.sleep(retry_interval) + self.connect(retry_interval=min(60, retry_interval * 2)) + + def send(self, data: bytes) -> None: + self.logger.debug(f"[>] Sending data: {data}") + try: + self.socket.sendall(data) + self.logger.info("[>] Data sent") + self._send_ack() + except BrokenPipeError as e: + self.logger.error(f"[!] Broken pipe: {e}") + self.logger.info("[.] Reconnecting...") + self.connect() + self.send(data) + except (socket.error, ValueError) as e: + self.logger.error(f"[!] Failed to send data: {e}") + + def receive(self) -> bytes: + self.logger.debug("[.] Waiting to receive data...") + try: + data: bytes = self.socket.recv(4096) + if not data: + self.logger.warning("[!] Received empty message") + self.logger.debug(f"[<] Received data: {data}") + return data + except socket.error as e: + self.logger.error(f"[!] Failed to receive data: {e}") + return b"" + + def close(self) -> None: + self.logger.debug("Closing connection...") + self.socket.close() + self.logger.info("Connection closed.") + + def _loop(self) -> None: + self.logger.debug("Starting main loop...") + while True: + data: bytes = self.receive() + if not data: + continue + message = Message.from_bytes(data.strip()) + self.logger.info(f"[<] Message received: {message}") + # if self._check_ack(): + # self.logger.debug("[.] ACK verified") + # else: + # self.logger.error("[!] ACK verification failed") + time.sleep(1) + + def run(self) -> None: + self.logger.debug("Running Connector...") + try: + self.connect() + # send hello message + self.send(Message.hello(self.mac_address).to_bytes()) + self._loop() + except KeyboardInterrupt: + self.logger.info("Interrupted by user.") + finally: + self.close() From 18e60ee8c7d7d7cdf700456f6975d9e17e647e29 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 19:45:57 +0200 Subject: [PATCH 05/12] chore(.vscode/launch.json): add 'Debug client' preset --- .vscode/launch.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a83a8cc --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug client", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/judas_client/__main__.py", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} From 43e61e7e681a7a133bc7b66aae722ad80abdb594 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 20:51:35 +0200 Subject: [PATCH 06/12] refactor(connector.py): move time.sleep to the top of `Connector._loop()` to avoid infinite immediate retrying if data empty --- src/judas_client/connector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index 56deb23..57be436 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -83,6 +83,7 @@ class Connector: self.socket.connect((self.host, self.port)) self.socket.settimeout(self.socket_timeout) self.logger.info(f"[+] Connected to {self.host}:{self.port}") + self.send_hello() except ( socket.timeout, ConnectionRefusedError, @@ -131,6 +132,7 @@ class Connector: def _loop(self) -> None: self.logger.debug("Starting main loop...") while True: + time.sleep(1) data: bytes = self.receive() if not data: continue @@ -140,7 +142,6 @@ class Connector: # self.logger.debug("[.] ACK verified") # else: # self.logger.error("[!] ACK verification failed") - time.sleep(1) def run(self) -> None: self.logger.debug("Running Connector...") From 49f2d69e0bc6a5b0203025dec5d208e4808d04a4 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 20:52:23 +0200 Subject: [PATCH 07/12] fix(connector.py): return empty bytestring if no data in `receive()` --- src/judas_client/connector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index 57be436..9fb7b5d 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -118,6 +118,7 @@ class Connector: data: bytes = self.socket.recv(4096) if not data: self.logger.warning("[!] Received empty message") + return b"" self.logger.debug(f"[<] Received data: {data}") return data except socket.error as e: From 432ef9e2428c198497be28bc9bc53e7b7847cb05 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 23:21:54 +0200 Subject: [PATCH 08/12] refactor(connector.py): change max retry_interval to more sensible 30 secs --- src/judas_client/connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index 9fb7b5d..e8c3708 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -96,7 +96,7 @@ class Connector: f"[.] Retrying connection in {retry_interval} s..." ) time.sleep(retry_interval) - self.connect(retry_interval=min(60, retry_interval * 2)) + self.connect(retry_interval=min(30, retry_interval * 2)) def send(self, data: bytes) -> None: self.logger.debug(f"[>] Sending data: {data}") From 25f6ebbf59c5cbeb7a0c9ea3de764f0552361dd3 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 23:24:39 +0200 Subject: [PATCH 09/12] feat(connector.py): add `send_hello()` method --- src/judas_client/connector.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index e8c3708..a55103d 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -130,6 +130,11 @@ class Connector: self.socket.close() self.logger.info("Connection closed.") + def send_hello(self) -> None: + self.logger.debug("[.] Sending hello message...") + hello_message: Message = Message.hello(self.mac_address) + self.send(hello_message.to_bytes()) + def _loop(self) -> None: self.logger.debug("Starting main loop...") while True: From 4496fc60aa8e048870ca5a958f3423c0e3c53f4f Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 23:25:25 +0200 Subject: [PATCH 10/12] feat(connector.py): add `reconnect()` method to simplify reconnecting to server --- src/judas_client/connector.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index a55103d..bfbfc97 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -130,6 +130,12 @@ class Connector: self.socket.close() self.logger.info("Connection closed.") + def reconnect(self) -> None: + self.logger.debug("Reconnecting...") + self.close() + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect() + def send_hello(self) -> None: self.logger.debug("[.] Sending hello message...") hello_message: Message = Message.hello(self.mac_address) @@ -141,6 +147,7 @@ class Connector: time.sleep(1) data: bytes = self.receive() if not data: + self.reconnect() continue message = Message.from_bytes(data.strip()) self.logger.info(f"[<] Message received: {message}") From eef39bc2c0f7f8488867d0c6b8d2eb6224c72454 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 28 Aug 2025 23:26:19 +0200 Subject: [PATCH 11/12] chore(connector.py): remove redundant hello send in `run()` --- src/judas_client/connector.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index bfbfc97..5fd6ee0 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -160,8 +160,6 @@ class Connector: self.logger.debug("Running Connector...") try: self.connect() - # send hello message - self.send(Message.hello(self.mac_address).to_bytes()) self._loop() except KeyboardInterrupt: self.logger.info("Interrupted by user.") From 5844d4b52123a3e28be79c980f037e922656fe58 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Fri, 29 Aug 2025 00:40:35 +0200 Subject: [PATCH 12/12] feat(connector.py): require ACK for HELLO message, retry if not received --- src/judas_client/connector.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/judas_client/connector.py b/src/judas_client/connector.py index 5fd6ee0..bfa71d0 100644 --- a/src/judas_client/connector.py +++ b/src/judas_client/connector.py @@ -139,7 +139,16 @@ class Connector: def send_hello(self) -> None: self.logger.debug("[.] Sending hello message...") hello_message: Message = Message.hello(self.mac_address) - self.send(hello_message.to_bytes()) + acknowledged: bool = False + while not acknowledged: + self.send(hello_message.to_bytes()) + self.logger.debug("[.] Hello message sent, waiting for ACK...") + acknowledged = self._check_ack() + if not acknowledged: + self.logger.warning( + "[!] Hello message not acknowledged, retrying..." + ) + time.sleep(1) def _loop(self) -> None: self.logger.debug("Starting main loop...")