Fix telnet console silent-proxy hang on non-ConnectionError exit (#2344)

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.
This commit is contained in:
arl1984 2026-04-16 14:46:49 +00:00
parent ae3ae46151
commit de0d050689

View File

@ -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():