Skip to content

Commit 8ae6b2b

Browse files
committed
Improve handling of exceptions in connection_lost() callbacks
This commit adds wrappers around calls to connection_lost() in the SSHClient, SSHServer, SSHClientSession, and SSHServerSession classes to make sure that cleanup completes properly in the SSHConnection and SSHChannel classes. Exceptions in connection_lost() will be reported in the debug log but cleanup operations will continue, ignoring those exceptions. Thanks go to Danil Slinchuk for reporting this issue!
1 parent 13d8bbc commit 8ae6b2b

4 files changed

Lines changed: 49 additions & 2 deletions

File tree

asyncssh/channel.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import inspect
2727
import re
2828
import signal as _signal
29+
import sys
2930
from types import MappingProxyType
3031
from typing import TYPE_CHECKING, Any, AnyStr, Awaitable, Callable
3132
from typing import Dict, Generic, Iterable, List, Mapping, Optional
@@ -225,7 +226,13 @@ def _cleanup(self, exc: Optional[Exception] = None) -> None:
225226
self._request_waiters = []
226227

227228
if self._session is not None:
228-
self._session.connection_lost(exc)
229+
# pylint: disable=broad-except
230+
try:
231+
self._session.connection_lost(exc)
232+
except Exception:
233+
self.logger.debug1('Uncaught exception in session ignored',
234+
exc_info=sys.exc_info)
235+
229236
self._session = None
230237

231238
self._close_event.set()

asyncssh/connection.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,13 @@ def _cleanup(self, exc: Optional[Exception]) -> None:
10751075
self._wait = None
10761076

10771077
if self._owner: # pragma: no branch
1078-
self._owner.connection_lost(exc)
1078+
# pylint: disable=broad-except
1079+
try:
1080+
self._owner.connection_lost(exc)
1081+
except Exception:
1082+
self.logger.debug1('Uncaught exception in owner ignored',
1083+
exc_info=sys.exc_info)
1084+
10791085
self._owner = None
10801086

10811087
self._cancel_login_timer()

tests/test_channel.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,15 @@ def session_started(self):
247247
chan.close()
248248

249249

250+
class _ClientSessionCleanupError(asyncssh.SSHClientSession):
251+
"""Test of exception during client session cleanup"""
252+
253+
def connection_lost(self, exc):
254+
"""Raise an error when a client session is cleaned up"""
255+
256+
raise RuntimeError('Exception in session cleanup test')
257+
258+
250259
class _ChannelServer(Server):
251260
"""Server for testing the AsyncSSH channel API"""
252261

@@ -1761,6 +1770,13 @@ async def test_unknown_action(self):
17611770
await chan.wait_closed()
17621771
self.assertEqual(session.exit_status, 255)
17631772

1773+
@asynctest
1774+
async def test_client_session_cleanup_error(self):
1775+
"""Test error in client session cleanup"""
1776+
1777+
async with self.connect() as conn:
1778+
await conn.create_session(_ClientSessionCleanupError)
1779+
17641780

17651781
class _TestChannelNoPTY(ServerTestCase):
17661782
"""Unit tests for AsyncSSH channel module with PTYs disallowed"""

tests/test_connection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,17 @@ def connection_made(self, conn):
282282
raise RuntimeError('Exception handler test')
283283

284284

285+
class _ClientCleanupError(asyncssh.SSHClient):
286+
"""Test of exception during client cleanup"""
287+
288+
def connection_lost(self, exc):
289+
"""Raise an error when a client is cleaned up"""
290+
291+
# pylint: disable=unused-argument
292+
293+
raise RuntimeError('Exception in cleanup test')
294+
295+
285296
class _TunnelServer(Server):
286297
"""Allow forwarding to test server host key request tunneling"""
287298

@@ -1719,6 +1730,13 @@ async def test_internal_error(self):
17191730
with self.assertRaises(RuntimeError):
17201731
await self.create_connection(_InternalErrorClient)
17211732

1733+
@asynctest
1734+
async def test_client_cleanup_error(self):
1735+
"""Test error in client cleanup"""
1736+
1737+
async with self.connect(client_factory=_ClientCleanupError):
1738+
pass
1739+
17221740

17231741
@patch_extra_kex
17241742
class _TestConnectionNoStrictKex(ServerTestCase):

0 commit comments

Comments
 (0)