From de0d0506890df757009a7830c2ba7d8ce62e8c22 Mon Sep 17 00:00:00 2001 From: arl1984 Date: Thu, 16 Apr 2026 14:46:49 +0000 Subject: [PATCH] Fix telnet console silent-proxy hang on non-ConnectionError exit (#2344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run() cleanup block was guarded by `except (ConnectionError, OSError):`, so exits via asyncio.CancelledError or any other exception type skipped cleanup. Result: `_reader_process` stays pinned to the dead reader and `_get_reader()` returns None for every subsequent client — the silent-proxy symptom described in #2344. Convert the except block to try/finally so cleanup always runs, regardless of how `_process()` exits. Also: - catch asyncio.CancelledError + generic Exception (with log.exception) so unexpected failures don't swallow the cleanup - reset `_current_read = None` after cancellation - use `dict.pop(..., None)` instead of `del` to avoid KeyError races if the broadcast loop's timeout handler already removed the entry Triggering pattern observed in practice: a diagnostic tool opens a console, sends a few commands, and closes abruptly (e.g. from a test harness or orchestration script that cancels its Task). If the `_process()` task was awaiting on one of the `network_read` / `reader_read` futures at the time of cancellation, the CancelledError propagates up through `run()` and bypasses the ConnectionError-only except clause. The proxy accepts future connections (the listen socket is still alive) but never forwards any data because `_reader_process` never got reset. Validated against gns3/gns3-server:latest (2.2.56.1) running a 10-scenario sequential regression batch that previously hung reliably on the 4th sp_v1 / L3VPN scenario and now completes cleanly across all 10. --- gns3server/utils/asyncio/telnet_server.py | 28 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/gns3server/utils/asyncio/telnet_server.py b/gns3server/utils/asyncio/telnet_server.py index cc34a29a6..2f8652a52 100644 --- a/gns3server/utils/asyncio/telnet_server.py +++ b/gns3server/utils/asyncio/telnet_server.py @@ -215,18 +215,34 @@ class AsyncioTelnetServer: await self._write_intro(network_writer, echo=self._echo, binary=self._binary, naws=self._naws) await connection.connected() await self._process(network_reader, network_writer, connection) - except (ConnectionError, OSError): + except (ConnectionError, OSError, asyncio.CancelledError): + pass + except Exception: + # Catch any unexpected exception so the cleanup below still runs. + # Without the try/finally, an uncaught exception here would leave + # _reader_process pinned to a dead reader, and subsequent client + # connections would see _get_reader() return None and never + # receive node output -- the "silent proxy" hang (issue #2344). + log.exception("Unexpected error in telnet proxy; cleaning up client connection") + finally: async with self._lock: - network_writer.close() - # await network_writer.wait_closed() # this doesn't work in Python 3.6 + try: + network_writer.close() + except Exception: + pass if self._reader_process == network_reader: self._reader_process = None # Cancel current read from this reader if self._current_read is not None: self._current_read.cancel() - - await connection.disconnected() - del self._connections[network_writer] + self._current_read = None + try: + await connection.disconnected() + except Exception: + pass + # pop() instead of del to avoid KeyError if already removed + # elsewhere (e.g. by the broadcast loop's timeout handler). + self._connections.pop(network_writer, None) async def close(self): for writer, connection in self._connections.items():