From f0eeeb79a188548c3f306f4bc875bacc6fd89588 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Mon, 22 Sep 2025 21:56:40 +0200 Subject: [PATCH 01/13] fix(backend_server.py): handle unregister exceptions --- src/judas_server/backend/backend_server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index be7c124..ebf8708 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -123,7 +123,10 @@ class BackendServer: sock (socket.socket): The client socket to disconnect. """ self.logger.info(f"[-] Disconnecting {client}") - self.selector.unregister(client.socket) + try: + self.selector.unregister(client.socket) + except Exception as e: + self.logger.error(f"Error unregistering client {client}: {e}") client.disconnect() def _handle_connection( -- 2.39.5 From 6f5fa33a122e9ebdf9df91628be67721cfc031e3 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Mon, 22 Sep 2025 21:57:43 +0200 Subject: [PATCH 02/13] feat(backend_server.py): handle all other client Exceptions in `_handle_connection()` --- src/judas_server/backend/backend_server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index ebf8708..89930c6 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -171,9 +171,13 @@ class BackendServer: client.outbound = client.outbound[sent:] # TODO: wait for ACK from client + except ConnectionResetError as e: self.logger.error(f"Connection reset by {client}, disconnect: {e}") self._disconnect(client) + except Exception as e: + self.logger.error(f"Connection error for {client}: {e}") + self._disconnect(client) def run(self) -> None: """Start the backend server.""" -- 2.39.5 From 50248621650590c17fcf96c0cec9618a2b0dd07c Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Mon, 22 Sep 2025 21:58:59 +0200 Subject: [PATCH 03/13] fix(backend_server.py): add 1ms sleep to prevent 100% CPU usage in `_loop()` --- src/judas_server/backend/backend_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 89930c6..0411a15 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -197,6 +197,8 @@ class BackendServer: self._accept_connection(key.fileobj) else: self._handle_connection(key, mask) + time.sleep(0.001) # prevent 100% CPU usage + except Exception as e: self.logger.error(f"Server error: {e}") raise e -- 2.39.5 From 1211ca20294dcc3907dbadfee4b294cf7e24a8f9 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Mon, 22 Sep 2025 21:59:06 +0200 Subject: [PATCH 04/13] build(uv.lock): update judas_protocol to 0.2.0 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index f849611..fc6d175 100644 --- a/uv.lock +++ b/uv.lock @@ -319,8 +319,8 @@ wheels = [ [[package]] name = "judas-protocol" -version = "0.1.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#fd070b176347a0f7b81f937b189d8f50736f3514" } +version = "0.2.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#bc1bf46388eb904738893a2f86b5050b4ce2489e" } [[package]] name = "judas-server" -- 2.39.5 From 721ab87e7156045703f55ecefc6ed3e15ffa36aa Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Tue, 23 Sep 2025 23:31:27 +0200 Subject: [PATCH 05/13] build(uv.lock): update judas_protocol to 0.3.0 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index fc6d175..ab89936 100644 --- a/uv.lock +++ b/uv.lock @@ -319,8 +319,8 @@ wheels = [ [[package]] name = "judas-protocol" -version = "0.2.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#bc1bf46388eb904738893a2f86b5050b4ce2489e" } +version = "0.3.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#c25ee1ebdfff8ff51bf00131732720091562e101" } [[package]] name = "judas-server" -- 2.39.5 From cee30251ddb87b1f3ca84d0f3c8cf40fb37debcc Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 5 Oct 2025 13:13:10 +0200 Subject: [PATCH 06/13] build(uv.lock): update judas_protocol to 0.4.3 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index ab89936..c025daa 100644 --- a/uv.lock +++ b/uv.lock @@ -319,8 +319,8 @@ wheels = [ [[package]] name = "judas-protocol" -version = "0.3.0" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#c25ee1ebdfff8ff51bf00131732720091562e101" } +version = "0.4.3" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#5ef300ff93bb43d4db28ae019fec30f48f88152b" } [[package]] name = "judas-server" -- 2.39.5 From fe7d78c1c8cd1d0e13276ce82549ee97b896aff0 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Wed, 19 Nov 2025 21:46:31 +0100 Subject: [PATCH 07/13] build(uv.lock): update judas_protocol to 0.5.0 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index c025daa..f007aa1 100644 --- a/uv.lock +++ b/uv.lock @@ -319,8 +319,8 @@ wheels = [ [[package]] name = "judas-protocol" -version = "0.4.3" -source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#5ef300ff93bb43d4db28ae019fec30f48f88152b" } +version = "0.5.0" +source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#c48b69ecee16f5824ffd8bce8921341d5fa326b7" } [[package]] name = "judas-server" -- 2.39.5 From 61a607c20e205c18e60a9b26f6fb3c37a47d6a55 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Fri, 21 Nov 2025 18:39:44 +0100 Subject: [PATCH 08/13] refactor: wip --- src/judas_server/backend/backend_server.py | 157 ++++++++++++++------- src/judas_server/backend/client.py | 11 +- 2 files changed, 109 insertions(+), 59 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 0411a15..8cb64c9 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -7,7 +7,7 @@ import socket import threading import time -from judas_protocol import Message +from judas_protocol import Category, ControlAction, Message from judas_server.backend.client import Client @@ -60,13 +60,14 @@ class BackendServer: ) time.sleep(1) - def _send_ack(self, client: Client) -> None: + def _send_ack(self, client: Client, target_id: str) -> None: """Send an ACK message to a client. Args: client (Client): The client to send the ACK to. + target_id (str): The id of the ACK'd message. """ - ack: bytes = Message.ack().to_bytes() + ack: bytes = Message.ack(target_id=target_id).to_bytes() self.logger.debug(f"[>] Sending ACK to {client}") client.outbound += ack @@ -78,43 +79,13 @@ class BackendServer: """ conn, addr = sock.accept() self.logger.info(f"[+] Accepted connection from {addr}") - conn.setblocking(False) - # wait for hello message to get mac_id - - conn.settimeout(5) - try: - message = conn.recv(1024) - if not message: - self.logger.error(f"[-] No data received from {addr}") - conn.close() - return - except socket.timeout: - self.logger.error(f"[-] Timeout waiting for hello from {addr}") - conn.close() - return - conn.settimeout(None) - - message = message.split(b"\n")[0] # get first line only - message = Message.from_bytes(message) - - mac_id = message.payload.get("mac", None) - if mac_id is None: - self.logger.error( - f"[-] No mac_id provided by {addr}, closing connection" - ) - conn.close() - return - - client = Client(id_=mac_id, addr=addr, socket=conn) - self.clients[mac_id] = client - - self._send_ack(client) + client = Client(mac_id=None, addr=addr, socket=conn) events = selectors.EVENT_READ | selectors.EVENT_WRITE self.selector.register(conn, events, data=client) - self.logger.info(f"[+] Registered client {client}") + self.logger.info(f"[+] Registered client {client}, HELLO pending...") def _disconnect(self, client: Client) -> None: """Disconnect a client and clean up resources. @@ -122,13 +93,54 @@ class BackendServer: Args: sock (socket.socket): The client socket to disconnect. """ - self.logger.info(f"[-] Disconnecting {client}") + self.logger.info(f"[-] Disconnecting {client}...") + self.logger.debug("[*] Sending DNR message...") + try: self.selector.unregister(client.socket) except Exception as e: self.logger.error(f"Error unregistering client {client}: {e}") + client.disconnect() + def _send_outbound( + self, sock: socket.socket, client: Client, data: bytes + ) -> None: + """Queue data to be sent to a client. + + Args: + client (Client): The client to send data to. + data (bytes): The data to send. + """ + self.logger.debug(f"[>] Sending data to {client}: {client.outbound!r}") + sent = sock.send(client.outbound) + + client.outbound = client.outbound[sent:] + + def _receive_inbound( + self, sock: socket.socket, client: Client, packet_size: int = 4096 + ) -> None: + """Receive data from a client socket. + + Args: + sock (socket.socket): The client socket to receive data from. + client (Client): The client object. + packet_size (int): The maximum amount of data to be received at once. + Returns: + bytes: The received data. + """ + recv_data = sock.recv(1024) + if recv_data: + self.logger.debug( + f"[<] Received data from {client}: {recv_data!r}" + ) + client.inbound += recv_data + + # set last seen + client.last_seen = time.time() + else: + self._disconnect(client) + def _handle_connection( self, key: selectors.SelectorKey, mask: int ) -> None: @@ -138,39 +150,76 @@ class BackendServer: key (selectors.SelectorKey): The selector key for the client. mask (int): The event mask. """ - sock: socket.socket = key.fileobj + sock: socket.socket = key.fileobj # type: ignore client = key.data try: if mask & selectors.EVENT_READ: - recv_data = sock.recv(1024) - if recv_data: - self.logger.debug( - f"[<] Received data from {client}: {recv_data!r}" - ) - client.inbound += recv_data + self._receive_inbound(sock, client) + if client.inbound: + if client.mac_id is None: + # expect HELLO message + try: + msg = Message.from_bytes(client.inbound) + if ( + msg.category == Category.CONTROL + and msg.action == ControlAction.HELLO + and msg.payload.get("mac") is not None + ): + client.mac_id = msg.payload["mac"] + if ( + client.mac_id in self.clients + and self.clients[client.mac_id].status + == "connected" + ): + old_client: Client = self.clients[ + client.mac_id + ] + self.logger.warning( + f"Client {client.mac_id} is already connected from {old_client.addr}, disconnecting old client..." + ) + self._disconnect(old_client) + # TODO: tell client not to reconnect + self.clients[client.mac_id] = client + self.logger.info( + f"[+] Registered new client {client}" + ) + else: + self.logger.error( + f"Expected HELLO message from {client}, got {msg}" + ) + self._disconnect(client) + return + except Exception as e: + self.logger.error( + f"Failed to parse HELLO message from {client}: {e}" + ) + self._disconnect(client) + return while b"\n" in client.inbound: line, client.inbound = client.inbound.split(b"\n", 1) self.logger.info( f"[<] Complete message from {client}: {line!r}" ) + try: + msg = Message.from_bytes(line) + self.logger.info(f"[.] Parsed message {msg.id}") + if msg.ack_required: + self._send_ack(client, target_id=msg.id) - self._send_ack(client) + except Exception as e: + self.logger.error( + f"Failed to parse message from {client}: {e}" + ) + self._disconnect(client) + return - # set last seen - client.last_seen = time.time() else: self._disconnect(client) if mask & selectors.EVENT_WRITE: if client.outbound: - self.logger.debug( - f"[>] Sending data to {client}: {client.outbound!r}" - ) - sent = sock.send(client.outbound) - - client.outbound = client.outbound[sent:] - # TODO: wait for ACK from client + self._send_outbound(sock, client, client.outbound) except ConnectionResetError as e: self.logger.error(f"Connection reset by {client}, disconnect: {e}") @@ -213,7 +262,7 @@ class BackendServer: self.logger.warning(f"Client {client_id} not found") return None return { - "id": client.id, + "id": client.mac_id, "addr": client.addr, "last_seen": client.last_seen, "status": client.status, diff --git a/src/judas_server/backend/client.py b/src/judas_server/backend/client.py index dd35fde..b680250 100644 --- a/src/judas_server/backend/client.py +++ b/src/judas_server/backend/client.py @@ -17,12 +17,13 @@ class Client: """Represents a client.""" def __init__( - self, id_: str, addr: tuple[str, int], socket: socket.socket + self, mac_id: str | None, addr: tuple[str, int], socket: socket.socket ) -> None: """Initialize the client. Args: - id_ (str): The unique identifier for the client. + mac_id (str | None): The unique identifier for the client. + Can be None if not yet assigned. addr (tuple[str, int]): The (IP, port) address of the client. socket (socket.socket): The socket object for communication. """ @@ -31,7 +32,7 @@ class Client: ) self.logger.debug(f"Initializing Client {addr}...") - self.id: str = id_ + self.mac_id: str | None = mac_id self.last_seen: float = 0.0 # unix timestanp of last inbound message self.status: ClientStatus = ClientStatus.CONNECTED @@ -41,10 +42,10 @@ class Client: self.outbound: bytes = b"" def __str__(self) -> str: - return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))" + return f"Client({self.mac_id} ({self.addr[0]}:{self.addr[1]}))" def __repr__(self) -> str: - return f"Client({self.id}, {self.addr})" + return f"Client({self.mac_id}, {self.addr})" def disconnect(self) -> None: """Disconnect the client and close the socket.""" -- 2.39.5 From faf1f4eeee341f5f8d234dc7d93ae5c86e276217 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 30 Nov 2025 18:10:37 +0100 Subject: [PATCH 09/13] refactor(backend_server.py): remove unused argument in `BackendServer._send_outbound()` --- src/judas_server/backend/backend_server.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 8cb64c9..6338a02 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -103,14 +103,11 @@ class BackendServer: client.disconnect() - def _send_outbound( - self, sock: socket.socket, client: Client, data: bytes - ) -> None: + def _send_outbound(self, sock: socket.socket, client: Client) -> None: """Queue data to be sent to a client. Args: client (Client): The client to send data to. - data (bytes): The data to send. """ self.logger.debug(f"[>] Sending data to {client}: {client.outbound!r}") sent = sock.send(client.outbound) @@ -219,7 +216,7 @@ class BackendServer: if mask & selectors.EVENT_WRITE: if client.outbound: - self._send_outbound(sock, client, client.outbound) + self._send_outbound(sock, client) except ConnectionResetError as e: self.logger.error(f"Connection reset by {client}, disconnect: {e}") -- 2.39.5 From cc6b650f5cd1f3889c251fbe7b36c681be48b2a0 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 30 Nov 2025 18:11:41 +0100 Subject: [PATCH 10/13] fix(backend_server.py): import Any from typing --- src/judas_server/backend/backend_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 6338a02..7beb0a6 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -7,6 +7,8 @@ import socket import threading import time +from typing import Any + from judas_protocol import Category, ControlAction, Message from judas_server.backend.client import Client -- 2.39.5 From f365139e9f064de309f655296b99868833ba7dce Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 30 Nov 2025 18:12:24 +0100 Subject: [PATCH 11/13] style(backend_server.py): ignore type error in `_handle_connection()` --- src/judas_server/backend/backend_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 7beb0a6..19e929e 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -242,7 +242,7 @@ class BackendServer: events = self.selector.select(timeout=1) for key, mask in events: if key.data is None: - self._accept_connection(key.fileobj) + self._accept_connection(key.fileobj) # type: ignore else: self._handle_connection(key, mask) time.sleep(0.001) # prevent 100% CPU usage -- 2.39.5 From 8d2b8f9519bb809df45ba79ea53f12775c3097d4 Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 30 Nov 2025 18:30:59 +0100 Subject: [PATCH 12/13] refactor(backend_server.py): change log levels --- src/judas_server/backend/backend_server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 19e929e..67eff44 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -70,7 +70,7 @@ class BackendServer: target_id (str): The id of the ACK'd message. """ ack: bytes = Message.ack(target_id=target_id).to_bytes() - self.logger.debug(f"[>] Sending ACK to {client}") + self.logger.info(f"[>] Sending ACK to {client}") client.outbound += ack def _accept_connection(self, sock: socket.socket) -> None: @@ -195,9 +195,10 @@ class BackendServer: ) self._disconnect(client) return + while b"\n" in client.inbound: line, client.inbound = client.inbound.split(b"\n", 1) - self.logger.info( + self.logger.debug( f"[<] Complete message from {client}: {line!r}" ) try: -- 2.39.5 From 115deaab4b0aaa9f0d3577be56e64fa3bb03f76b Mon Sep 17 00:00:00 2001 From: Artur Borecki Date: Sun, 30 Nov 2025 18:32:48 +0100 Subject: [PATCH 13/13] fix(backend_server.py): use unused `packet_size` argument to `_receive_inbound()` --- src/judas_server/backend/backend_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/judas_server/backend/backend_server.py b/src/judas_server/backend/backend_server.py index 67eff44..86cdcf1 100644 --- a/src/judas_server/backend/backend_server.py +++ b/src/judas_server/backend/backend_server.py @@ -128,7 +128,7 @@ class BackendServer: Returns: bytes: The received data. """ - recv_data = sock.recv(1024) + recv_data = sock.recv(packet_size) if recv_data: self.logger.debug( f"[<] Received data from {client}: {recv_data!r}" -- 2.39.5