Skip to content

Commit 6ac89bb

Browse files
authored
Write fixes (#272)
* Added multi scp send test with timeout * Updated clients to yield event loop in between writes. * Moved eagain handling to base client * Updated changelog * Updated documentation
1 parent 59ad4b8 commit 6ac89bb

File tree

7 files changed

+92
-38
lines changed

7 files changed

+92
-38
lines changed

Changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Change Log
22
============
33

4+
2.5.3
5+
+++++
6+
7+
Fixes
8+
-----
9+
10+
* Sending files via ``scp_send`` or ``sftp_put`` with timeout set could timeout unexpectedly on opening remote file - #271.
11+
12+
413
2.5.2
514
+++++
615

doc/clients.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Below is a comparison of feature support for the two client types.
99
Feature ssh2-python (libssh2) ssh-python (libssh)
1010
=============================== ====================== ======================
1111
Agent forwarding No Not yet implemented
12-
Proxying/tunnelling Yes No
12+
Proxying/tunneling Yes No
1313
Kerberos (GSS) authentication Not supported Yes
1414
Private key file authentication Yes Yes
1515
Agent authentication Yes Yes
@@ -25,4 +25,4 @@ Keep-alive functionality Yes No
2525

2626
The default client offers the most features, but lacks certain authentication mechanisms like GSS-API and certificate authentication. Both client types are based on C libraries and offer similar levels of performance.
2727

28-
Users that need the authentication mechanisms not supported by the default client can ``from pssh.clients.ssh import ParallelSSHClient`` instead of the default client.
28+
Users that need the authentication mechanisms not supported by the default client can ``from pssh.clients.ssh import ParallelSSHClient, SSHClient`` instead of the default clients.

doc/installation.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ Dependency Minimum Version
3636
Building from Source
3737
----------------------
3838

39-
4039
``parallel-ssh`` is hosted on GitHub and the repository can be cloned with the following
4140

4241
.. code-block:: shell
@@ -48,7 +47,7 @@ To install from source run:
4847

4948
.. code-block:: shell
5049
51-
python setup.py install
50+
pip install .
5251
5352
Or for developing changes:
5453

@@ -60,10 +59,14 @@ Or for developing changes:
6059
Python 2
6160
--------
6261

63-
As of January 2021, Python 2 is no longer supported by the Python Software Foundation nor ``parallel-ssh`` - see `Sunset Python 2 <https://www.python.org/doc/sunset-python-2/>`_.
62+
As of January 1st 2021, Python 2 is no longer supported by the Python Software Foundation nor ``parallel-ssh`` - see `Sunset Python 2 <https://www.python.org/doc/sunset-python-2/>`_.
6463

6564
Versions of ``parallel-ssh<=2.4.0`` will still work.
6665

6766
Future releases are not guaranteed to be compatible or work at all with Python 2.
6867

68+
Versions ``>=2.5.0`` may still work, no code changes have yet been made to stop Python 2 from working, though no support is offered if they do not.
69+
70+
Vendors that distribute this package for Python 2 are advised to run integration tests themselves on versions ``>=2.5.0`` to confirm compatibility - no automated testing is performed on Python 2 by this project.
71+
6972
If your company requires Python 2 support contact the author directly at the email address on Github commits to discuss rates.

pssh/clients/base/single.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,25 @@ def run_command(self, command, sudo=False, user=None,
519519
host_out = self._make_host_output(channel, encoding, _timeout)
520520
return host_out
521521

522+
def _eagain_write_errcode(self, write_func, data, eagain, timeout=None):
523+
data_len = len(data)
524+
total_written = 0
525+
while total_written < data_len:
526+
rc, bytes_written = write_func(data[total_written:])
527+
total_written += bytes_written
528+
if rc == eagain:
529+
self.poll(timeout=timeout)
530+
sleep()
531+
532+
def _eagain_errcode(self, func, eagain, *args, **kwargs):
533+
timeout = kwargs.pop('timeout', self.timeout)
534+
with GTimeout(seconds=timeout, exception=Timeout):
535+
ret = func(*args, **kwargs)
536+
while ret == eagain:
537+
self.poll()
538+
ret = func(*args, **kwargs)
539+
return ret
540+
522541
def _eagain_write(self, write_func, data, timeout=None):
523542
raise NotImplementedError
524543

pssh/clients/native/single.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from collections import deque
2121
from warnings import warn
2222

23-
from gevent import sleep, spawn, get_hub, Timeout as GTimeout
23+
from gevent import sleep, spawn, get_hub
2424
from gevent.select import POLLIN, POLLOUT
2525
from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN
2626
from ssh2.exceptions import SFTPHandleError, SFTPProtocolError, \
@@ -314,13 +314,7 @@ def close_channel(self, channel):
314314
self._eagain(channel.close)
315315

316316
def _eagain(self, func, *args, **kwargs):
317-
timeout = kwargs.pop('timeout', self.timeout)
318-
with GTimeout(seconds=timeout, exception=Timeout):
319-
ret = func(*args, **kwargs)
320-
while ret == LIBSSH2_ERROR_EAGAIN:
321-
self.poll()
322-
ret = func(*args, **kwargs)
323-
return ret
317+
return self._eagain_errcode(func, LIBSSH2_ERROR_EAGAIN, *args, **kwargs)
324318

325319
def _make_sftp(self):
326320
"""Make SFTP client from open transport"""
@@ -707,18 +701,12 @@ def poll(self, timeout=None):
707701
events |= POLLOUT
708702
self._poll_socket(events, timeout=timeout)
709703

710-
def eagain_write(self, write_func, data, timeout=None):
704+
def _eagain_write(self, write_func, data, timeout=None):
711705
"""Write data with given write_func for an ssh2-python session while
712706
handling EAGAIN and resuming writes from last written byte on each call to
713707
write_func.
714708
"""
715-
data_len = len(data)
716-
total_written = 0
717-
while total_written < data_len:
718-
rc, bytes_written = write_func(data[total_written:])
719-
total_written += bytes_written
720-
if rc == LIBSSH2_ERROR_EAGAIN:
721-
self.poll(timeout=timeout)
709+
return self._eagain_write_errcode(write_func, data, LIBSSH2_ERROR_EAGAIN, timeout=timeout)
722710

723-
def _eagain_write(self, write_func, data, timeout=None):
724-
return self.eagain_write(write_func, data, timeout=timeout)
711+
def eagain_write(self, write_func, data, timeout=None):
712+
return self._eagain_write(write_func, data, timeout=timeout)

pssh/clients/ssh/single.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def close_channel(self, channel):
304304
self._eagain(channel.close, timeout=self.timeout)
305305

306306
def poll(self, timeout=None):
307-
"""ssh-python based co-operative gevent select on session socket."""
307+
"""ssh-python based co-operative gevent poll on session socket."""
308308
timeout = self.timeout if timeout is None else timeout
309309
directions = self.session.get_poll_flags()
310310
if directions == 0:
@@ -318,19 +318,7 @@ def poll(self, timeout=None):
318318

319319
def _eagain(self, func, *args, **kwargs):
320320
"""Run function given and handle EAGAIN for an ssh-python session"""
321-
timeout = kwargs.pop('timeout', self.timeout)
322-
with GTimeout(seconds=timeout, exception=Timeout):
323-
ret = func(*args, **kwargs)
324-
while ret == SSH_AGAIN:
325-
self.poll(timeout=timeout)
326-
ret = func(*args, **kwargs)
327-
return ret
321+
return self._eagain_errcode(func, SSH_AGAIN, *args, **kwargs)
328322

329323
def _eagain_write(self, write_func, data, timeout=None):
330-
data_len = len(data)
331-
total_written = 0
332-
while total_written < data_len:
333-
rc, bytes_written = write_func(data[total_written:])
334-
total_written += bytes_written
335-
if rc == SSH_AGAIN:
336-
self.poll(timeout=timeout)
324+
return self._eagain_write_errcode(write_func, data, SSH_AGAIN, timeout=timeout)

tests/native/test_parallel_client.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,53 @@ def test_scp_send_dir_recurse(self):
15821582
except OSError:
15831583
pass
15841584

1585+
def test_scp_send_large_files_timeout(self):
1586+
hosts = ['127.0.0.1%s' % (i,) for i in range(1, 11)]
1587+
servers = [OpenSSHServer(host, port=self.port) for host in hosts]
1588+
for server in servers:
1589+
server.start_server()
1590+
client = ParallelSSHClient(
1591+
hosts, port=self.port, pkey=self.user_key, num_retries=1, timeout=1)
1592+
local_filename = 'test_file'
1593+
remote_filepath = 'file_copy'
1594+
copy_args = [{
1595+
'local_file': local_filename,
1596+
'remote_file': 'host_%s_%s' % (n, remote_filepath)}
1597+
for n in range(len(hosts))]
1598+
remote_file_names = [arg['remote_file'] for arg in copy_args]
1599+
sha = sha256()
1600+
with open(local_filename, 'wb') as file_h:
1601+
for _ in range(5000):
1602+
data = os.urandom(1024)
1603+
file_h.write(data)
1604+
sha.update(data)
1605+
source_file_sha = sha.hexdigest()
1606+
sha = sha256()
1607+
cmds = client.scp_send('%(local_file)s', '%(remote_file)s', copy_args=copy_args)
1608+
try:
1609+
joinall(cmds, raise_error=True)
1610+
except Exception:
1611+
raise
1612+
else:
1613+
sleep(.2)
1614+
for remote_file_name in remote_file_names:
1615+
remote_file_abspath = os.path.expanduser('~/' + remote_file_name)
1616+
self.assertTrue(os.path.isfile(remote_file_abspath))
1617+
with open(remote_file_abspath, 'rb') as remote_fh:
1618+
for data in remote_fh:
1619+
sha.update(data)
1620+
remote_file_sha = sha.hexdigest()
1621+
sha = sha256()
1622+
self.assertEqual(source_file_sha, remote_file_sha)
1623+
finally:
1624+
try:
1625+
os.unlink(local_filename)
1626+
for remote_file_name in remote_file_names:
1627+
remote_file_abspath = os.path.expanduser('~/' + remote_file_name)
1628+
os.unlink(remote_file_abspath)
1629+
except OSError:
1630+
pass
1631+
15851632
def test_scp_send(self):
15861633
server2_host = '127.0.0.11'
15871634
server3_host = '127.0.0.12'

0 commit comments

Comments
 (0)