Skip to content

Commit 10fd9d7

Browse files
committed
Improve output_if_timeout handling
This fixes two issues with the implementation of output_if_timeout: 1. The raw byte data was sent to the logger. In order to improve improve readability, the output is now decoded and stdout and stderr is separated by a header/footer. 2. The parameter handled only the timeout generated by the library, not the timeout generated by robot. Now both timeouts write the output to the logger, if output_if_timeout=True Additionally for paramiko, a last read of the channel is done, to fetch as much of the output available as possible.
1 parent ba6c3ee commit 10fd9d7

2 files changed

Lines changed: 83 additions & 34 deletions

File tree

src/SSHLibrary/abstractclient.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@
2323
import ntpath
2424
import fnmatch
2525

26+
from robot.errors import TimeoutError
27+
2628
from .config import (Configuration, IntegerEntry, NewlineEntry, StringEntry,
2729
TimeEntry)
2830
from .logger import logger
29-
from .utils import is_bytes, is_string, unicode, is_list_like
30-
31+
from .utils import is_bytes, is_string, unicode, is_list_like, is_truthy
3132

3233
class SSHClientException(RuntimeError):
3334
pass
3435

36+
class SSHClientTimeoutException(SSHClientException):
37+
pass
3538

3639
class _ClientConfiguration(Configuration):
3740

