Logging: add informative console logs for startup, ports, connections, JOIN/deny, input, and periodic events
- Configure logging via LOG_LEVEL env (default INFO) - Log when servers start listening (WT/QUIC/InMemory/HTTPS static) - Log WT CONNECT accept, QUIC peer connect, datagram traffic at DEBUG - Log GameServer creation, tick loop start, JOIN accept/deny, config_update broadcasts, and input reception
This commit is contained in:
7
run.py
7
run.py
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
from server.server import GameServer
|
from server.server import GameServer
|
||||||
from server.config import ServerConfig
|
from server.config import ServerConfig
|
||||||
@@ -51,12 +52,18 @@ async def run_webtransport():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
|
# Logging setup
|
||||||
|
level = os.environ.get("LOG_LEVEL", "INFO").upper()
|
||||||
|
logging.basicConfig(level=getattr(logging, level, logging.INFO), format="[%(asctime)s] %(levelname)s: %(message)s")
|
||||||
mode = os.environ.get("MODE", "mem").lower()
|
mode = os.environ.get("MODE", "mem").lower()
|
||||||
if mode == "wt":
|
if mode == "wt":
|
||||||
|
logging.info("Starting in WebTransport mode")
|
||||||
asyncio.run(run_webtransport())
|
asyncio.run(run_webtransport())
|
||||||
elif mode == "quic":
|
elif mode == "quic":
|
||||||
|
logging.info("Starting in QUIC datagram mode")
|
||||||
asyncio.run(run_quic())
|
asyncio.run(run_quic())
|
||||||
else:
|
else:
|
||||||
|
logging.info("Starting in in-memory transport mode")
|
||||||
asyncio.run(run_in_memory())
|
asyncio.run(run_in_memory())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Awaitable, Callable, Dict, Optional
|
from typing import Awaitable, Callable, Dict, Optional
|
||||||
|
|
||||||
from .transport import DatagramServerTransport, OnDatagram, TransportPeer
|
from .transport import DatagramServerTransport, OnDatagram, TransportPeer
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -31,14 +32,17 @@ class GameQuicProtocol(QuicConnectionProtocol): # type: ignore[misc]
|
|||||||
# Register by connection id
|
# Register by connection id
|
||||||
self._peer_id = int(self._quic.connection_id) # type: ignore[attr-defined]
|
self._peer_id = int(self._quic.connection_id) # type: ignore[attr-defined]
|
||||||
self._peers[self._peer_id] = self
|
self._peers[self._peer_id] = self
|
||||||
|
logging.info("QUIC peer connected: id=%s", self._peer_id)
|
||||||
elif isinstance(event, DatagramFrameReceived):
|
elif isinstance(event, DatagramFrameReceived):
|
||||||
# Schedule async callback
|
# Schedule async callback
|
||||||
if self._peer_id is None:
|
if self._peer_id is None:
|
||||||
return
|
return
|
||||||
peer = TransportPeer(addr=self)
|
peer = TransportPeer(addr=self)
|
||||||
|
logging.debug("QUIC datagram received from id=%s, %d bytes", self._peer_id, len(event.data))
|
||||||
asyncio.ensure_future(self._on_datagram(bytes(event.data), peer))
|
asyncio.ensure_future(self._on_datagram(bytes(event.data), peer))
|
||||||
|
|
||||||
async def send_datagram(self, data: bytes) -> None:
|
async def send_datagram(self, data: bytes) -> None:
|
||||||
|
logging.debug("QUIC send datagram: %d bytes", len(data))
|
||||||
self._quic.send_datagram_frame(data) # type: ignore[attr-defined]
|
self._quic.send_datagram_frame(data) # type: ignore[attr-defined]
|
||||||
await self._loop.run_in_executor(None, self.transmit) # type: ignore[attr-defined]
|
await self._loop.run_in_executor(None, self.transmit) # type: ignore[attr-defined]
|
||||||
|
|
||||||
@@ -67,9 +71,9 @@ class QuicWebTransportServer(DatagramServerTransport):
|
|||||||
async def _create_protocol(*args, **kwargs):
|
async def _create_protocol(*args, **kwargs):
|
||||||
return GameQuicProtocol(*args, on_datagram=self._on_datagram, peers=self._peers, **kwargs)
|
return GameQuicProtocol(*args, on_datagram=self._on_datagram, peers=self._peers, **kwargs)
|
||||||
|
|
||||||
|
logging.info("QUIC datagram server listening on %s:%d", self.host, self.port)
|
||||||
self._server = await serve(self.host, self.port, configuration=configuration, create_protocol=_create_protocol)
|
self._server = await serve(self.host, self.port, configuration=configuration, create_protocol=_create_protocol)
|
||||||
try:
|
try:
|
||||||
await self._server.wait_closed()
|
await self._server.wait_closed()
|
||||||
finally:
|
finally:
|
||||||
self._server.close()
|
self._server.close()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ class GameServer:
|
|||||||
self.runtime = ServerRuntime(config=config, state=GameState(config.width, config.height))
|
self.runtime = ServerRuntime(config=config, state=GameState(config.width, config.height))
|
||||||
self.sessions: Dict[int, PlayerSession] = {}
|
self.sessions: Dict[int, PlayerSession] = {}
|
||||||
self._config_update_interval_ticks = 50 # periodic resend
|
self._config_update_interval_ticks = 50 # periodic resend
|
||||||
|
logging.info("GameServer created: field=%dx%d, tick_rate=%d TPS, wrap=%s", config.width, config.height, config.tick_rate, config.wrap_edges)
|
||||||
|
|
||||||
async def on_datagram(self, data: bytes, peer: TransportPeer) -> None:
|
async def on_datagram(self, data: bytes, peer: TransportPeer) -> None:
|
||||||
# Minimal header parse
|
# Minimal header parse
|
||||||
@@ -85,6 +87,7 @@ class GameServer:
|
|||||||
# Broadcast to all sessions (requires real peer handles)
|
# Broadcast to all sessions (requires real peer handles)
|
||||||
for session in list(self.sessions.values()):
|
for session in list(self.sessions.values()):
|
||||||
await self.transport.send(payload, TransportPeer(session.peer))
|
await self.transport.send(payload, TransportPeer(session.peer))
|
||||||
|
logging.debug("Broadcasted config_update to %d sessions", len(self.sessions))
|
||||||
|
|
||||||
async def relay_input_broadcast(
|
async def relay_input_broadcast(
|
||||||
self,
|
self,
|
||||||
@@ -115,6 +118,7 @@ class GameServer:
|
|||||||
r = self.runtime
|
r = self.runtime
|
||||||
tick_duration = 1.0 / max(1, r.config.tick_rate)
|
tick_duration = 1.0 / max(1, r.config.tick_rate)
|
||||||
next_cfg_resend = self._config_update_interval_ticks
|
next_cfg_resend = self._config_update_interval_ticks
|
||||||
|
logging.info("Tick loop started at %.2f TPS", 1.0 / tick_duration)
|
||||||
while True:
|
while True:
|
||||||
start = asyncio.get_event_loop().time()
|
start = asyncio.get_event_loop().time()
|
||||||
# process inputs, update snakes, collisions, apples, deltas
|
# process inputs, update snakes, collisions, apples, deltas
|
||||||
@@ -388,12 +392,14 @@ class GameServer:
|
|||||||
if pid is None:
|
if pid is None:
|
||||||
payload = build_join_deny(version=self.runtime.version, seq=self.runtime.next_seq(), reason="Server full")
|
payload = build_join_deny(version=self.runtime.version, seq=self.runtime.next_seq(), reason="Server full")
|
||||||
await self.transport.send(payload, peer)
|
await self.transport.send(payload, peer)
|
||||||
|
logging.warning("JOIN denied: server full (name=%s)", name)
|
||||||
return
|
return
|
||||||
# Spawn snake
|
# Spawn snake
|
||||||
snake = self._find_spawn()
|
snake = self._find_spawn()
|
||||||
if snake is None:
|
if snake is None:
|
||||||
payload = build_join_deny(version=self.runtime.version, seq=self.runtime.next_seq(), reason="No free cell, please wait")
|
payload = build_join_deny(version=self.runtime.version, seq=self.runtime.next_seq(), reason="No free cell, please wait")
|
||||||
await self.transport.send(payload, peer)
|
await self.transport.send(payload, peer)
|
||||||
|
logging.warning("JOIN denied: no free cell (name=%s)", name)
|
||||||
return
|
return
|
||||||
# Register session and snake
|
# Register session and snake
|
||||||
color_id = preferred if (preferred is not None) else self._choose_color_id()
|
color_id = preferred if (preferred is not None) else self._choose_color_id()
|
||||||
@@ -403,6 +409,7 @@ class GameServer:
|
|||||||
self.runtime.state.snakes[pid] = snake
|
self.runtime.state.snakes[pid] = snake
|
||||||
self.runtime.state.occupy_snake(snake)
|
self.runtime.state.occupy_snake(snake)
|
||||||
self._ensure_apples()
|
self._ensure_apples()
|
||||||
|
logging.info("JOIN accepted: player_id=%d name=%s color=%d len=%d at=%s", pid, name, color_id, snake.length, snake.body[0])
|
||||||
# Send join_ack
|
# Send join_ack
|
||||||
cfg = self.runtime.config
|
cfg = self.runtime.config
|
||||||
ack = build_join_ack(
|
ack = build_join_ack(
|
||||||
@@ -445,7 +452,7 @@ class GameServer:
|
|||||||
full = pack_header(
|
full = pack_header(
|
||||||
self.runtime.version, PacketType.STATE_FULL, 0, self.runtime.next_seq(), self.runtime.state.tick & 0xFFFF
|
self.runtime.version, PacketType.STATE_FULL, 0, self.runtime.next_seq(), self.runtime.state.tick & 0xFFFF
|
||||||
) + body
|
) + body
|
||||||
await self.transport.send(full, peer)
|
await self.transport.send(full, peer)
|
||||||
else:
|
else:
|
||||||
# Partition by packing whole snakes first, apples only in first part; chunk a single oversized snake using 2-bit chunks
|
# Partition by packing whole snakes first, apples only in first part; chunk a single oversized snake using 2-bit chunks
|
||||||
update_id = self.runtime.next_update_id()
|
update_id = self.runtime.next_update_id()
|
||||||
@@ -563,6 +570,7 @@ class GameServer:
|
|||||||
break
|
break
|
||||||
if player_id is None:
|
if player_id is None:
|
||||||
return
|
return
|
||||||
|
logging.debug("INPUT from player_id=%d: %d events (base_tick=%d)", player_id, len(events), base_tick)
|
||||||
# Relay to others immediately for prediction
|
# Relay to others immediately for prediction
|
||||||
await self.relay_input_broadcast(
|
await self.relay_input_broadcast(
|
||||||
from_player_id=player_id,
|
from_player_id=player_id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ssl
|
import ssl
|
||||||
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
|
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -31,5 +32,5 @@ def start_https_static(host: str, port: int, certfile: str, keyfile: str, docroo
|
|||||||
|
|
||||||
t = threading.Thread(target=httpd.serve_forever, name="https-static", daemon=True)
|
t = threading.Thread(target=httpd.serve_forever, name="https-static", daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
logging.info("HTTPS static server listening on https://%s:%d serving '%s'", host, port, docroot_path)
|
||||||
return httpd, t
|
return httpd, t
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Awaitable, Callable, Optional, Tuple
|
from typing import Awaitable, Callable, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class InMemoryTransport(DatagramServerTransport):
|
|||||||
await self._on_datagram(data, peer)
|
await self._on_datagram(data, peer)
|
||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
# Nothing to do in in-memory test transport
|
logging.info("InMemory transport started (no network)")
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Awaitable, Callable, Dict, Optional
|
from typing import Awaitable, Callable, Dict, Optional
|
||||||
|
|
||||||
from .transport import DatagramServerTransport, OnDatagram, TransportPeer
|
from .transport import DatagramServerTransport, OnDatagram, TransportPeer
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -49,6 +50,7 @@ class GameWTProtocol(QuicConnectionProtocol): # type: ignore[misc]
|
|||||||
# In WebTransport over H3, datagrams use the CONNECT stream id as flow_id
|
# In WebTransport over H3, datagrams use the CONNECT stream id as flow_id
|
||||||
flow_id = event.stream_id # type: ignore[attr-defined]
|
flow_id = event.stream_id # type: ignore[attr-defined]
|
||||||
self._sessions[flow_id] = WTSession(flow_id=flow_id, proto=self)
|
self._sessions[flow_id] = WTSession(flow_id=flow_id, proto=self)
|
||||||
|
logging.info("WT CONNECT accepted: flow_id=%s", flow_id)
|
||||||
# Send 2xx to accept the session
|
# Send 2xx to accept the session
|
||||||
if self._http is not None:
|
if self._http is not None:
|
||||||
self._http.send_headers(event.stream_id, [(b":status", b"200")])
|
self._http.send_headers(event.stream_id, [(b":status", b"200")])
|
||||||
@@ -62,6 +64,7 @@ class GameWTProtocol(QuicConnectionProtocol): # type: ignore[misc]
|
|||||||
if not sess:
|
if not sess:
|
||||||
return
|
return
|
||||||
peer = TransportPeer(addr=(self, flow_id))
|
peer = TransportPeer(addr=(self, flow_id))
|
||||||
|
logging.debug("WT datagram received: flow_id=%s, %d bytes", flow_id, len(data))
|
||||||
asyncio.ensure_future(self._on_datagram(bytes(data), peer))
|
asyncio.ensure_future(self._on_datagram(bytes(data), peer))
|
||||||
|
|
||||||
def quic_event_received(self, event) -> None: # type: ignore[override]
|
def quic_event_received(self, event) -> None: # type: ignore[override]
|
||||||
@@ -79,6 +82,7 @@ class GameWTProtocol(QuicConnectionProtocol): # type: ignore[misc]
|
|||||||
if self._http is None:
|
if self._http is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
logging.debug("WT send datagram: flow_id=%s, %d bytes", flow_id, len(data))
|
||||||
self._http.send_datagram(flow_id, data)
|
self._http.send_datagram(flow_id, data)
|
||||||
await self._loop.run_in_executor(None, self.transmit) # type: ignore[attr-defined]
|
await self._loop.run_in_executor(None, self.transmit) # type: ignore[attr-defined]
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -117,9 +121,9 @@ class WebTransportServer(DatagramServerTransport):
|
|||||||
async def _create_protocol(*args, **kwargs):
|
async def _create_protocol(*args, **kwargs):
|
||||||
return GameWTProtocol(*args, on_datagram=self._on_datagram, sessions=self._sessions, **kwargs)
|
return GameWTProtocol(*args, on_datagram=self._on_datagram, sessions=self._sessions, **kwargs)
|
||||||
|
|
||||||
|
logging.info("WebTransport (H3) server listening on %s:%d", self.host, self.port)
|
||||||
self._server = await serve(self.host, self.port, configuration=configuration, create_protocol=_create_protocol)
|
self._server = await serve(self.host, self.port, configuration=configuration, create_protocol=_create_protocol)
|
||||||
try:
|
try:
|
||||||
await self._server.wait_closed()
|
await self._server.wait_closed()
|
||||||
finally:
|
finally:
|
||||||
self._server.close()
|
self._server.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user