21 Commits

Author SHA1 Message Date
github-actions[bot]
6a024bbfc6 chore(release): 0.6.0 2026-03-05 19:59:49 +00:00
e5ca1066fd Merge pull request 'chore(release): 0.6.0' (#14) from release/0.6.0 into main
Reviewed-on: #14
2026-03-05 19:59:05 +00:00
61be674258 chore: remove cache/ directory 2026-03-05 20:52:41 +01:00
72d51b451f feat(backend_server.py): add notice to known_clients.yaml 2026-03-05 20:52:18 +01:00
efbf99f356 refactor(backend_server.py): if known_clients.yaml not present, call _save_known_clients() 2026-03-05 20:51:53 +01:00
f46d27164b chore(.gitignore): ignore config/known_clients.yaml as it's generated automatically 2026-03-05 20:38:26 +01:00
f13f243b5b chore(__main__.py): set logging level for werkzeug to WARNING 2026-03-05 13:14:08 +01:00
fbb75c263c fix(backend_server.py): fix double-connection handling 2026-03-05 13:05:48 +01:00
0e7c948668 feat(config/): add config/ directory 2026-03-04 09:23:54 +01:00
35899d3668 refactor(backend_server.py): move known_clients.yaml to config/ 2026-03-04 09:23:21 +01:00
88d349090e fix(backend_server.py): fix double disconnect if client.inbound empty 2026-03-03 22:20:36 +01:00
332238b403 chore(__init__.py): correct version 2026-03-03 21:55:56 +01:00
e54cc479b5 chore(handler/__init__.py): add AckHandler to __all__ 2026-03-03 20:56:18 +01:00
97fc17fbb3 feat(hello_handler.py): remove client from pending_hello if recv'd HELLO 2026-03-03 20:55:39 +01:00
f5b14fc610 feat(backend_server.py): add timeout on HELLO 2026-03-03 20:55:39 +01:00
bf1ad0ead0 feat(ack_handler.py): add handling for ACKs 2026-03-03 20:55:39 +01:00
a9bace8aca feat(backend_server.py): add ACK_TIMEOUT constant 2026-03-03 20:42:16 +01:00
c88e39c735 feat(backend_server.py): track message ACKs and resend if no ACK recv'd within 5 seconds 2026-03-03 20:39:53 +01:00
e308a07dab fix(backend_server.py): call _initialize_handlers() on init 2026-03-03 20:38:55 +01:00
dafe418916 feat(backend_server.py): add warning if received an unknown message (no handler) 2026-03-03 20:18:24 +01:00
c64a258243 build(uv.lock): update judas_protocol to 0.8.0 2026-03-03 19:34:32 +01:00
11 changed files with 162 additions and 24 deletions

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ logs/
# Sphinx # Sphinx
docs/_build/ docs/_build/
docs/ref/modules/ docs/ref/modules/
# known clients
config/known_clients.yaml

View File

@@ -2,6 +2,59 @@
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.6.0] - 2026-03-05
### Bug Fixes
- [`fbb75c2`](https://github.com/pufereq/template-repo/commit/fbb75c263c13726d7e1c3fa28fb4e8f1f8cf0a17) **backend_server.py**: fix double-connection handling
- [`88d3490`](https://github.com/pufereq/template-repo/commit/88d349090e6e3605800656cc3e85779bf4b417c3) **backend_server.py**: fix double disconnect if client.inbound empty
- [`e308a07`](https://github.com/pufereq/template-repo/commit/e308a07dabcf20d9b080ce12a8928760dcde3057) **backend_server.py**: call `_initialize_handlers()` on init
- [`ead2224`](https://github.com/pufereq/template-repo/commit/ead22240660ff9f723c36ee35fa5918c33a0c66d) **backend_server.py**: do not disconnect a client if Exception raised on msg handling
- [`6446fe8`](https://github.com/pufereq/template-repo/commit/6446fe883cea4268992efc9f97cdb493af416576) **backend_server.py**: check if client to disconnect has an open socket
### Features
- [`72d51b4`](https://github.com/pufereq/template-repo/commit/72d51b451fc11ebe2f38d5e50bf263f9f441b03c) **backend_server.py**: add notice to known_clients.yaml
- [`0e7c948`](https://github.com/pufereq/template-repo/commit/0e7c9486683be4ceb91203109986f0b9fc83eb8b) **config/**: add `config/` directory
- [`97fc17f`](https://github.com/pufereq/template-repo/commit/97fc17fbb3ed5d681a4f5a0d488b7a2b09ab35df) **hello_handler.py**: remove client from `pending_hello` if recv'd HELLO
- [`f5b14fc`](https://github.com/pufereq/template-repo/commit/f5b14fc6109e83730c4b13aaecf6f4314c0136a4) **backend_server.py**: add timeout on HELLO
- [`bf1ad0e`](https://github.com/pufereq/template-repo/commit/bf1ad0ead0e4fc35c20d0d747cca05388b223d35) **ack_handler.py**: add handling for ACKs
- [`a9bace8`](https://github.com/pufereq/template-repo/commit/a9bace8acaab07e6e5d02155a9966c6381ad107e) **backend_server.py**: add `ACK_TIMEOUT` constant
- [`c88e39c`](https://github.com/pufereq/template-repo/commit/c88e39c7355b6f8c982198e6e56da2d09d17c61f) **backend_server.py**: track message ACKs and resend if no ACK recv'd within 5 seconds
- [`dafe418`](https://github.com/pufereq/template-repo/commit/dafe418916485258694949538eda151d7da33695) **backend_server.py**: add warning if received an unknown message (no handler)
- [`0ed478a`](https://github.com/pufereq/template-repo/commit/0ed478a88efa46f2f28b24371ad68010ecbf88d0) **backend_server.py**: implement message handling
- [`c952413`](https://github.com/pufereq/template-repo/commit/c952413d912b7a5d2000bc48c11a4d9274533caf) **hello_handler.py**: add HELLO message handler
- [`882c878`](https://github.com/pufereq/template-repo/commit/882c8780e15ec827fac1854cf7419b71d9a41a4f) **base_handler.py**: add `BaseHandler` class for message handling
- [`faecc38`](https://github.com/pufereq/template-repo/commit/faecc382610daf890c90074809085560931fd178) **client_status.py**: move `ClientStatus` enum to own module
### Miscellaneous Tasks
- [`61be674`](https://github.com/pufereq/template-repo/commit/61be674258dcce8a5b54efddb7d1d856c6cba9bc) remove `cache/` directory
- [`f46d271`](https://github.com/pufereq/template-repo/commit/f46d27164b79107abe405b221c9d424add55a723) **.gitignore**: ignore `config/known_clients.yaml` as it's generated automatically
- [`f13f243`](https://github.com/pufereq/template-repo/commit/f13f243b5b389d84acaa4ab933a234a00c6a981c) **__main__.py**: set logging level for werkzeug to WARNING
- [`332238b`](https://github.com/pufereq/template-repo/commit/332238b403782a08a8974b9267071f8d0ff5f006) **__init__.py**: correct version
- [`e54cc47`](https://github.com/pufereq/template-repo/commit/e54cc479b5f6bb3fb887803122fea0c551e27ee2) **handler/__init__.py**: add `AckHandler` to `__all__`
- [`ee38141`](https://github.com/pufereq/template-repo/commit/ee381414a931ed9d7562410e08c9de5cdc7b94e7) **backend_server.py**: remove redundant HELLO msg handling
- [`ec58a52`](https://github.com/pufereq/template-repo/commit/ec58a5257a3007a6edd502eb3967d92a9b652358) **handler/__init__.py**: add module init
- [`d3f68d3`](https://github.com/pufereq/template-repo/commit/d3f68d3baff8b903be125b0da7a88b415cd56bac) **backend/__init__.py**: add `Client` and `ClientStatus` to `__all__`
- [`bda10a6`](https://github.com/pufereq/template-repo/commit/bda10a6248478e99dfb42d8c605996cafdaf1e6e) **cache/**: add cache/ directory
### Refactor
- [`efbf99f`](https://github.com/pufereq/template-repo/commit/efbf99f356c20fbd48937f1c4e8df0a2d57f0651) **backend_server.py**: if known_clients.yaml not present, call `_save_known_clients()`
- [`35899d3`](https://github.com/pufereq/template-repo/commit/35899d366823c2878330eb06dc703f2965f61c3f) **backend_server.py**: move `known_clients.yaml` to `config/`
- [`3eb681e`](https://github.com/pufereq/template-repo/commit/3eb681e233fa151c2476581854c947bdc77b4900) **backend_server.py**: move loading known clients to its own method
- [`fa2da20`](https://github.com/pufereq/template-repo/commit/fa2da207a9cf3d90cdbfb5977033fee21d449528) **backend_server.py**: refactor calls to Message class constructors after protocol changes
### Styling
- [`62acc4b`](https://github.com/pufereq/template-repo/commit/62acc4b181cd359557909fa94f0cb0e6a4109255) **client.py**: correct property typing
### Build
- [`c64a258`](https://github.com/pufereq/template-repo/commit/c64a2582439e5ef1ab68bb9c588f7c4644bb4e07) **uv.lock**: update judas_protocol to 0.8.0
- [`f41a777`](https://github.com/pufereq/template-repo/commit/f41a7774ec5cb4997005ac8e31ed7bdc3541a8d6) **uv.lock**: update judas_protocol to 0.7.0
## [0.5.0] - 2026-02-28 ## [0.5.0] - 2026-02-28
### Features ### Features

View File

@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project] [project]
name = "judas_server" name = "judas_server"
version = "0.5.0" version = "0.6.0"
description = "The backbone of the remote PC fleet management system." description = "The backbone of the remote PC fleet management system."
readme = "README.md" readme = "README.md"
authors = [] authors = []

View File

@@ -1 +1 @@
__version__: str = "0.1.0" __version__: str = "0.5.0"

View File

@@ -12,6 +12,8 @@ if __name__ == "__main__":
format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s", 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 = lg.getLogger(f"{__name__}.LAGA_DYGA")
ladygaga_logger.info(LADY_GAGA) ladygaga_logger.info(LADY_GAGA)

View File

@@ -6,7 +6,7 @@ import selectors
import socket import socket
import threading import threading
import time import time
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Final
import yaml import yaml
from judas_protocol import Category, ControlAction, Message from judas_protocol import Category, ControlAction, Message
@@ -21,6 +21,9 @@ if TYPE_CHECKING:
class BackendServer: 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: def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None:
"""Initialize the backend server. """Initialize the backend server.
@@ -46,13 +49,17 @@ class BackendServer:
) )
self.clients: dict[str, Client] = {} 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[ self.message_handlers: dict[
tuple[Category, ActionType], Callable[[Client, Message], None] tuple[Category, ActionType], Callable[[Client, Message], None]
] = {} ] = {}
self._initialize_handlers()
self.pending_acks: list[tuple[Client, Message, float]] = []
self.pending_hello: dict[Client, float] = {}
self.running: bool = False self.running: bool = False
@@ -70,7 +77,7 @@ class BackendServer:
known_clients: dict[str, dict[str, str | float]] = {} known_clients: dict[str, dict[str, str | float]] = {}
try: try:
with open("cache/known_clients.yaml", "r") as f: with open("config/known_clients.yaml", "r") as f:
data = yaml.safe_load(f) data = yaml.safe_load(f)
if not isinstance(data, dict): if not isinstance(data, dict):
@@ -107,8 +114,7 @@ class BackendServer:
self.logger.warning( self.logger.warning(
"known_clients.yaml not found, creating empty known clients list" "known_clients.yaml not found, creating empty known clients list"
) )
with open("cache/known_clients.yaml", "w") as f: self._save_known_clients()
yaml.safe_dump({"known_clients": {}}, f)
except Exception as e: except Exception as e:
self.logger.error(f"Error loading known clients: {e}") self.logger.error(f"Error loading known clients: {e}")
raise raise
@@ -117,7 +123,12 @@ class BackendServer:
def _save_known_clients(self) -> None: def _save_known_clients(self) -> None:
"""Save the list of known clients to a YAML file.""" """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:
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) yaml.safe_dump({"known_clients": self.known_clients}, f)
self.logger.debug("Saved known clients") self.logger.debug("Saved known clients")
@@ -140,6 +151,22 @@ class BackendServer:
) )
time.sleep(1) 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: def send_ack(self, client: Client, target_id: str) -> None:
"""Send an ACK message to a client. """Send an ACK message to a client.
@@ -147,9 +174,9 @@ class BackendServer:
client (Client): The client to send the ACK to. client (Client): The client to send the ACK to.
target_id (str): The id of the ACK'd message. 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}") self.logger.info(f"[>] Sending ACK to {client}")
client.outbound += ack self.send(client, ack)
def send_close(self, client: Client) -> None: def send_close(self, client: Client) -> None:
"""Send a CLOSE message to a client. """Send a CLOSE message to a client.
@@ -157,9 +184,9 @@ class BackendServer:
Args: Args:
client (Client): The client to send the CLOSE message to. 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}") 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: def _accept_connection(self, sock: socket.socket) -> None:
"""Accept a new client connection. """Accept a new client connection.
@@ -175,6 +202,8 @@ class BackendServer:
events = selectors.EVENT_READ | selectors.EVENT_WRITE events = selectors.EVENT_READ | selectors.EVENT_WRITE
self.selector.register(conn, events, data=client) self.selector.register(conn, events, data=client)
self.pending_hello[client] = time.time()
self.logger.info(f"[+] Registered client {client}, HELLO pending...") self.logger.info(f"[+] Registered client {client}, HELLO pending...")
def _disconnect(self, client: Client) -> None: def _disconnect(self, client: Client) -> None:
@@ -185,7 +214,7 @@ class BackendServer:
""" """
self.logger.info(f"[-] Disconnecting {client}...") self.logger.info(f"[-] Disconnecting {client}...")
if client.socket is None: if client.socket is None or client.socket._closed:
self.logger.warning( self.logger.warning(
f"Client {client} has no socket, nothing to disconnect." f"Client {client} has no socket, nothing to disconnect."
) )
@@ -249,9 +278,6 @@ class BackendServer:
try: try:
if mask & selectors.EVENT_READ: if mask & selectors.EVENT_READ:
self._receive_inbound(sock, client) self._receive_inbound(sock, client)
if not client.inbound:
self._disconnect(client)
return
while b"\n" in client.inbound: while b"\n" in client.inbound:
line, client.inbound = client.inbound.split(b"\n", 1) line, client.inbound = client.inbound.split(b"\n", 1)
@@ -274,7 +300,7 @@ class BackendServer:
f"First message from {client} must be HELLO, disconnecting..." f"First message from {client} must be HELLO, disconnecting..."
) )
self._disconnect(client) self._disconnect(client)
return continue
handler: Callable[[Client, Message], None] | None = ( handler: Callable[[Client, Message], None] | None = (
self.message_handlers.get( self.message_handlers.get(
@@ -283,6 +309,11 @@ class BackendServer:
) )
if handler is not None: if handler is not None:
handler(client, msg) 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: if msg.ack_required:
self.send_ack(client, target_id=msg.id) self.send_ack(client, target_id=msg.id)
@@ -329,6 +360,25 @@ class BackendServer:
and now - client.last_seen > 60 * 60 * 24 # 24 hours and now - client.last_seen > 60 * 60 * 24 # 24 hours
): ):
self.clients[client.id].status = ClientStatus.STALE self.clients[client.id].status = ClientStatus.STALE
# check pending ACKs
for client, msg, timestamp in self.pending_acks[:]:
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))
# 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 time.sleep(0.001) # prevent 100% CPU usage
except Exception as e: except Exception as e:

View File

@@ -1,4 +1,5 @@
from .base_handler import BaseHandler from .base_handler import BaseHandler
from .hello_handler import HelloHandler from .hello_handler import HelloHandler
from .ack_handler import AckHandler
__all__ = ["BaseHandler", "HelloHandler"] __all__ = ["BaseHandler", "HelloHandler", "AckHandler"]

View File

@@ -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}."
)

View File

@@ -44,7 +44,8 @@ class HelloHandler(BaseHandler):
# check if client already connected, if so disconnect old client and register new one # check if client already connected, if so disconnect old client and register new one
if ( if (
client.id in self.backend_server.clients 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] old_client: Client = self.backend_server.clients[client.id]
self.backend_server.logger.warning( self.backend_server.logger.warning(
@@ -58,6 +59,7 @@ class HelloHandler(BaseHandler):
"last_seen": client.last_seen "last_seen": client.last_seen
} }
del self.backend_server.pending_hello[client]
self.backend_server._save_known_clients() self.backend_server._save_known_clients()
client.status = ClientStatus.ONLINE client.status = ClientStatus.ONLINE

6
uv.lock generated
View File

@@ -358,12 +358,12 @@ wheels = [
[[package]] [[package]]
name = "judas-protocol" name = "judas-protocol"
version = "0.7.0" version = "0.8.0"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#48a848bb60bea199020f2b765a6cc3f8acc21067" } source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" }
[[package]] [[package]]
name = "judas-server" name = "judas-server"
version = "0.5.0" version = "0.6.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "flask" }, { name = "flask" },