Skip to content

Commit 56db95f

Browse files
author
Pan
committed
Added timeout exception raising on output reading time out. Added timeout file read test, updated tests
1 parent ab79b3a commit 56db95f

File tree

5 files changed

+123
-41
lines changed

5 files changed

+123
-41
lines changed

Changelog.rst

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

4+
1.5.1
5+
++++++
6+
7+
Fixes
8+
--------
9+
10+
* Output ``pssh.exceptions.Timeout`` exception raising was not enabled.
11+
412
1.5.0
513
++++++
614

doc/advanced.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,38 @@ The native clients have timeout functionality on reading output and ``client.joi
178178
179179
The client will raise a ``Timeout`` exception if remote commands have not finished within five seconds in the above examples.
180180

181+
In some cases, such as when the remote command never terminates unless interrupted, it is necessary to use PTY and to close the channel to force the process to be terminated before a ``join`` sans timeout can complete. For example:
182+
183+
.. code-block:: python
184+
185+
output = client.run_command('tail -f /var/log/messages', use_pty=True)
186+
client.join(output, timeout=1)
187+
# Closing channel which has PTY has the effect of terminating
188+
# any running processes started on that channel.
189+
for host in client.hosts:
190+
client.host_clients[host].close_channel(output[host].channel)
191+
client.join(output)
192+
193+
Without a PTY, the ``join`` will complete but the remote process will be left running as per SSH protocol specifications.
194+
195+
Output reading and Timeouts
196+
______________________________
197+
198+
Furthermore, once reading output has timed out, it is necessary to restart the output generators as by Python design they only iterate once. This can be done as follows:
199+
200+
.. code-block:: python
201+
202+
output = client.run_command(.., timeout=1)
203+
for host, host_out in output.items():
204+
try:
205+
stdout = list(host_out.stdout)
206+
except Timeout:
207+
stdout_buf = client.host_clients[host].read_output_buffer(
208+
client.host_clients[host].read_output(
209+
output[host].channel, timeout=1))
210+
# Reset generator to be able to gather new output
211+
host_out.stdout = stdout_buf
212+
181213
.. note::
182214

183215
``join`` with a timeout forces output to be consumed as otherwise the pending output will keep the channel open and make it appear as if command has not yet finished.

pssh/native/_ssh2.c

Lines changed: 46 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pssh/native/_ssh2.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ from ssh2.sftp_handle cimport SFTPHandle
3333
from ssh2.exceptions import SFTPIOError
3434
from ssh2.utils cimport to_bytes
3535

36-
from ..exceptions import SessionError
36+
from ..exceptions import SessionError, Timeout
3737

3838

3939
cdef bytes LINESEP = b'\n'
@@ -54,7 +54,7 @@ def _read_output(Session session, read_func, timeout=None):
5454
_wait_select(_sock, _session, timeout)
5555
_size, _data = read_func()
5656
if timeout is not None and _size == LIBSSH2_ERROR_EAGAIN:
57-
break
57+
raise Timeout
5858
while _size > 0:
5959
while _pos < _size:
6060
linesep = _data[:_size].find(LINESEP, _pos)

tests/test_pssh_ssh2_client.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,14 +1193,43 @@ def test_join_timeout_set_no_timeout(self):
11931193
def test_read_timeout(self):
11941194
client = ParallelSSHClient([self.host], port=self.port,
11951195
pkey=self.user_key)
1196-
output = client.run_command('sleep 2', timeout=1)
1197-
stdout = list(output[self.host].stdout)
1196+
output = client.run_command('sleep 2; echo me', timeout=1)
1197+
for host, host_out in output.items():
1198+
self.assertRaises(Timeout, list, host_out.stdout)
11981199
self.assertFalse(output[self.host].channel.eof())
1199-
self.assertEqual(len(stdout), 0)
1200-
list(output[self.host].stdout)
1201-
list(output[self.host].stdout)
12021200
client.join(output)
1203-
self.assertTrue(output[self.host].channel.eof())
1201+
for host, host_out in output.items():
1202+
stdout_buf = client.host_clients[self.host].read_output_buffer(
1203+
client.host_clients[self.host].read_output(
1204+
output[self.host].channel, timeout=1))
1205+
host_out.stdout = stdout_buf
1206+
stdout = list(output[self.host].stdout)
1207+
self.assertEqual(len(stdout), 1)
1208+
1209+
def test_timeout_file_read(self):
1210+
dir_name = os.path.dirname(__file__)
1211+
_file = os.sep.join((dir_name, 'file_to_read'))
1212+
contents = [b'a line\n' for _ in range(50)]
1213+
with open(_file, 'wb') as fh:
1214+
fh.writelines(contents)
1215+
try:
1216+
output = self.client.run_command('tail -f %s' % (_file,),
1217+
use_pty=True,
1218+
timeout=1)
1219+
self.assertRaises(Timeout, self.client.join, output, timeout=1)
1220+
for host, host_out in output.items():
1221+
try:
1222+
for line in host_out.stdout:
1223+
pass
1224+
except Timeout:
1225+
pass
1226+
else:
1227+
raise Exception("Timeout should have been raised")
1228+
channel = output[self.host].channel
1229+
self.client.host_clients[self.host].close_channel(channel)
1230+
self.client.join(output)
1231+
finally:
1232+
os.unlink(_file)
12041233

12051234
## OpenSSHServer needs to run in its own thread for this test to work
12061235
## Race conditions otherwise.

0 commit comments

Comments
 (0)