fix: handle client disconnect in compute console WebSocket forwarding

The compute-side telnet_forward (and vnc_forward) let the
WebSocketDisconnect that starlette raises from send_bytes once the peer
is gone escape to the generic task-exception handler. Its str() is
empty, so every client disconnect during active node output logged a
message-less warning:

  WARNING gns3server.compute.base_node:658 Exception while forwarding
  WebSocket data to TELNET server:

Catch WebSocketDisconnect in both forwarders and log it at INFO as the
normal end of a session, and format the remaining task exceptions with
!r so their type stays visible. Mirrors the controller-side fix for
the same scenario (2324dcd74).
This commit is contained in:
YueGuobin committed 2026-08-29 10:09:05 +08:00
1 parent f95d4c7a4e
commit 3cf9dbff5b
2 files changed
+79 -7

No files matched your search

+54
View File
@@ -453,3 +453,57 @@ async def test_stop_ubridge_clears_marker_bridges(compute_project, manager):
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
await node._stop_ubridge()
assert node._marker_filter_bridges == {}
class _GoneConsoleWebsocket:
"""
Stand-in for the compute-side WebSocket when the peer (the controller, or a
browser) is already gone: receive() yields the disconnect message and
send_bytes raises WebSocketDisconnect — what starlette raises from send
after the transport reports OSError.
"""
def __init__(self):
from types import SimpleNamespace
self.client = SimpleNamespace(host="127.0.0.1", port=5000)
async def receive(self):
return {"type": "websocket.disconnect"}
async def send_bytes(self, data):
from starlette.websockets import WebSocketDisconnect
raise WebSocketDisconnect(code=1006)
@pytest.mark.asyncio
async def test_console_websocket_client_disconnect_while_node_output_streams(
compute_project, manager, port_manager, monkeypatch, caplog):
# regression test: the client disconnects while the node is still streaming
# console output. telnet_forward used to let the (empty-str) WebSocketDisconnect
# from send_bytes escape, logging a message-less WARNING.
import asyncio
import logging
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node.status = "started"
telnet_reader = MagicMock()
telnet_reader.at_eof.return_value = False
telnet_reader.read = AsyncioMagicMock(return_value=b"device output")
telnet_writer = MagicMock()
telnet_writer.wait_closed = AsyncioMagicMock()
async def fake_open_connection(*args, **kwargs):
return telnet_reader, telnet_writer
monkeypatch.setattr(asyncio, "open_connection", fake_open_connection)
with caplog.at_level(logging.INFO):
await node.start_websocket_console(_GoneConsoleWebsocket())
assert telnet_writer.close.called
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
assert any(
"has disconnected from compute console WebSocket while node output" in r.message
for r in caplog.records
)