Initial json server implementation
Completed the initial implementation of the JsonServer class, handling the worker thread that runs the async task, as well as the connection handler callback The current handler awaits readline to cleanly exit when the connection is closed, no data ingress is happening yet Next steps include gating the start procedure to wait for everything to start proprerly in order to avoid having start exit early and async tasks crashing later
This commit is contained in:
+3
-1
@@ -7,7 +7,9 @@ authors = [
|
|||||||
{ name = "Hector van der Aa", email = "hector@h3cx.dev" }
|
{ name = "Hector van der Aa", email = "hector@h3cx.dev" }
|
||||||
]
|
]
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"pydantic>=2.13.4",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.12.1,<0.13.0"]
|
requires = ["uv_build>=0.12.1,<0.13.0"]
|
||||||
|
|||||||
@@ -2,4 +2,93 @@
|
|||||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from asyncio import Server
|
||||||
|
import threading
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
from dynalab_core.config import CoreConfig
|
||||||
|
from dynalab_core.constants import CORE_VERSION
|
||||||
|
from dynalab_core.errors import StateError
|
||||||
|
from dynalab_core.protocols.json.common import VersionDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
class JsonServer:
|
||||||
|
_thread: Thread | None = None
|
||||||
|
_server: Server | None = None
|
||||||
|
_config: CoreConfig
|
||||||
|
_stop_event: threading.Event
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: CoreConfig,
|
||||||
|
stop_event: threading.Event,
|
||||||
|
) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._stop_event = stop_event
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
self._thread = Thread(
|
||||||
|
target=self._json_server_thread_main, name="json_server_thread", daemon=True
|
||||||
|
)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def _json_server_thread_main(self) -> None:
|
||||||
|
asyncio.run(self._run_json_server())
|
||||||
|
|
||||||
|
async def _run_json_server(self) -> None:
|
||||||
|
try:
|
||||||
|
self._server = await asyncio.start_server(
|
||||||
|
self._handle_json_connection,
|
||||||
|
host=self._config.host,
|
||||||
|
port=self._config.port,
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
print(
|
||||||
|
f"Failed to start ingress server on {self._config.bind_str()}: {error}"
|
||||||
|
)
|
||||||
|
async with self._server:
|
||||||
|
await asyncio.to_thread(self._stop_event.wait)
|
||||||
|
|
||||||
|
async def _handle_json_connection(
|
||||||
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||||
|
) -> None:
|
||||||
|
print(f"Connected to: {writer.get_extra_info('peername')}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await reader.readline()
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
class Core:
|
||||||
|
_state: Literal["uninitd", "initd", "started", "stopping", "stopped"] = "unintid"
|
||||||
|
_core_version: VersionDescriptor = CORE_VERSION
|
||||||
|
_core_config: CoreConfig = CoreConfig()
|
||||||
|
_stop_event: threading.Event = threading.Event()
|
||||||
|
_json_server: JsonServer
|
||||||
|
|
||||||
|
def __init__(self, config: CoreConfig) -> None:
|
||||||
|
self._core_config = config
|
||||||
|
self._state = "initd"
|
||||||
|
self._json_server = JsonServer(self._core_config, self._stop_event)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
if self._state != "initd":
|
||||||
|
raise StateError(
|
||||||
|
f'Unable to start DynaLab Core, expected state to be "initd", found {self._state}'
|
||||||
|
)
|
||||||
|
self._json_server.start()
|
||||||
|
self._state = "started"
|
||||||
|
|
||||||
|
def wait(self, timeout: float | None = None) -> None:
|
||||||
|
self._stop_event.wait(timeout)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self._stop_event.set()
|
||||||
|
self._state = "stopped"
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(BaseModel):
|
||||||
|
host: str = "127.0.0.1"
|
||||||
|
port: int = Field(default=58763, ge=0, le=65535)
|
||||||
|
|
||||||
|
def bind_str(self) -> str:
|
||||||
|
return f"{self.host}:{self.port}"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from dynalab_core.protocols.json.common import VersionDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
CORE_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
class StateError(Exception):
|
||||||
|
"""DynaLab Core state error."""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
from uuid import UUID
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class VersionDescriptor(BaseModel):
|
||||||
|
model_config = ConfigDict(frozen=True, populate_by_name=True, extra="forbid")
|
||||||
|
type: Literal["alpha", "beta", "release"] = Field(alias="type")
|
||||||
|
major: int = Field(ge=0)
|
||||||
|
minor: int = Field(ge=0)
|
||||||
|
patch: int = Field(ge=0)
|
||||||
|
|
||||||
|
def get_version(self) -> str:
|
||||||
|
return f"{self.major}.{self.minor}.{self.patch}-{self.type}"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from dynalab_core.protocols.json.common import VersionDescriptor
|
||||||
|
|
||||||
|
PROTOCOL_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
from uuid import UUID
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from dynalab_core.protocols.json.common import VersionDescriptor
|
||||||
|
from dynalab_core.protocols.json.constants import PROTOCOL_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class DynaLabHello(BaseModel):
|
||||||
|
type: Literal["dynalab_hello"] = "dynalab_hello"
|
||||||
|
instance_id: UUID
|
||||||
|
core_version: VersionDescriptor
|
||||||
|
protocol_version: VersionDescriptor = PROTOCOL_VERSION
|
||||||
|
|
||||||
|
heartbeat_interval_ms: int = Field(default_factory=1000, ge=0)
|
||||||
|
heartbeat_timeout_ms: int = Field(default_factory=5000, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorHello(BaseModel):
|
||||||
|
type: Literal["connector_hello"] = "connector_hello"
|
||||||
|
connector_uuid: UUID
|
||||||
|
protocol_version: VersionDescriptor = PROTOCOL_VERSION
|
||||||
|
|
||||||
|
connector_name: str
|
||||||
|
connector_version: str
|
||||||
|
|
||||||
|
# TODO: implement SignalDescriptor
|
||||||
|
# signals: list[SignalDescriptor]
|
||||||
|
|
||||||
|
|
||||||
|
class HandshakeAccepted(BaseModel):
|
||||||
|
type: Literal["handshake_accepted"] = "handshake_accepted"
|
||||||
|
accepted_signals: list[UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class HandshakeRejected(BaseModel):
|
||||||
|
type: Literal["handshake_rejected"] = "handshake_rejected"
|
||||||
|
reason: str
|
||||||
Reference in New Issue
Block a user