@@ -381,11 +384,24 @@ def read_command_output(self, timeout=None, output_during_execution=False, outpu
381384
"""
382385
if timeout:
383386
timeout = float(TimeEntry(timeout).value)
384-
try:
385-
return self._started_commands.pop().read_outputs(timeout, output_during_execution, output_if_timeout)
386-
except IndexError:
387+
388+
if not self._started_commands:
387389
raise SSHClientException('No started commands to read output from.')
388390

391+
command = self._started_commands.pop()
392+
try:
393+
return command.read_outputs(timeout, output_during_execution, output_if_timeout)
394+
except (TimeoutError, SSHClientTimeoutException):
395+
if is_truthy(output_if_timeout):
396+
try:
397+
stdout, stderr = command.read_unfinished_outputs()
398+
if stdout or stderr:
399+
logger.info("---- Stdout ----\n" + stdout + ("-" * 16) + "\n")
400+
logger.info("---- Stderr ----\n" + stderr + ("-" * 16) + "\n")
401+
except NotImplementedError:
402+
pass
403+
raise
404+
389405
def write(self, text, add_newline=False):
390406
"""Writes `text` in the current shell.
391407
@@ -1351,6 +1367,15 @@ def read_outputs(self):
13511367
"""
13521368
raise NotImplementedError
13531369

1370+
def read_unfinished_outputs(self):
1371+
"""Returns the output of the last command, that hasn't been read.
1372+
This can be used, when the execution is aborted by a test timeout.
1373+
1374+
:returns: A 2-tuple (stdout, stderr) with values
1375+
`stdout` and `stderr` as strings.
1376+
"""
1377+
return NotImplementedError
1378+
13541379

13551380
class SFTPFileInfo(object):
13561381
"""Wrapper class for the language specific file information objects.

src/SSHLibrary/pythonclient.py

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535

3636
from .abstractclient import (AbstractShell, AbstractSFTPClient,
3737
AbstractSSHClient, AbstractCommand,
38-
SSHClientException, SFTPFileInfo)
38+
SSHClientException, SSHClientTimeoutException,
39+
SFTPFileInfo)
3940
from .pythonforward import LocalPortForwarding
4041
from .utils import is_bytes, is_list_like, is_unicode, is_truthy
4142
from robot.api import logger
@@ -431,55 +432,78 @@ def _put_file(self, source, destination, mode, newline, path_separator, scp_pres
431432
def _get_file(self, remote_path, local_path, scp_preserve_times=False):
432433
self._scp_client.get(remote_path, local_path, preserve_times=is_truthy(scp_preserve_times))
433434

435+
class OutputBuffers():
436+
def __init__(self, shell):
437+
self._shell = shell
438+
self._stdout_filebuffer = shell.makefile('rb', -1)
439+
self._stderr_filebuffer = shell.makefile_stderr('rb', -1)
440+
self._stdouts = []
441+
self._stderrs = []
442+
443+
def read(self, do_logging=False):
444+
if self._shell.recv_ready():
445+
stdout_output = self._stdout_filebuffer.read(len(self._shell.in_buffer))
446+
if do_logging:
447+
logger.console(stdout_output)
448+
self._stdouts.append(stdout_output)
449+
if self._shell.recv_stderr_ready():
450+
stderr_output = self._stderr_filebuffer.read(len(self._shell.in_stderr_buffer))
451+
if do_logging:
452+
logger.console(stderr_output)
453+
self._stderrs.append(stderr_output)
454+
455+
def serialize(self, encoding):
456+
stdout = (b''.join(self._stdouts) + self._stdout_filebuffer.read()).decode(encoding)
457+
stderr = (b''.join(self._stderrs) + self._stderr_filebuffer.read()).decode(encoding)
458+
return stdout, stderr
434459

435460
class RemoteCommand(AbstractCommand):
461+
_buffers = None
436462

437463
def read_outputs(self, timeout=None, output_during_execution=False, output_if_timeout=False):
438464
stderr, stdout = self._receive_stdout_and_stderr(timeout, output_during_execution, output_if_timeout)
439465
rc = self._shell.recv_exit_status()
440466
self._shell.close()
441467
return stdout, stderr, rc
442468

469+
def read_unfinished_outputs(self):
470+
if self._buffers is None:
471+
return None, None
472+
if not self._shell.closed:
473+
self._buffers.read() # Get remaining unread output from channel
474+
475+
stdout, stderr = self._buffers.serialize(self._encoding)
476+
self._cleanup_buffers()
477+
478+
return stdout, stderr
479+
480+
def _cleanup_buffers(self):
481+
self._buffers = None
482+
443483
def _receive_stdout_and_stderr(self, timeout=None, output_during_execution=False, output_if_timeout=False):
444-
stdout_filebuffer = self._shell.makefile('rb', -1)
445-
stderr_filebuffer = self._shell.makefile_stderr('rb', -1)
446-
stdouts = []
447-
stderrs = []
484+
self._buffers = OutputBuffers(self._shell)
448485
while self._shell_open():
449-
self._flush_stdout_and_stderr(stderr_filebuffer, stderrs, stdout_filebuffer, stdouts, timeout,
450-
output_during_execution, output_if_timeout)
486+
self._flush_stdout_and_stderr(timeout, output_during_execution, output_if_timeout)
451487
time.sleep(0.01) # lets not be so busy
452-
stdout = (b''.join(stdouts) + stdout_filebuffer.read()).decode(self._encoding)
453-
stderr = (b''.join(stderrs) + stderr_filebuffer.read()).decode(self._encoding)
488+
489+
stdout, stderr = self._buffers.serialize(self._encoding)
490+
491+
self._buffers = None
492+
454493
return stderr, stdout
455494

456-
def _flush_stdout_and_stderr(self, stderr_filebuffer, stderrs, stdout_filebuffer, stdouts, timeout=None,
457-
output_during_execution=False, output_if_timeout=False):
495+
def _flush_stdout_and_stderr(self, timeout=None, output_during_execution=False, output_if_timeout=False):
496+
do_logging = is_truthy(output_during_execution)
458497
if timeout:
459498
end_time = time.time() + timeout
460499
while time.time() < end_time:
461500
if self._shell.status_event.wait(0):
462501
break
463-
self._output_logging(stderr_filebuffer, stderrs, stdout_filebuffer, stdouts, output_during_execution)
502+
self._buffers.read(do_logging)
464503
if not self._shell.status_event.isSet():
465-
if is_truthy(output_if_timeout):
466-
logger.info(stdouts)
467-
logger.info(stderrs)
468-
raise SSHClientException('Timed out in %s seconds' % int(timeout))
504+
raise SSHClientTimeoutException('Timed out in %s seconds' % int(timeout))
469505
else:
470-
self._output_logging(stderr_filebuffer, stderrs, stdout_filebuffer, stdouts, output_during_execution)
471-
472-
def _output_logging(self, stderr_filebuffer, stderrs, stdout_filebuffer, stdouts, output_during_execution=False):
473-
if self._shell.recv_ready():
474-
stdout_output = stdout_filebuffer.read(len(self._shell.in_buffer))
475-
if is_truthy(output_during_execution):
476-
logger.console(stdout_output)
477-
stdouts.append(stdout_output)
478-
if self._shell.recv_stderr_ready():
479-
stderr_output = stderr_filebuffer.read(len(self._shell.in_stderr_buffer))
480-
if is_truthy(output_during_execution):
481-
logger.console(stderr_output)
482-
stderrs.append(stderr_output)
506+
self._buffers.read(do_logging)
483507

484508
def _shell_open(self):
485509
return not (self._shell.closed or

0 commit comments

Comments
 (0)