From 72b221162ffe304baa3c226d9156df7a3d61cc72 Mon Sep 17 00:00:00 2001 From: TimePath Date: Sun, 13 May 2018 17:53:08 +1000 Subject: [PATCH] slist: init --- derivation.nix | 31 ++++- misc/infrastructure/python/.gitignore | 1 + misc/infrastructure/python/mypy.ini | 16 +++ misc/infrastructure/python/slist/__init__.py | 3 + misc/infrastructure/python/slist/master.py | 128 +++++++++++++++++++ misc/infrastructure/python/slist/utils.py | 31 +++++ 6 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 misc/infrastructure/python/.gitignore create mode 100644 misc/infrastructure/python/mypy.ini create mode 100644 misc/infrastructure/python/slist/__init__.py create mode 100644 misc/infrastructure/python/slist/master.py create mode 100644 misc/infrastructure/python/slist/utils.py diff --git a/derivation.nix b/derivation.nix index d0365715..628c95f5 100644 --- a/derivation.nix +++ b/derivation.nix @@ -441,6 +441,33 @@ let ''; }; + slist = mkDerivation rec { + name = "slist-${version}"; + version = "xonotic-${VERSION}"; + + src = "${srcs."xonotic"}/misc/infrastructure/python/slist"; + + buildInputs = with pkgs; [ + python3 + python3Packages.attrs + (python3Packages.buildPythonApplication rec { + pname = "mypy"; + version = "0.600"; + doCheck = false; + src = python3Packages.fetchPypi { + inherit pname version; + sha256 = "1pd3kkz435wlvi9fwqbi3xag5zs59jcjqi6c9gzdjdn23friq9dw"; + }; + propagatedBuildInputs = with python3Packages; [ lxml typed-ast psutil ]; + }) + ]; + phases = [ "installPhase" ]; + installPhase = '' + mkdir $out + cp -r $src/. $out + ''; + }; + xonotic = mkDerivation rec { name = "xonotic-${version}"; version = vers."xonotic"; @@ -580,8 +607,8 @@ let shell = let inputs = (lib.mapAttrsToList (k: v: v) targets); in stdenv.mkDerivation (rec { name = "xonotic-shell"; - nativeBuildInputs = builtins.map (it: it.nativeBuildInputs) (builtins.filter (it: it?nativeBuildInputs) inputs); - buildInputs = builtins.map (it: it.buildInputs) (builtins.filter (it: it?buildInputs) inputs); + nativeBuildInputs = lib.unique (builtins.map (it: it.nativeBuildInputs) (builtins.filter (it: it?nativeBuildInputs) inputs)); + buildInputs = lib.unique (builtins.map (it: it.buildInputs) (builtins.filter (it: it?buildInputs) inputs)); shellHook = builtins.map (it: it.shellHook) (builtins.filter (it: it?shellHook) inputs); }); in { inherit shell; } // targets diff --git a/misc/infrastructure/python/.gitignore b/misc/infrastructure/python/.gitignore new file mode 100644 index 00000000..020de353 --- /dev/null +++ b/misc/infrastructure/python/.gitignore @@ -0,0 +1 @@ +/.mypy_cache diff --git a/misc/infrastructure/python/mypy.ini b/misc/infrastructure/python/mypy.ini new file mode 100644 index 00000000..2f1052dc --- /dev/null +++ b/misc/infrastructure/python/mypy.ini @@ -0,0 +1,16 @@ +# check with `mypy .` +[mypy] + +[slist] +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_subclassing_any = True +disallow_untyped_decorators = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_ignores = True +warn_unused_configs = True +no_implicit_optional = True +strict_optional = True diff --git a/misc/infrastructure/python/slist/__init__.py b/misc/infrastructure/python/slist/__init__.py new file mode 100644 index 00000000..b9f9ca83 --- /dev/null +++ b/misc/infrastructure/python/slist/__init__.py @@ -0,0 +1,3 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) diff --git a/misc/infrastructure/python/slist/master.py b/misc/infrastructure/python/slist/master.py new file mode 100644 index 00000000..12bfd220 --- /dev/null +++ b/misc/infrastructure/python/slist/master.py @@ -0,0 +1,128 @@ +import ipaddress +import logging +from struct import Struct + +import attr + +from .utils import * + +logger = logging.getLogger(__name__) + +HEADER = b"\xFF\xFF\xFF\xFF" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class CLGetServersExt(Writable): + game: str + protocol: int + + def encode(self) -> bytes: + return HEADER + f"getserversExt {self.game} {self.protocol} empty full".encode(UTF_8) + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class SVGetServersExtResponse(Readable): + @attr.s(auto_attribs=True, frozen=True, slots=True) + class Server: + addr: str + port: int + + servers: List[Server] + + @classmethod + @generator + def decode(cls) -> Generator[Optional["SVGetServersExtResponse"], bytes, None]: + end = SVGetServersExtResponse.Server("", 0) + ipv4 = Struct(">4sH") + ipv6 = Struct(">16sH") + + def servers() -> Iterator[SVGetServersExtResponse.Server]: + offset = 0 + while True: + h = buf[offset:offset + 1] + offset += 1 + if h == b"": + return + elif h == b"\\": + record = ipv4 + elif h == b"/": + record = ipv6 + else: + assert False, f"unknown record type: {h}" + + it = record.unpack_from(buf, offset) + if record == ipv4: + addr, port = it + if addr == b"EOT\x00" and port == 0: + yield end + return + addr = ipaddress.IPv4Address(addr) + yield SVGetServersExtResponse.Server(addr=addr, port=port) + elif record == ipv6: + addr, port = it + addr = ipaddress.IPv6Address(addr) + yield SVGetServersExtResponse.Server(addr=addr, port=port) + offset += record.size + + chunks: List[List[SVGetServersExtResponse.Server]] = [] + ret: Optional[SVGetServersExtResponse] = None + done = False + while True: + buf: bytes + buf = yield ret + if done: + return + chunk = list(servers()) + chunks.append(chunk) + if chunk[-1] == end: + chunk.pop() + ret = SVGetServersExtResponse(servers=[x for l in chunks for x in l]) + done = True + + +SVMessage = Union[SVGetServersExtResponse] + + +@generator +def sv_parse() -> Generator[Optional[SVMessage], bytes, None]: + getservers_ext_response = b"getserversExtResponse" + getservers_ext_gen: Optional[Generator[Optional[SVGetServersExtResponse], bytes, None]] = None + ret: Optional[SVMessage] = None + while True: + buf: bytes + buf = yield ret + ret = None + if buf.startswith(HEADER): + buf = buf[len(HEADER):] + if buf.startswith(getservers_ext_response): + buf = buf[len(getservers_ext_response):] + if not getservers_ext_gen: + getservers_ext_gen = SVGetServersExtResponse.decode() + assert getservers_ext_gen + ret = getservers_ext_gen.send(buf) + if ret: + getservers_ext_gen = None + continue + + +if __name__ == "__main__": + import socket + + connection = Tuple[str, int] + connections: Dict[connection, Generator[Optional[SVMessage], bytes, None]] = {} + + conn = (socket.gethostbyname("dpmaster.deathmask.net"), 27950) + connections[conn] = sv_parse() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + q = CLGetServersExt(game="Xonotic", protocol=3) + sock.sendto(q.encode(), conn) + while True: + logger.debug("wait") + data, addr = sock.recvfrom(1400) + logger.debug(f"recv({addr}): {data}") + msg = connections[addr].send(data) + if msg: + logger.info(f"recv({addr}): {msg}") + if isinstance(msg, SVGetServersExtResponse): + logger.info(f"servers: {len(msg.servers)}") diff --git a/misc/infrastructure/python/slist/utils.py b/misc/infrastructure/python/slist/utils.py new file mode 100644 index 00000000..1cc36aa1 --- /dev/null +++ b/misc/infrastructure/python/slist/utils.py @@ -0,0 +1,31 @@ +from functools import wraps +from typing import * + +UTF_8 = "utf-8" + + +class Readable: + @classmethod + def decode(cls) -> Generator[Optional[object], bytes, None]: + raise NotImplementedError + + +class Writable: + def encode(self) -> bytes: + raise NotImplementedError + + +def generator(f): + O = TypeVar("O") + I = TypeVar("I") + R = TypeVar("R") + + def prepare(g: Generator[O, I, R]) -> Generator[O, I, R]: + next(g) + return g + + @wraps(f) + def w(*args, **kwargs): + return prepare(f(*args, **kwargs)) + + return w -- 2.39.2