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