mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-30 22:10:13 +03:00
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:
parent
f95d4c7a4e
commit
3cf9dbff5b
@ -640,10 +640,20 @@ class BaseNode:
|
||||
|
||||
async def telnet_forward(telnet_reader):
|
||||
|
||||
while not telnet_reader.at_eof():
|
||||
data = await telnet_reader.read(1024)
|
||||
if data:
|
||||
await websocket.send_bytes(data)
|
||||
try:
|
||||
while not telnet_reader.at_eof():
|
||||
data = await telnet_reader.read(1024)
|
||||
if data:
|
||||
await websocket.send_bytes(data)
|
||||
except WebSocketDisconnect:
|
||||
# the client disconnected while node output was still streaming:
|
||||
# normal end of the session, not an error. Starlette raises
|
||||
# WebSocketDisconnect (whose str() is empty) from send once the
|
||||
# peer is gone, which used to surface as a message-less warning.
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute"
|
||||
f" console WebSocket while node output was being forwarded"
|
||||
)
|
||||
|
||||
# keep forwarding websocket data in both direction
|
||||
if sys.version_info >= (3, 11, 0):
|
||||
@ -657,7 +667,7 @@ class BaseNode:
|
||||
if task.exception():
|
||||
log.warning(
|
||||
f"Exception while forwarding WebSocket data to "
|
||||
f"{self._console_type.upper()} server: {task.exception()}"
|
||||
f"{self._console_type.upper()} server: {task.exception()!r}"
|
||||
)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
@ -728,8 +738,16 @@ class BaseNode:
|
||||
data = await vnc_reader.read(65536) # Larger buffer for VNC frames
|
||||
if data:
|
||||
await websocket.send_bytes(data)
|
||||
except WebSocketDisconnect:
|
||||
# the browser disconnected while VNC frames were still streaming
|
||||
# (starlette raises WebSocketDisconnect with an empty str() from
|
||||
# send once the peer is gone — not an error)
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute "
|
||||
f"VNC console WebSocket while frames were being forwarded"
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"Exception while forwarding VNC data to WebSocket: {e}")
|
||||
log.warning(f"Exception while forwarding VNC data to WebSocket: {e!r}")
|
||||
|
||||
# Keep forwarding WebSocket data in both directions
|
||||
if sys.version_info >= (3, 11, 0):
|
||||
@ -741,7 +759,7 @@ class BaseNode:
|
||||
done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in done:
|
||||
if task.exception():
|
||||
log.warning(f"Exception while forwarding WebSocket data to VNC server: {task.exception()}")
|
||||
log.warning(f"Exception while forwarding WebSocket data to VNC server: {task.exception()!r}")
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user