Skip to content

Commit 0567bda

Browse files
authored
Disc (#402)
* Fix socket shutdown on disconnect - resolves #274 * Updated tests * Updated clients, changelog * Move disconnect to internal function * Refactor ci tests. Updated disconnect on clients * Refactor for keep alive * Added example script * Updated setup.cfg * Updated tunnel socket failure tests. Added tunnel test. * Added shutdown tests, updated changelog * Updated tunnel
1 parent 45f20fa commit 0567bda

18 files changed

+557
-255
lines changed

Changelog.rst

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
Change Log
2-
============
2+
==========
3+
4+
2.14.0
5+
++++++
6+
7+
Changes
8+
-------
9+
10+
* Handle disconnects better to allow for file descriptor reuse for both clients.
11+
* Parallel clients no longer forcefully disconnect their clients at de-allocation -
12+
now done by each individual ``SSHClient`` instead when that ``SSHClient`` goes out of scope.
13+
This allows reading of output and anything associated with output, exit codes et al,
14+
to work as long as one of either the client or an associated output object is alive.
15+
* ``SSHClient.disconnect`` is now a no-op and deprecated - handled by object de-allocation.
16+
* ``SSHClient.eagain`` is now a public function - wrapper for polling socket and calling a given socket using function.
17+
* ``SSHClient.eagain_write`` is now a public function - wrapper for polling socket and calling a given socket using
18+
write function.
19+
* ``SSHClient``, ``TunnelServer`` and ``LocalForwarder`` now use their own gevent pools for greenlets spawned so they
20+
are cleaned up correctly at shutdown.
21+
22+
Fixes
23+
------
24+
25+
* Forwarder threads used for proxies would not exit gracefully at interpreter shutdown, sometimes causing segfaults.
26+
* Client, both parallel and single, going out of scope would cause reading output from existing output objects
27+
to break.
28+
* Explicitly calling ``SSHClient.disconnect`` would sometimes cause segfaults at interpreter shutdown.
29+
* Keepalives being configured on native client would keep client in scope forever.
330

431

532
2.13.0
@@ -418,7 +445,7 @@ Fixes
418445
------
419446

420447
* Reading from output streams with timeout via `run_command(<..>, timeout=<timeout>)` would raise timeout early when
421-
trying to read from a stream with no data written to it while other streams have pending data - #180.
448+
trying to read from a stream with no data written to it while other streams have pending data - #180.
422449

423450

424451
1.12.0

ci/integration_tests/native/test_parallel_client.py renamed to ci/integration_tests/libssh2_clients/test_parallel_client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1779,7 +1779,7 @@ def test_client_disconnect(self):
17791779
client.join(consume_output=True)
17801780
single_client = list(client._host_clients.values())[0]
17811781
del client
1782-
self.assertEqual(single_client.session, None)
1782+
self.assertIsNotNone(single_client.session)
17831783

17841784
def test_client_disconnect_error(self):
17851785
def disc():
@@ -1788,7 +1788,7 @@ def disc():
17881788
pkey=self.user_key, num_retries=1)
17891789
output = client.run_command(self.cmd)
17901790
client.join(output)
1791-
client._host_clients[(0, self.host)].disconnect = disc
1791+
client._host_clients[(0, self.host)]._disconnect = disc
17921792
del client
17931793

17941794
def test_multiple_join_timeout(self):

ci/integration_tests/native/test_single_client.py renamed to ci/integration_tests/libssh2_clients/test_single_client.py

+87-15
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,22 @@
1616
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1717

1818
import os
19+
import pytest
1920
import shutil
2021
import subprocess
2122
import tempfile
2223
from datetime import datetime
23-
from hashlib import sha256
24-
from tempfile import NamedTemporaryFile
25-
from unittest.mock import MagicMock, call, patch
26-
27-
import pytest
2824
from gevent import sleep, spawn, Timeout as GTimeout, socket
25+
from hashlib import sha256
2926
from pytest import raises
3027
from ssh2.exceptions import (SocketDisconnectError, BannerRecvError, SocketRecvError,
3128
AgentConnectionError, AgentListIdentitiesError,
3229
AgentAuthenticationError, AgentGetIdentityError, SFTPProtocolError,
3330
AuthenticationError as SSH2AuthenticationError,
3431
)
3532
from ssh2.session import Session
33+
from tempfile import NamedTemporaryFile
34+
from unittest.mock import MagicMock, call, patch
3635

3736
from pssh.clients.native import SSHClient
3837
from pssh.exceptions import (AuthenticationException, ConnectionErrorException,
@@ -272,16 +271,14 @@ class _SSHClient(SSHClient):
272271
pkey=self.user_key,
273272
num_retries=1,
274273
allow_agent=False)
275-
client.disconnect()
276274
client.pkey = None
277-
del client.session
278-
del client.sock
275+
client._disconnect()
279276
client._connect(self.host, self.port)
280277
client._init_session()
281278
client.IDENTITIES = (self.user_key,)
282279
# Default identities auth only should succeed
283280
client._identity_auth()
284-
client.disconnect()
281+
client._disconnect()
285282
client._connect(self.host, self.port)
286283
client._init_session()
287284
# Auth should succeed
@@ -360,9 +357,37 @@ def test_handshake_fail(self):
360357
client = SSHClient(self.host, port=self.port,
361358
pkey=self.user_key,
362359
num_retries=1)
363-
client.session.disconnect()
360+
client.eagain(client.session.disconnect)
364361
self.assertRaises((SocketDisconnectError, BannerRecvError, SocketRecvError), client._init_session)
365362

