From 5af910add77a9220428a5b4588a310094fcba348 Mon Sep 17 00:00:00 2001 From: Ivar Matstoms <ivma@lysator.liu.se> Date: Sat, 15 Feb 2025 10:35:04 +0100 Subject: [PATCH] Fix: Handle chunked packets --- pyskom/client/client_base.py | 102 +++++++++++++++++----------- pyskom/client/parser.py | 7 +- pyskom/client/shell/client_shell.py | 7 ++ 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/pyskom/client/client_base.py b/pyskom/client/client_base.py index cd18c2f..ec147eb 100644 --- a/pyskom/client/client_base.py +++ b/pyskom/client/client_base.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Awaitable from .async_types import ASYNC_TYPES from .errors import ERRORS from .kom_error import InvalidErrorCodeError -from .parser import MessageParser, parse_kom_type +from .parser import MessageParser, parse_kom_type, MessageEOF from .types import Info, ConfZInfo, Conference, ConfType from .writer import to_hollerith, MessageWriter, write_kom_type from .kom_types import KomType, SimpleType, KomTypeGroup, AsyncMessage @@ -36,45 +36,54 @@ class ClientBase: async def on_packet(self, packet: bytes): parser = MessageParser(packet, cursor=0) + message_start = 0 while True: - reply_type = parser.read_reply() - if reply_type is None: - break - elif reply_type == 61: # = (result ok) - ref = parser.read_int() - if (call := self._calls.pop(ref, None)) is None: - logger.warning("Unknown ref number received, discarding") - return - if call[1] is not None: - call[0].set_result(call[1](parser)) - else: - call[0].set_result(None) - elif reply_type == 37: # % (Error) - ref = parser.read_int() - if (call := self._calls.pop(ref, None)) is None: - logger.warning("Unknown ref number received, discarding") - return - error_code = parser.read_int() - error_status = parser.read_int() - - try: - error_type = ERRORS[error_code] - except KeyError: - raise InvalidErrorCodeError() - - call[1].set_exception(error_type(ref, error_status)) - elif reply_type == 58: - _ = parser.read_int() - async_type_nr = parser.read_int() - if (async_type := ASYNC_TYPES.get(async_type_nr)) is None: - logger.warning(f"Received unknown async-nr: {async_type_nr!r}") - return - message = async_type.parse(parser) - if self._on_event_hook is not None: - await self._on_event_hook(message) + try: + message_start = parser.cursor + reply_type = parser.read_reply() + if reply_type is None: + return None # Everything read + elif reply_type == 61: # = (result ok) + ref = parser.read_int() + if (call := self._calls.get(ref, None)) is None: + logger.warning("Unknown ref number received, discarding") + return + if call[1] is not None: + parsed = call[1](parser) + else: + parsed = None + del self._calls[ref] + + call[0].set_result(parsed) + + elif reply_type == 37: # % (Error) + ref = parser.read_int() + if (call := self._calls.pop(ref, None)) is None: + logger.warning("Unknown ref number received, discarding") + return + error_code = parser.read_int() + error_status = parser.read_int() + + try: + error_type = ERRORS[error_code] + except KeyError: + raise InvalidErrorCodeError() + + call[1].set_exception(error_type(ref, error_status)) + elif reply_type == 58: + _ = parser.read_int() + async_type_nr = parser.read_int() + if (async_type := ASYNC_TYPES.get(async_type_nr)) is None: + logger.warning(f"Received unknown async-nr: {async_type_nr!r}") + return + message = async_type.parse(parser) + if self._on_event_hook is not None: + asyncio.ensure_future(self._on_event_hook(message)) - else: - logger.warning(fr"Unable to parse packet {packet!r}") + else: + logger.warning(fr"Unable to parse packet {packet!r}") + except MessageEOF: + return packet[message_start:] async def send_request(self, call_nr: int, args: bytearray | None, parse_callable: Callable[[MessageParser], Any] | None) -> Any: @@ -134,13 +143,26 @@ class ClientBase: if self._on_connected_hook is not None: asyncio.ensure_future(self._on_connected_hook()) + buff = b"" while self.alive: - data = await self.reader.read(1024) + data = await self.reader.read(8192) if len(data) == 0: self.alive = False break - asyncio.ensure_future(self.on_packet(data)) # This might not be perfect + buff += data + if not buff[-1] == 10: + # If buffer doesn't end with newline the message is chunked + continue + try: + remaining = await self.on_packet(buff) # This might not be perfect + except Exception as e: + logger.error(e, exc_info=True, stack_info=True) + else: + if remaining is None: + buff = b"" + else: + buff = remaining def on_async(self, f: Callable[[AsyncMessage], Awaitable[None]]): self._on_event_hook = f diff --git a/pyskom/client/parser.py b/pyskom/client/parser.py index 5968cb1..6d74424 100644 --- a/pyskom/client/parser.py +++ b/pyskom/client/parser.py @@ -14,6 +14,10 @@ class MessageParser: self._cursor = cursor self._encoding = encoding + @property + def cursor(self) -> int: + return self._cursor + def _next_element(self): while self._cursor < self._len and (self._message[self._cursor] == 32 or self._message[self._cursor] == 10): self._cursor += 1 @@ -44,8 +48,7 @@ class MessageParser: while h_cursor < self._len and self._message[h_cursor] != 72: h_cursor += 1 if h_cursor >= self._len: - # Todo - raise Exception("Unable to parse str") + raise MessageEOF() length = int(self._message[self._cursor:h_cursor]) string = self._message[h_cursor + 1:h_cursor + 1 + length].decode(self._encoding) self._cursor = h_cursor + 1 + length diff --git a/pyskom/client/shell/client_shell.py b/pyskom/client/shell/client_shell.py index 11f726a..f066067 100644 --- a/pyskom/client/shell/client_shell.py +++ b/pyskom/client/shell/client_shell.py @@ -86,6 +86,10 @@ class RepPysKomClient: async def cmd_help(self, args: Namespace): self.parser.print_help() + @command(("get-person-stat", "49")) + async def cmd_get_person_stat(self, args: Namespace): + print(await self.client.get_person_stat(args.person)) + @command(("login", "62")) async def cmd_login(self, args: Namespace): password = await self.session.prompt_async("Password: ", is_password=True) @@ -132,6 +136,9 @@ class RepPysKomClient: subparsers.add_parser('get-info', aliases=["94"],help='get-info [94] call') + call_49 = subparsers.add_parser("get-person-stat", aliases=["49"], help='[49] call') + call_49.add_argument("person", type=int, help="Person id to loop up") + call_62 = subparsers.add_parser("login", aliases=["62"], help='login [62] call') call_62.add_argument("person", type=int, help="Person id to login as") call_62.add_argument("-i","--invisible", action="store_true", help="Login as invisible") -- GitLab