From f41a7774ec5cb4997005ac8e31ed7bdc3541a8d6 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 1 Mar 2026 20:14:03 +0100 Subject: [PATCH 01/33] build(uv.lock): update judas_protocol to 0.7.0 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index a96478f..626d065 100644 --- a/uv.lock +++ b/uv.lock @@ -358,8 +358,8 @@ wheels = [ [[package]] name = "judas-protocol" -version = "0.6.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#d16c1914ba343aed300f1c5fae0201370c3274de" } +version = "0.7.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#48a848bb60bea199020f2b765a6cc3f8acc21067" } [[package]] name = "judas-server" -- 2.39.5 From fa2da207a9cf3d90cdbfb5977033fee21d449528 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 1 Mar 2026 20:17:05 +0100 Subject: [PATCH 02/33] refactor(backend_server.py): refactor calls to Message class constructors after protocol changes --- src/judas_server/backend/backend_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index b1bea71..fac89b1 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -106,7 +106,7 @@ class BackendServer: client (Client): The client to send the ACK to. target_id (str): The id of the ACK'd message. """ - ack: bytes = Message.ack(target_id=target_id).to_bytes() + ack: bytes = Message.Control.ack(target_id=target_id).to_bytes() self.logger.info(f"[>] Sending ACK to {client}") client.outbound += ack @@ -116,7 +116,7 @@ class BackendServer: Args: client (Client): The client to send the CLOSE message to. """ - close_msg: bytes = Message.close().to_bytes() + close_msg: bytes = Message.Control.close().to_bytes() self.logger.info(f"[>] Sending CLOSE to {client}") client.outbound += close_msg -- 2.39.5 From bda10a6248478e99dfb42d8c605996cafdaf1e6e Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 17:41:48 +0100 Subject: [PATCH 03/33] chore(cache/): add cache/ directory --- cache/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cache/.gitkeep diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 -- 2.39.5 From 3eb681e233fa151c2476581854c947bdc77b4900 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 17:43:19 +0100 Subject: [PATCH 04/33] refactor(backend_server.py): move loading known clients to its own method --- src/judas_server/backend/backend_server.py | 74 ++++++++++++++-------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index fac89b1..6dc5c21 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -28,27 +28,6 @@ class BackendServer: ) self.logger.debug("Initializing Server...") - self.known_clients: dict[str, dict[str, str | float]] = {} - try: - with open("cache/known_clients.yaml", "r") as f: - self.known_clients = ( - yaml.safe_load(f).get("known_clients", {}) or {} - ) - self.logger.debug( - f"Loaded known clients: {self.known_clients}" - ) - self.logger.info( - f"Loaded {len(self.known_clients)} known clients" - ) - except FileNotFoundError: - self.logger.warning( - "known_clients.yaml not found, creating empty known clients list" - ) - with open("cache/known_clients.yaml", "w") as f: - yaml.safe_dump({"known_clients": {}}, f) - except Exception as e: - self.logger.error(f"Error loading known clients: {e}") - self.selector = selectors.DefaultSelector() self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt( @@ -63,16 +42,61 @@ class BackendServer: self.clients: dict[str, Client] = {} - if self.known_clients: - for client_id in self.known_clients: + self.known_clients: dict[str, dict[str, str | float]] = ( + self._load_known_clients() + ) + + self.running: bool = False + + def _load_known_clients(self) -> dict[str, dict[str, str | float]]: + """Load the list of known clients from a YAML file and validate.""" + known_clients: dict[str, dict[str, str | float]] = {} + + try: + with open("cache/known_clients.yaml", "r") as f: + data = yaml.safe_load(f) + + if not isinstance(data, dict): + raise ValueError("YAML root must be a dict") + + known_clients = data.get("known_clients", {}) or {} + + if not isinstance(known_clients, dict): + raise ValueError("'known_clients' must be a dict") + + for client_id, client_data in known_clients.items(): + if not isinstance(client_data, dict): + raise ValueError( + f"Client {client_id} data must be a dict" + ) + last_seen = client_data.get("last_seen", 0.0) + if not isinstance(last_seen, (float, int)): + raise ValueError( + f"Client {client_id} 'last_seen' must be a float or int" + ) + + self.logger.debug(f"Loaded known clients: {known_clients}") + self.logger.info(f"Loaded {len(known_clients)} known clients") + + for client_id in known_clients: client = Client(id=client_id, addr=None, socket=None) client.status = ClientStatus.OFFLINE client.last_seen = float( - self.known_clients[client_id].get("last_seen", 0.0) + known_clients[client_id].get("last_seen", 0.0) ) self.clients[client_id] = client - self.running: bool = False + except FileNotFoundError: + self.logger.warning( + "known_clients.yaml not found, creating empty known clients list" + ) + with open("cache/known_clients.yaml", "w") as f: + yaml.safe_dump({"known_clients": {}}, f) + except Exception as e: + self.logger.error(f"Error loading known clients: {e}") + raise + + return known_clients def _save_known_clients(self) -> None: """Save the list of known clients to a YAML file.""" -- 2.39.5 From faecc382610daf890c90074809085560931fd178 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 17:45:01 +0100 Subject: [PATCH 05/33] feat(client_status.py): move `ClientStatus` enum to own module --- src/judas_server/backend/client.py | 10 +--------- src/judas_server/backend/client_status.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 src/judas_server/backend/client_status.py diff --git a/src/judas_server/backend/client.py b/src/judas_server/backend/client.py index 500f9f4..d397760 100644 --- a/src/judas_server/backend/client.py +++ b/src/judas_server/backend/client.py @@ -5,17 +5,9 @@ from __future__ import annotations import logging as lg import socket -from enum import Enum import time - -class ClientStatus(str, Enum): - """Enumeration of client connection statuses.""" - - ONLINE = "online" - PENDING = "pending" - OFFLINE = "offline" - STALE = "stale" +from judas_server.backend.client_status import ClientStatus class Client: diff --git a/src/judas_server/backend/client_status.py b/src/judas_server/backend/client_status.py new file mode 100644 index 0000000..1dabba6 --- /dev/null +++ b/src/judas_server/backend/client_status.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from enum import Enum + + +class ClientStatus(str, Enum): + """Enumeration of client connection statuses.""" + + ONLINE = "online" + PENDING = "pending" + OFFLINE = "offline" + STALE = "stale" -- 2.39.5 From 62acc4b181cd359557909fa94f0cb0e6a4109255 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 17:46:06 +0100 Subject: [PATCH 06/33] style(client.py): correct property typing --- src/judas_server/backend/client.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/judas_server/backend/client.py b/src/judas_server/backend/client.py index d397760..1c20fa2 100644 --- a/src/judas_server/backend/client.py +++ b/src/judas_server/backend/client.py @@ -14,7 +14,10 @@ class Client: """Represents a client.""" def __init__( - self, id: str | None, addr: tuple[str, int], socket: socket.socket + self, + id: str | None, + addr: tuple[str, int] | None, + socket: socket.socket | None, ) -> None: """Initialize the client. @@ -33,13 +36,15 @@ class Client: self.last_seen: float = 0.0 # unix timestanp of last inbound message self.status: ClientStatus = ClientStatus.PENDING - self.socket: socket.socket = socket - self.addr: tuple[str, int] = addr + self.socket: socket.socket | None = socket + self.addr: tuple[str, int] | None = addr self.inbound: bytes = b"" self.outbound: bytes = b"" def __str__(self) -> str: - return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))" + if self.addr: + return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))" + return f"Client({self.id} (not connected))" def __repr__(self) -> str: return f"Client({self.id}, {self.addr})" @@ -47,6 +52,11 @@ class Client: def disconnect(self) -> None: """Disconnect the client and close the socket.""" self.logger.debug(f"Disconnecting Client {self}...") + if self.socket is None: + self.logger.warning( + f"Client {self} not connected, nothing to disconnect." + ) + return try: self.socket.close() except Exception as e: -- 2.39.5 From d3f68d3baff8b903be125b0da7a88b415cd56bac Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 17:46:51 +0100 Subject: [PATCH 07/33] chore(backend/__init__.py): add `Client` and `ClientStatus` to `__all__` --- src/judas_server/backend/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/judas_server/backend/__init__.py b/src/judas_server/backend/__init__.py index 8e31517..60e15d3 100644 --- a/src/judas_server/backend/__init__.py +++ b/src/judas_server/backend/__init__.py @@ -1,3 +1,5 @@ from .backend_server import BackendServer +from .client import Client +from .client_status import ClientStatus -__all__ = ["BackendServer"] +__all__ = ["BackendServer", "Client", "ClientStatus"] -- 2.39.5 From 882c8780e15ec827fac1854cf7419b71d9a41a4f Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:50:57 +0100 Subject: [PATCH 08/33] feat(base_handler.py): add `BaseHandler` class for message handling --- .../backend/handler/base_handler.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/judas_server/backend/handler/base_handler.py diff --git a/src/judas_server/backend/handler/base_handler.py b/src/judas_server/backend/handler/base_handler.py new file mode 100644 index 0000000..e7dfa8c --- /dev/null +++ b/src/judas_server/backend/handler/base_handler.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging as lg +from typing import TYPE_CHECKING + +from judas_server.backend.client import Client + +if TYPE_CHECKING: + from judas_protocol import Message + + from judas_server.backend import BackendServer + + +class BaseHandler: + """BaseHandler is the base class for all message handlers in the backend server. + + It defines the interface for handling messages and provides common functionality for all handlers. + """ + + def __init__(self, backend_server: BackendServer) -> None: + """Initialize the BaseHandler with a reference to the backend server. + + Args: + backend_server (BackendServer): The backend server instance that this handler belongs to. + """ + self.logger: lg.Logger = lg.getLogger( + f"{__name__}.{self.__class__.__name__}" + ) + self.backend_server: BackendServer = backend_server + + def handle(self, client: Client, message: Message) -> None: + """Handle a message from a client. + + This method must be implemented by subclasses to define the specific handling logic for different message types. + + Args: + client (Client): The client that sent the message. + message (Message): The message to be handled. + """ + raise NotImplementedError("handle() must be implemented by subclasses") -- 2.39.5 From c952413d912b7a5d2000bc48c11a4d9274533caf Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:52:39 +0100 Subject: [PATCH 09/33] feat(hello_handler.py): add HELLO message handler --- .../backend/handler/hello_handler.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/judas_server/backend/handler/hello_handler.py diff --git a/src/judas_server/backend/handler/hello_handler.py b/src/judas_server/backend/handler/hello_handler.py new file mode 100644 index 0000000..c2458a5 --- /dev/null +++ b/src/judas_server/backend/handler/hello_handler.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import TYPE_CHECKING, override + +from judas_protocol import Category, ControlAction, Message + +from judas_server.backend.client import ClientStatus +from judas_server.backend.handler import BaseHandler + +if TYPE_CHECKING: + from judas_server.backend.backend_server import BackendServer + from judas_server.backend.client import Client + + +class HelloHandler(BaseHandler): + def __init__(self, backend_server: BackendServer) -> None: + super().__init__(backend_server) + + @override + def handle(self, client: Client, message: Message) -> None: + if client.id is not None: + return + + if ( + message.category != Category.CONTROL + or message.action != ControlAction.HELLO + ): + self.logger.error( + f"Expected HELLO message from {client}, got {message}, disconnecting client..." + ) + self.backend_server._disconnect(client) + return + + if message.payload.get("id") is None: + self.logger.error( + f"HELLO message from {client} missing 'id' field, disconnecting client..." + ) + self.backend_server._disconnect(client) + return + + client.id = message.payload["id"] + + # check if client already connected, if so disconnect old client and register new one + if ( + client.id in self.backend_server.clients + and self.backend_server.clients[client.id].status == "connected" + ): + old_client: Client = self.backend_server.clients[client.id] + self.backend_server.logger.warning( + f"Client {client.id} is already connected from {old_client.addr}, disconnecting old client..." + ) + self.backend_server.send_close(old_client) + return + + self.backend_server.clients[client.id] = client # type: ignore + self.backend_server.known_clients[client.id] = { # type: ignore + "last_seen": client.last_seen + } + + self.backend_server._save_known_clients() + client.status = ClientStatus.ONLINE + + self.logger.info(f"[+] Registered new client {client}") -- 2.39.5 From ec58a5257a3007a6edd502eb3967d92a9b652358 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:53:05 +0100 Subject: [PATCH 10/33] chore(handler/__init__.py): add module init --- src/judas_server/backend/handler/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/judas_server/backend/handler/__init__.py diff --git a/src/judas_server/backend/handler/__init__.py b/src/judas_server/backend/handler/__init__.py new file mode 100644 index 0000000..d133708 --- /dev/null +++ b/src/judas_server/backend/handler/__init__.py @@ -0,0 +1,4 @@ +from .base_handler import BaseHandler +from .hello_handler import HelloHandler + +__all__ = ["BaseHandler", "HelloHandler"] -- 2.39.5 From 6446fe883cea4268992efc9f97cdb493af416576 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:54:45 +0100 Subject: [PATCH 11/33] fix(backend_server.py): check if client to disconnect has an open socket --- src/judas_server/backend/backend_server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 6dc5c21..e62e0bc 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -168,6 +168,12 @@ class BackendServer: """ self.logger.info(f"[-] Disconnecting {client}...") + if client.socket is None: + self.logger.warning( + f"Client {client} has no socket, nothing to disconnect." + ) + return + try: self.selector.unregister(client.socket) except Exception as e: -- 2.39.5 From 0ed478a88efa46f2f28b24371ad68010ecbf88d0 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:55:24 +0100 Subject: [PATCH 12/33] feat(backend_server.py): implement message handling --- src/judas_server/backend/backend_server.py | 48 ++++++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index e62e0bc..4bdee84 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -6,13 +6,18 @@ import selectors import socket import threading import time +from typing import TYPE_CHECKING, Any + import yaml - -from typing import Any - from judas_protocol import Category, ControlAction, Message from judas_server.backend.client import Client, ClientStatus +from judas_server.backend.handler.hello_handler import HelloHandler + +if TYPE_CHECKING: + from typing import Callable + + from judas_protocol import ActionType class BackendServer: @@ -41,13 +46,25 @@ class BackendServer: ) self.clients: dict[str, Client] = {} - self.known_clients: dict[str, dict[str, str | float]] = ( self._load_known_clients() ) + self.message_handlers: dict[ + tuple[Category, ActionType], Callable[[Client, Message], None] + ] = {} + self.running: bool = False + def _initialize_handlers(self) -> None: + """Initialize message handlers.""" + + hello_handler = HelloHandler(self) + + self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = ( + hello_handler.handle + ) + def _load_known_clients(self) -> dict[str, dict[str, str | float]]: """Load the list of known clients from a YAML file and validate.""" known_clients: dict[str, dict[str, str | float]] = {} @@ -286,6 +303,29 @@ class BackendServer: try: msg = Message.from_bytes(line) self.logger.info(f"[.] Parsed message {msg.id}") + + if client.id is None: + self.logger.debug( + f"Client {client} has no ID, expecting HELLO message..." + ) + if ( + msg.category != Category.CONTROL + or msg.action != ControlAction.HELLO + ): + self.logger.warning( + f"First message from {client} must be HELLO, disconnecting..." + ) + self._disconnect(client) + return + + handler: Callable[[Client, Message], None] | None = ( + self.message_handlers.get( + (msg.category, msg.action), None + ) + ) + if handler is not None: + handler(client, msg) + if msg.ack_required: self.send_ack(client, target_id=msg.id) except Exception as e: -- 2.39.5 From ee381414a931ed9d7562410e08c9de5cdc7b94e7 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:55:45 +0100 Subject: [PATCH 13/33] chore(backend_server.py): remove redundant HELLO msg handling --- src/judas_server/backend/backend_server.py | 42 ---------------------- 1 file changed, 42 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 4bdee84..f4ab627 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -253,48 +253,6 @@ class BackendServer: self._disconnect(client) return - if client.id is None: - # expect HELLO message - try: - msg = Message.from_bytes(client.inbound) - except Exception as e: - self.logger.error( - f"Failed to parse HELLO message from {client}: {e}" - ) - self._disconnect(client) - return - - if ( - msg.category == Category.CONTROL - and msg.action == ControlAction.HELLO - and msg.payload.get("id") is not None - ): - client.id = msg.payload["id"] - if ( - client.id in self.clients - and self.clients[client.id].status == "connected" - ): - old_client: Client = self.clients[client.id] - self.logger.warning( - f"Client {client.id} is already connected from {old_client.addr}, disconnecting old client..." - ) - self.send_close(old_client) - - self.clients[client.id] = client - self.known_clients[client.id] = { - "last_seen": client.last_seen - } - self._save_known_clients() - client.status = ClientStatus.ONLINE - - self.logger.info(f"[+] Registered new client {client}") - else: - self.logger.error( - f"Expected HELLO message from {client}, got {msg}" - ) - self._disconnect(client) - return - while b"\n" in client.inbound: line, client.inbound = client.inbound.split(b"\n", 1) self.logger.debug( -- 2.39.5 From ead22240660ff9f723c36ee35fa5918c33a0c66d Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 18:56:34 +0100 Subject: [PATCH 14/33] fix(backend_server.py): do not disconnect a client if Exception raised on msg handling --- src/judas_server/backend/backend_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index f4ab627..60e8019 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -290,7 +290,6 @@ class BackendServer: self.logger.error( f"Failed to parse message from {client}: {e}" ) - self._disconnect(client) return if mask & selectors.EVENT_WRITE and client.outbound: -- 2.39.5 From c64a2582439e5ef1ab68bb9c588f7c4644bb4e07 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 19:34:32 +0100 Subject: [PATCH 15/33] build(uv.lock): update judas_protocol to 0.8.0 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 626d065..1c9901a 100644 --- a/uv.lock +++ b/uv.lock @@ -358,8 +358,8 @@ wheels = [ [[package]] name = "judas-protocol" -version = "0.7.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#48a848bb60bea199020f2b765a6cc3f8acc21067" } +version = "0.8.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" } [[package]] name = "judas-server" -- 2.39.5 From dafe418916485258694949538eda151d7da33695 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:18:24 +0100 Subject: [PATCH 16/33] feat(backend_server.py): add warning if received an unknown message (no handler) --- src/judas_server/backend/backend_server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 60e8019..3d42da3 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -274,7 +274,7 @@ class BackendServer: f"First message from {client} must be HELLO, disconnecting..." ) self._disconnect(client) - return + continue handler: Callable[[Client, Message], None] | None = ( self.message_handlers.get( @@ -283,6 +283,11 @@ class BackendServer: ) if handler is not None: handler(client, msg) + else: + self.logger.warning( + f"No handler for message {msg.id} with category {msg.category} and action {msg.action}" + ) + continue if msg.ack_required: self.send_ack(client, target_id=msg.id) -- 2.39.5 From e308a07dabcf20d9b080ce12a8928760dcde3057 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:38:55 +0100 Subject: [PATCH 17/33] fix(backend_server.py): call `_initialize_handlers()` on init --- src/judas_server/backend/backend_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 3d42da3..98f49d4 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -53,6 +53,7 @@ class BackendServer: self.message_handlers: dict[ tuple[Category, ActionType], Callable[[Client, Message], None] ] = {} + self._initialize_handlers() self.running: bool = False -- 2.39.5 From c88e39c7355b6f8c982198e6e56da2d09d17c61f Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:39:53 +0100 Subject: [PATCH 18/33] feat(backend_server.py): track message ACKs and resend if no ACK recv'd within 5 seconds --- src/judas_server/backend/backend_server.py | 35 +++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 98f49d4..7bf4f20 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -55,6 +55,8 @@ class BackendServer: ] = {} self._initialize_handlers() + self.pending_acks: list[tuple[Client, Message, float]] = [] + self.running: bool = False def _initialize_handlers(self) -> None: @@ -141,6 +143,22 @@ class BackendServer: ) time.sleep(1) + def send(self, client: Client, msg: Message) -> None: + """Send a message to a client. + + Args: + client (Client): The client to send the message to. + msg (Message): The message to send. + """ + msg_bytes: bytes = msg.to_bytes() + self.logger.info( + f"[>] Sending message {msg.id} to {client}, category: {msg.category}, action: {msg.action}, ack_required: {msg.ack_required}" + ) + self.logger.debug(f"[>] Message bytes: {msg_bytes!r}") + if msg.ack_required: + self.pending_acks.append((client, msg, time.time())) + client.outbound += msg_bytes + def send_ack(self, client: Client, target_id: str) -> None: """Send an ACK message to a client. @@ -148,9 +166,9 @@ class BackendServer: client (Client): The client to send the ACK to. target_id (str): The id of the ACK'd message. """ - ack: bytes = Message.Control.ack(target_id=target_id).to_bytes() + ack: Message = Message.Control.ack(target_id=target_id) self.logger.info(f"[>] Sending ACK to {client}") - client.outbound += ack + self.send(client, ack) def send_close(self, client: Client) -> None: """Send a CLOSE message to a client. @@ -158,9 +176,9 @@ class BackendServer: Args: client (Client): The client to send the CLOSE message to. """ - close_msg: bytes = Message.Control.close().to_bytes() + close_msg: Message = Message.Control.close() self.logger.info(f"[>] Sending CLOSE to {client}") - client.outbound += close_msg + self.send(client, close_msg) def _accept_connection(self, sock: socket.socket) -> None: """Accept a new client connection. @@ -335,6 +353,15 @@ class BackendServer: and now - client.last_seen > 60 * 60 * 24 # 24 hours ): self.clients[client.id].status = ClientStatus.STALE + + # check pending ACKs + for client, msg, timestamp in self.pending_acks[:]: + if time.time() - timestamp > 5: # 5 second timeout + self.logger.warning( + f"ACK timeout for message {msg.id} to {client}, resending..." + ) + self.send(client, msg) + self.pending_acks.remove((client, msg, timestamp)) time.sleep(0.001) # prevent 100% CPU usage except Exception as e: -- 2.39.5 From a9bace8acaab07e6e5d02155a9966c6381ad107e Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:41:58 +0100 Subject: [PATCH 19/33] feat(backend_server.py): add `ACK_TIMEOUT` constant --- src/judas_server/backend/backend_server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 7bf4f20..dfdbcf0 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -6,7 +6,7 @@ import selectors import socket import threading import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final import yaml from judas_protocol import Category, ControlAction, Message @@ -21,6 +21,8 @@ if TYPE_CHECKING: class BackendServer: + ACK_TIMEOUT: Final[float] = 5.0 # seconds + def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None: """Initialize the backend server. @@ -356,12 +358,13 @@ class BackendServer: # check pending ACKs for client, msg, timestamp in self.pending_acks[:]: - if time.time() - timestamp > 5: # 5 second timeout + if time.time() - timestamp > self.ACK_TIMEOUT: self.logger.warning( f"ACK timeout for message {msg.id} to {client}, resending..." ) self.send(client, msg) self.pending_acks.remove((client, msg, timestamp)) + time.sleep(0.001) # prevent 100% CPU usage except Exception as e: -- 2.39.5 From bf1ad0ead0e4fc35c20d0d747cca05388b223d35 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:55:09 +0100 Subject: [PATCH 20/33] feat(ack_handler.py): add handling for ACKs --- .../backend/handler/ack_handler.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/judas_server/backend/handler/ack_handler.py diff --git a/src/judas_server/backend/handler/ack_handler.py b/src/judas_server/backend/handler/ack_handler.py new file mode 100644 index 0000000..8204be0 --- /dev/null +++ b/src/judas_server/backend/handler/ack_handler.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from typing import TYPE_CHECKING + +from .base_handler import BaseHandler + +if TYPE_CHECKING: + from judas_protocol import Message + + from judas_server.backend import BackendServer, Client + + +class AckHandler(BaseHandler): + def __init__(self, backend_server: BackendServer) -> None: + super().__init__(backend_server) + + def handle(self, client: Client, message: Message) -> None: + pending_acks = self.backend_server.pending_acks + if message.id in pending_acks: + del pending_acks[message.id] + self.logger.debug( + f"[*] Received ACK for message {message.id} from {client}." + ) + else: + self.logger.warning( + f"[!] Received ACK for unknown (or ACK'd) message {message.id} from {client}." + ) -- 2.39.5 From f5b14fc6109e83730c4b13aaecf6f4314c0136a4 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:54:04 +0100 Subject: [PATCH 21/33] feat(backend_server.py): add timeout on HELLO --- src/judas_server/backend/backend_server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index dfdbcf0..910a969 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: class BackendServer: ACK_TIMEOUT: Final[float] = 5.0 # seconds + HELLO_TIMEOUT: Final[float] = 3.0 # seconds def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None: """Initialize the backend server. @@ -58,6 +59,7 @@ class BackendServer: self._initialize_handlers() self.pending_acks: list[tuple[Client, Message, float]] = [] + self.pending_hello: dict[Client, float] = {} self.running: bool = False @@ -196,6 +198,8 @@ class BackendServer: events = selectors.EVENT_READ | selectors.EVENT_WRITE self.selector.register(conn, events, data=client) + self.pending_hello[client] = time.time() + self.logger.info(f"[+] Registered client {client}, HELLO pending...") def _disconnect(self, client: Client) -> None: @@ -365,6 +369,15 @@ class BackendServer: self.send(client, msg) self.pending_acks.remove((client, msg, timestamp)) + # check pending HELLOs + for client, timestamp in list(self.pending_hello.items()): + if time.time() - timestamp > self.HELLO_TIMEOUT: + self.logger.warning( + f"HELLO timeout for {client}, disconnecting..." + ) + self._disconnect(client) + del self.pending_hello[client] + time.sleep(0.001) # prevent 100% CPU usage except Exception as e: -- 2.39.5 From 97fc17fbb3ed5d681a4f5a0d488b7a2b09ab35df Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:54:29 +0100 Subject: [PATCH 22/33] feat(hello_handler.py): remove client from `pending_hello` if recv'd HELLO --- src/judas_server/backend/handler/hello_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/judas_server/backend/handler/hello_handler.py b/src/judas_server/backend/handler/hello_handler.py index c2458a5..747632f 100644 --- a/src/judas_server/backend/handler/hello_handler.py +++ b/src/judas_server/backend/handler/hello_handler.py @@ -58,6 +58,7 @@ class HelloHandler(BaseHandler): "last_seen": client.last_seen } + del self.backend_server.pending_hello[client] self.backend_server._save_known_clients() client.status = ClientStatus.ONLINE -- 2.39.5 From e54cc479b5f6bb3fb887803122fea0c551e27ee2 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 20:56:18 +0100 Subject: [PATCH 23/33] chore(handler/__init__.py): add `AckHandler` to `__all__` --- src/judas_server/backend/handler/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/judas_server/backend/handler/__init__.py b/src/judas_server/backend/handler/__init__.py index d133708..dc07972 100644 --- a/src/judas_server/backend/handler/__init__.py +++ b/src/judas_server/backend/handler/__init__.py @@ -1,4 +1,5 @@ from .base_handler import BaseHandler from .hello_handler import HelloHandler +from .ack_handler import AckHandler -__all__ = ["BaseHandler", "HelloHandler"] +__all__ = ["BaseHandler", "HelloHandler", "AckHandler"] -- 2.39.5 From 332238b403782a08a8974b9267071f8d0ff5f006 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 21:55:56 +0100 Subject: [PATCH 24/33] chore(__init__.py): correct version --- src/judas_server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/judas_server/__init__.py b/src/judas_server/__init__.py index c66c5db..5bcf5d2 100644 --- a/src/judas_server/__init__.py +++ b/src/judas_server/__init__.py @@ -1 +1 @@ -__version__: str = "0.1.0" +__version__: str = "0.5.0" -- 2.39.5 From 88d349090e6e3605800656cc3e85779bf4b417c3 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 3 Mar 2026 22:20:36 +0100 Subject: [PATCH 25/33] fix(backend_server.py): fix double disconnect if client.inbound empty --- src/judas_server/backend/backend_server.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 910a969..ead4fd9 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -210,7 +210,7 @@ class BackendServer: """ self.logger.info(f"[-] Disconnecting {client}...") - if client.socket is None: + if client.socket is None or client.socket._closed: self.logger.warning( f"Client {client} has no socket, nothing to disconnect." ) @@ -274,9 +274,6 @@ class BackendServer: try: if mask & selectors.EVENT_READ: self._receive_inbound(sock, client) - if not client.inbound: - self._disconnect(client) - return while b"\n" in client.inbound: line, client.inbound = client.inbound.split(b"\n", 1) -- 2.39.5 From 35899d366823c2878330eb06dc703f2965f61c3f Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Wed, 4 Mar 2026 09:23:21 +0100 Subject: [PATCH 26/33] refactor(backend_server.py): move `known_clients.yaml` to `config/` --- src/judas_server/backend/backend_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index ead4fd9..03bc6e2 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -77,7 +77,7 @@ class BackendServer: known_clients: dict[str, dict[str, str | float]] = {} try: - with open("cache/known_clients.yaml", "r") as f: + with open("config/known_clients.yaml", "r") as f: data = yaml.safe_load(f) if not isinstance(data, dict): @@ -114,7 +114,7 @@ class BackendServer: self.logger.warning( "known_clients.yaml not found, creating empty known clients list" ) - with open("cache/known_clients.yaml", "w") as f: + with open("config/known_clients.yaml", "w") as f: yaml.safe_dump({"known_clients": {}}, f) except Exception as e: self.logger.error(f"Error loading known clients: {e}") @@ -124,7 +124,7 @@ class BackendServer: def _save_known_clients(self) -> None: """Save the list of known clients to a YAML file.""" - with open("cache/known_clients.yaml", "w") as f: + with open("config/known_clients.yaml", "w") as f: yaml.safe_dump({"known_clients": self.known_clients}, f) self.logger.debug("Saved known clients") -- 2.39.5 From 0e7c9486683be4ceb91203109986f0b9fc83eb8b Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Wed, 4 Mar 2026 09:23:54 +0100 Subject: [PATCH 27/33] feat(config/): add `config/` directory --- config/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 config/.gitkeep diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 -- 2.39.5 From fbb75c263c13726d7e1c3fa28fb4e8f1f8cf0a17 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 13:01:09 +0100 Subject: [PATCH 28/33] fix(backend_server.py): fix double-connection handling --- src/judas_server/backend/handler/hello_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/judas_server/backend/handler/hello_handler.py b/src/judas_server/backend/handler/hello_handler.py index 747632f..a9d78a1 100644 --- a/src/judas_server/backend/handler/hello_handler.py +++ b/src/judas_server/backend/handler/hello_handler.py @@ -44,7 +44,8 @@ class HelloHandler(BaseHandler): # check if client already connected, if so disconnect old client and register new one if ( client.id in self.backend_server.clients - and self.backend_server.clients[client.id].status == "connected" + and self.backend_server.clients[client.id].status + == ClientStatus.ONLINE ): old_client: Client = self.backend_server.clients[client.id] self.backend_server.logger.warning( -- 2.39.5 From f13f243b5b389d84acaa4ab933a234a00c6a981c Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 13:14:08 +0100 Subject: [PATCH 29/33] chore(__main__.py): set logging level for werkzeug to WARNING --- src/judas_server/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/judas_server/__main__.py b/src/judas_server/__main__.py index d03289a..e850375 100644 --- a/src/judas_server/__main__.py +++ b/src/judas_server/__main__.py @@ -12,6 +12,8 @@ if __name__ == "__main__": format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s", ) + lg.getLogger("werkzeug").setLevel(lg.WARNING) + ladygaga_logger = lg.getLogger(f"{__name__}.LAGA_DYGA") ladygaga_logger.info(LADY_GAGA) -- 2.39.5 From f46d27164b79107abe405b221c9d424add55a723 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 20:38:26 +0100 Subject: [PATCH 30/33] chore(.gitignore): ignore `config/known_clients.yaml` as it's generated automatically --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 07febb3..181b2a7 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ logs/ # Sphinx docs/_build/ docs/ref/modules/ + +# known clients +config/known_clients.yaml -- 2.39.5 From efbf99f356c20fbd48937f1c4e8df0a2d57f0651 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 20:51:53 +0100 Subject: [PATCH 31/33] refactor(backend_server.py): if known_clients.yaml not present, call `_save_known_clients()` --- src/judas_server/backend/backend_server.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 03bc6e2..4529297 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -49,9 +49,9 @@ class BackendServer: ) self.clients: dict[str, Client] = {} - self.known_clients: dict[str, dict[str, str | float]] = ( - self._load_known_clients() - ) + + self.known_clients: dict[str, dict[str, str | float]] = {} + self.known_clients = self._load_known_clients() self.message_handlers: dict[ tuple[Category, ActionType], Callable[[Client, Message], None] @@ -114,8 +114,7 @@ class BackendServer: self.logger.warning( "known_clients.yaml not found, creating empty known clients list" ) - with open("config/known_clients.yaml", "w") as f: - yaml.safe_dump({"known_clients": {}}, f) + self._save_known_clients() except Exception as e: self.logger.error(f"Error loading known clients: {e}") raise -- 2.39.5 From 72d51b451fc11ebe2f38d5e50bf263f9f441b03c Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 20:52:18 +0100 Subject: [PATCH 32/33] feat(backend_server.py): add notice to known_clients.yaml --- src/judas_server/backend/backend_server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 4529297..cce28a7 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -124,6 +124,11 @@ class BackendServer: def _save_known_clients(self) -> None: """Save the list of known clients to a YAML file.""" with open("config/known_clients.yaml", "w") as f: + f.write( + "# This file is automatically generated by BackendServer.\n" + + "# Do not edit manually.\n" + + f"# Generated at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}\n\n" + ) yaml.safe_dump({"known_clients": self.known_clients}, f) self.logger.debug("Saved known clients") -- 2.39.5 From 61be674258dcce8a5b54efddb7d1d856c6cba9bc Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Thu, 5 Mar 2026 20:52:41 +0100 Subject: [PATCH 33/33] chore: remove `cache/` directory --- cache/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cache/.gitkeep diff --git a/cache/.gitkeep b/cache/.gitkeep deleted file mode 100644 index e69de29..0000000 -- 2.39.5