]> git.xonotic.org Git - xonotic/xonotic.git/commitdiff
slist: init
authorTimePath <andrew.hardaker1995@gmail.com>
Sun, 13 May 2018 07:53:08 +0000 (17:53 +1000)
committerTimePath <andrew.hardaker1995@gmail.com>
Sun, 13 May 2018 07:55:45 +0000 (17:55 +1000)
derivation.nix
misc/infrastructure/python/.gitignore [new file with mode: 0644]
misc/infrastructure/python/mypy.ini [new file with mode: 0644]
misc/infrastructure/python/slist/__init__.py [new file with mode: 0644]
misc/infrastructure/python/slist/master.py [new file with mode: 0644]
misc/infrastructure/python/slist/utils.py [new file with mode: 0644]

index d0365715ca9a234943b3715607b1d4d1365de7e6..628c95f5361d3212a1926b9997a023a87271c546 100644 (file)
@@ -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 (file)
index 0000000..020de35
--- /dev/null
@@ -0,0 +1 @@
+/.mypy_cache
diff --git a/misc/infrastructure/python/mypy.ini b/misc/infrastructure/python/mypy.ini
new file mode 100644 (file)
index 0000000..2f1052d
--- /dev/null
@@ -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 (file)
index 0000000..b9f9ca8
--- /dev/null
@@ -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 (file)
index 0000000..12bfd22
--- /dev/null
@@ -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 (file)
index 0000000..1cc36aa
--- /dev/null
@@ -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