Compare commits
25 Commits
a9bace8aca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11bf344cb5 | ||
| 5ee8eca11b | |||
| f18935f793 | |||
|
d7b136851b
|
|||
|
7e9a9e6eed
|
|||
|
6ed03ab74d
|
|||
|
40c08d0169
|
|||
|
3d13d24116
|
|||
|
28b57b6964
|
|||
|
|
6a024bbfc6 | ||
| e5ca1066fd | |||
|
61be674258
|
|||
|
72d51b451f
|
|||
|
efbf99f356
|
|||
|
f46d27164b
|
|||
|
f13f243b5b
|
|||
|
fbb75c263c
|
|||
|
0e7c948668
|
|||
|
35899d3668
|
|||
|
88d349090e
|
|||
|
332238b403
|
|||
|
e54cc479b5
|
|||
|
97fc17fbb3
|
|||
|
f5b14fc610
|
|||
|
bf1ad0ead0
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ logs/
|
||||
# Sphinx
|
||||
docs/_build/
|
||||
docs/ref/modules/
|
||||
|
||||
# known clients
|
||||
config/known_clients.yaml
|
||||
|
||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -2,6 +2,73 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.7.0-dev.1] - 2026-03-08
|
||||
|
||||
### Features
|
||||
|
||||
- [`d7b1368`](https://github.com/pufereq/template-repo/commit/d7b136851bafa2c20e1634bd5568f4bac839177f) **client_details.html**: add temporary initial telemetry display
|
||||
- [`7e9a9e6`](https://github.com/pufereq/template-repo/commit/7e9a9e6eede6cc926fef64c626434e65984befbc) **backend_server.py**: add `initial_telemetry` support
|
||||
- [`6ed03ab`](https://github.com/pufereq/template-repo/commit/6ed03ab74de8e91d13b5f1971a3f4cec890e4fef) **client.py**: add `Client.initial_telemetry` property
|
||||
- [`40c08d0`](https://github.com/pufereq/template-repo/commit/40c08d01693973f29f13c133a11fc5f166891a25) **initial_handler.py**: add handler for `TELEMETRY/INTIIAL` msgs
|
||||
|
||||
### Build
|
||||
|
||||
- [`3d13d24`](https://github.com/pufereq/template-repo/commit/3d13d241168b011c0044eb64db4b0fe70878d748) **uv.lock**: update judas_protocol to 0.9.1
|
||||
- [`28b57b6`](https://github.com/pufereq/template-repo/commit/28b57b6964bfcd6ce78f2a77822f7221f6e4f7e5) **uv.lock**: update judas_protocol to 0.9.0
|
||||
|
||||
## [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
|
||||
|
||||
### Features
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "judas_server"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0-dev.1"
|
||||
description = "The backbone of the remote PC fleet management system."
|
||||
readme = "README.md"
|
||||
authors = []
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__: str = "0.1.0"
|
||||
__version__: str = "0.5.0"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -8,11 +8,15 @@ import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from judas_protocol.types import TelemetryAction
|
||||
import yaml
|
||||
from judas_protocol import Category, ControlAction, Message
|
||||
|
||||
from judas_server.backend.client import Client, ClientStatus
|
||||
from judas_server.backend.handler.hello_handler import HelloHandler
|
||||
from judas_server.backend.handler.telemetry.initial_handler import (
|
||||
InitialTelemetryHandler,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
@@ -22,6 +26,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.
|
||||
@@ -48,9 +53,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]
|
||||
@@ -58,6 +63,7 @@ class BackendServer:
|
||||
self._initialize_handlers()
|
||||
|
||||
self.pending_acks: list[tuple[Client, Message, float]] = []
|
||||
self.pending_hello: dict[Client, float] = {}
|
||||
|
||||
self.running: bool = False
|
||||
|
||||
@@ -65,17 +71,22 @@ class BackendServer:
|
||||
"""Initialize message handlers."""
|
||||
|
||||
hello_handler = HelloHandler(self)
|
||||
initial_telemetry_handler = InitialTelemetryHandler(self)
|
||||
|
||||
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
|
||||
hello_handler.handle
|
||||
)
|
||||
|
||||
self.message_handlers[
|
||||
(Category.TELEMETRY, TelemetryAction.INITIAL)
|
||||
] = initial_telemetry_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]] = {}
|
||||
|
||||
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):
|
||||
@@ -106,14 +117,16 @@ class BackendServer:
|
||||
client.last_seen = float(
|
||||
known_clients[client_id].get("last_seen", 0.0)
|
||||
)
|
||||
client.initial_telemetry = known_clients[client_id].get( # type: ignore
|
||||
"initial_telemetry", None
|
||||
)
|
||||
self.clients[client_id] = client
|
||||
|
||||
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)
|
||||
self._save_known_clients()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading known clients: {e}")
|
||||
raise
|
||||
@@ -122,7 +135,12 @@ 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:
|
||||
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")
|
||||
|
||||
@@ -196,6 +214,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:
|
||||
@@ -206,7 +226,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."
|
||||
)
|
||||
@@ -270,9 +290,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)
|
||||
@@ -365,6 +382,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:
|
||||
@@ -385,4 +411,5 @@ class BackendServer:
|
||||
"addr": client.addr,
|
||||
"last_seen": client.last_seen,
|
||||
"status": client.status,
|
||||
"initial_telemetry": client.initial_telemetry,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
import logging as lg
|
||||
import socket
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from judas_server.backend.client_status import ClientStatus
|
||||
|
||||
@@ -41,6 +42,8 @@ class Client:
|
||||
self.inbound: bytes = b""
|
||||
self.outbound: bytes = b""
|
||||
|
||||
self.initial_telemetry: dict[str, Any] | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.addr:
|
||||
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
27
src/judas_server/backend/handler/ack_handler.py
Normal file
27
src/judas_server/backend/handler/ack_handler.py
Normal 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}."
|
||||
)
|
||||
@@ -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(
|
||||
@@ -58,6 +59,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
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Initial telemetry handler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from judas_protocol import Message
|
||||
|
||||
from judas_server.backend.handler.base_handler import BaseHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from judas_server.backend import BackendServer
|
||||
from judas_server.backend.client import Client
|
||||
|
||||
|
||||
class InitialTelemetryHandler(BaseHandler):
|
||||
"""Handles the initial telemetry message from a client."""
|
||||
|
||||
def __init__(self, backend_server: BackendServer) -> None:
|
||||
"""Initialize the handler."""
|
||||
super().__init__(backend_server)
|
||||
|
||||
def handle(self, client: Client, message: Message) -> None:
|
||||
"""Handle the initial telemetry message."""
|
||||
self.logger.debug(
|
||||
f"Handling initial telemetry message from {client}..."
|
||||
)
|
||||
|
||||
client.initial_telemetry = message.payload
|
||||
self.backend_server.known_clients[client.id]["initial_telemetry"] = ( # type: ignore
|
||||
message.payload
|
||||
)
|
||||
@@ -18,6 +18,10 @@
|
||||
<strong>Last Seen:</strong>
|
||||
<span id="last-seen">{{ client.last_seen }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Initial:</strong>
|
||||
<pre>{{ client.initial_telemetry | tojson(indent=2) }}</pre>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -358,12 +358,12 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "judas-protocol"
|
||||
version = "0.8.0"
|
||||
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" }
|
||||
version = "0.9.1"
|
||||
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" }
|
||||
|
||||
[[package]]
|
||||
name = "judas-server"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0.dev1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
|
||||
Reference in New Issue
Block a user