363+
@patch('gevent.socket.socket')
364+
@patch('pssh.clients.native.single.Session')
365+
def test_sock_shutdown_fail(self, mock_sess, mock_sock):
366+
sess = MagicMock()
367+
sock = MagicMock()
368+
mock_sess.return_value = sess
369+
mock_sock.return_value = sock
370+
371+
hand_mock = MagicMock()
372+
sess.handshake = hand_mock
373+
retries = 2
374+
client = SSHClient(self.host, port=self.port,
375+
num_retries=retries,
376+
timeout=.1,
377+
retry_delay=.1,
378+
_auth_thread_pool=False,
379+
allow_agent=False,
380+
)
381+
self.assertIsInstance(client, SSHClient)
382+
hand_mock.side_effect = AuthenticationError
383+
sock.closed = False
384+
sock.detach = MagicMock()
385+
sock.detach.side_effect = Exception
386+
self.assertRaises(AuthenticationError, client._init_session)
387+
self.assertEqual(sock.detach.call_count, retries)
388+
client._disconnect()
389+
self.assertIsNone(client.sock)
390+
366391
def test_stdout_parsing(self):
367392
dir_list = os.listdir(os.path.expanduser('~'))
368393
host_out = self.client.run_command('ls -la')
@@ -438,15 +463,30 @@ def test_multiple_clients_exec_terminates_channels(self):
438463
# and break subsequent sessions even on different socket and
439464
# session
440465
def scope_killer():
441-
for _ in range(5):
466+
for _ in range(20):
442467
client = SSHClient(self.host, port=self.port,
443468
pkey=self.user_key,
444469
num_retries=1,
445470
allow_agent=False)
446471
host_out = client.run_command(self.cmd)
447472
output = list(host_out.stdout)
448473
self.assertListEqual(output, [self.resp])
449-
client.disconnect()
474+
475+
scope_killer()
476+
477+
def test_multiple_clients_exec_terminates_channels_explicit_disc(self):
478+
# Explicit disconnects should not affect subsequent connections
479+
def scope_killer():
480+
for _ in range(20):
481+
client = SSHClient(self.host, port=self.port,
482+
pkey=self.user_key,
483+
num_retries=1,
484+
allow_agent=False)
485+
host_out = client.run_command(self.cmd)
486+
output = list(host_out.stdout)
487+
self.assertListEqual(output, [self.resp])
488+
client._disconnect()
489+
450490
scope_killer()
451491

452492
def test_agent_auth_exceptions(self):
@@ -1036,7 +1076,8 @@ def _make_sftp():
10361076
client._make_sftp_eagain = _make_sftp
10371077
self.assertRaises(SFTPError, client._make_sftp)
10381078

1039-
def test_disconnect_exc(self):
1079+
@patch('pssh.clients.native.single.Session')
1080+
def test_disconnect_exc(self, mock_sess):
10401081
class DiscError(Exception):
10411082
pass
10421083

@@ -1091,5 +1132,36 @@ def test_many_short_lived_commands(self):
10911132
duration = end.total_seconds()
10921133
self.assertTrue(duration < timeout * 0.9, msg=f"Duration of instant cmd is {duration}")
10931134

1094-
# TODO
1095-
# * read output callback
1135+
def test_output_client_scope(self):
1136+
"""Output objects should keep client alive while they are in scope even if client is not."""
1137+
def make_client_run():
1138+
client = SSHClient(self.host, port=self.port,
1139+
pkey=self.user_key,
1140+
num_retries=1,
1141+
allow_agent=False,
1142+
)
1143+
host_out = client.run_command("%s; exit 1" % (self.cmd,))
1144+
return host_out
1145+
1146+
output = make_client_run()
1147+
stdout = list(output.stdout)
1148+
self.assertListEqual(stdout, [self.resp])
1149+
self.assertEqual(output.exit_code, 1)
1150+
1151+
def test_output_client_scope_disconnect(self):
1152+
"""Calling deprecated .disconnect on client that also goes out of scope should not break reading
1153+
any unread output."""
1154+
def make_client_run():
1155+
client = SSHClient(self.host, port=self.port,
1156+
pkey=self.user_key,
1157+
num_retries=1,
1158+
allow_agent=False,
1159+
)
1160+
host_out = client.run_command("%s; exit 1" % (self.cmd,))
1161+
client.disconnect()
1162+
return host_out
1163+
1164+
output = make_client_run()
1165+
stdout = list(output.stdout)
1166+
self.assertListEqual(stdout, [self.resp])
1167+
self.assertEqual(output.exit_code, 1)

0 commit comments

Comments
 (0)