aboutsummaryrefslogtreecommitdiff
path: root/lava_dispatcher/shell.py
diff options
context:
space:
mode:
authorRĂ©mi Duraffort <remi.duraffort@linaro.org>2017-09-12 14:46:34 +0200
committerNeil Williams <neil.williams@linaro.org>2017-10-25 10:37:31 +0000
commitddf5d2fc956f3b261635cb9487a6bdf6f6cca1c7 (patch)
tree6c13da8c8f8b0946d2e493b3773efafbdd4e11e6 /lava_dispatcher/shell.py
parent7dbdc2b65b039027540b77e986f62910d5620bc6 (diff)
Remove v1 code
Change-Id: I098edd9edfeb968b303eaedc6afb6654e43b4b98
Diffstat (limited to 'lava_dispatcher/shell.py')
-rw-r--r--lava_dispatcher/shell.py304
1 files changed, 304 insertions, 0 deletions
diff --git a/lava_dispatcher/shell.py b/lava_dispatcher/shell.py
new file mode 100644
index 000000000..7f35af49b
--- /dev/null
+++ b/lava_dispatcher/shell.py
@@ -0,0 +1,304 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@linaro.org>
+#
+# This file is part of LAVA Dispatcher.
+#
+# LAVA Dispatcher is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# LAVA Dispatcher is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along
+# with this program; if not, see <http://www.gnu.org/licenses>.
+
+import contextlib
+import logging
+import pexpect
+import sys
+import time
+from lava_dispatcher.action import (
+ Action,
+ InfrastructureError,
+ JobError,
+ LAVABug,
+ TestError,
+ Timeout,
+)
+from lava_dispatcher.connection import Connection
+from lava_dispatcher.utils.constants import LINE_SEPARATOR
+from lava_dispatcher.utils.strings import seconds_to_str
+
+
+class ShellLogger(object):
+ """
+ Builds a YAML log message out of the incremental output of the pexpect.spawn
+ using the logfile support built into pexpect.
+ """
+
+ def __init__(self, logger):
+ self.line = ''
+ self.logger = logger
+ self.is_feedback = False
+
+ def write(self, new_line):
+ replacements = {
+ '\n\n': '\n', # double lines to single
+ '\r': '',
+ '"': '\\\"', # escape double quotes for YAML syntax
+ '\x1b': '' # remove escape control characters
+ }
+ for key, value in replacements.items():
+ new_line = new_line.replace(key, value)
+ lines = self.line + new_line
+
+ # Print one full line at a time. A partial line is kept in memory.
+ if '\n' in lines:
+ last_ret = lines.rindex('\n')
+ self.line = lines[last_ret + 1:]
+ lines = lines[:last_ret]
+ for line in lines.split('\n'):
+ if self.is_feedback:
+ self.logger.feedback(line)
+ else:
+ self.logger.target(line)
+ else:
+ self.line = lines
+ return
+
+ def flush(self): # pylint: disable=no-self-use
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+
+class ShellCommand(pexpect.spawn): # pylint: disable=too-many-public-methods
+ """
+ Run a command over a connection using pexpect instead of
+ subprocess, i.e. not on the dispatcher itself.
+ Takes a Timeout object (to support overrides and logging)
+
+ A ShellCommand is a raw_connection for a ShellConnection instance.
+ """
+
+ def __init__(self, command, lava_timeout, logger=None, cwd=None):
+ if not lava_timeout or not isinstance(lava_timeout, Timeout):
+ raise LAVABug("ShellCommand needs a timeout set by the calling Action")
+ if not logger:
+ raise LAVABug("ShellCommand needs a logger")
+ if sys.version_info[0] == 2:
+ pexpect.spawn.__init__(
+ self, command,
+ timeout=lava_timeout.duration,
+ cwd=cwd,
+ logfile=ShellLogger(logger),
+ )
+ elif sys.version_info[0] == 3:
+ pexpect.spawn.__init__(
+ self, command,
+ timeout=lava_timeout.duration,
+ cwd=cwd,
+ logfile=ShellLogger(logger),
+ encoding='utf-8',
+ )
+ self.name = "ShellCommand"
+ self.logger = logger
+ # set a default newline character, but allow actions to override as neccessary
+ self.linesep = LINE_SEPARATOR
+ self.lava_timeout = lava_timeout
+
+ def sendline(self, s='', delay=0): # pylint: disable=arguments-differ
+ """
+ Extends pexpect.sendline so that it can support the delay argument which allows a delay
+ between sending each character to get around slow serial problems (iPXE).
+ pexpect sendline does exactly the same thing: calls send for the string then os.linesep.
+
+ :param s: string to send
+ :param delay: delay in milliseconds between sending each character
+ """
+ send_char = False
+ if delay > 0:
+ self.logger.debug("Sending with %s millisecond of delay", delay)
+ send_char = True
+ self.logger.input(s + self.linesep)
+ self.send(s, delay, send_char)
+ self.send(self.linesep, delay)
+
+ def sendcontrol(self, char):
+ self.logger.input(char)
+ return super(ShellCommand, self).sendcontrol(char)
+
+ def send(self, string, delay=0, send_char=True): # pylint: disable=arguments-differ
+ """
+ Extends pexpect.send to support extra arguments, delay and send by character flags.
+ """
+ sent = 0
+ if not string:
+ return sent
+ delay = float(delay) / 1000
+ if send_char:
+ for char in string:
+ sent += super(ShellCommand, self).send(char)
+ time.sleep(delay)
+ else:
+ sent = super(ShellCommand, self).send(string)
+ return sent
+
+ def expect(self, *args, **kw):
+ """
+ No point doing explicit logging here, the SignalDirector can help
+ the TestShellAction make much more useful reports of what was matched
+ """
+ try:
+ proc = super(ShellCommand, self).expect(*args, **kw)
+ except pexpect.TIMEOUT:
+ raise TestError("ShellCommand command timed out.")
+ except ValueError as exc:
+ raise TestError(exc)
+ except pexpect.EOF:
+ # FIXME: deliberately closing the connection (and starting a new one) needs to be supported.
+ raise InfrastructureError("Connection closed")
+ return proc
+
+ def empty_buffer(self):
+ """Make sure there is nothing in the pexpect buffer."""
+ index = 0
+ while index == 0:
+ index = self.expect(['.+', pexpect.EOF, pexpect.TIMEOUT], timeout=1)
+
+
+class ShellSession(Connection):
+
+ def __init__(self, job, shell_command):
+ """
+ A ShellSession monitors a pexpect connection.
+ Optionally, a prompt can be forced after
+ a percentage of the timeout.
+ """
+ super(ShellSession, self).__init__(job, shell_command)
+ self.name = "ShellSession"
+ # FIXME: rename __prompt_str__ to indicate it can be a list or str
+ self.__prompt_str__ = None
+ self.spawn = shell_command
+ self.__runner__ = None
+ self.timeout = shell_command.lava_timeout
+
+ def disconnect(self, reason):
+ # FIXME
+ pass
+
+ # FIXME: rename prompt_str to indicate it can be a list or str
+ @property
+ def prompt_str(self):
+ return self.__prompt_str__
+
+ @prompt_str.setter
+ def prompt_str(self, string):
+ # FIXME: Debug logging should show whenever this property is changed
+ self.__prompt_str__ = string
+
+ @contextlib.contextmanager
+ def test_connection(self):
+ """
+ Yields the actual connection which can be used to interact inside this shell.
+ """
+ yield self.raw_connection
+
+ def force_prompt_wait(self, remaining=None):
+ """
+ One of the challenges we face is that kernel log messages can appear
+ half way through a shell prompt. So, if things are taking a while,
+ we send a newline along to maybe provoke a new prompt. We wait for
+ half the timeout period and then wait for one tenth of the timeout
+ 6 times (so we wait for 1.1 times the timeout period overall).
+ :return: the index into the connection.prompt_str list
+ """
+ logger = logging.getLogger('dispatcher')
+ prompt_wait_count = 0
+ if not remaining:
+ return self.wait()
+ # connection_prompt_limit
+ partial_timeout = remaining / 2.0
+ while True:
+ try:
+ return self.raw_connection.expect(self.prompt_str, timeout=partial_timeout)
+ except (pexpect.TIMEOUT, TestError) as exc:
+ if prompt_wait_count < 6:
+ logger.warning(
+ '%s: Sending %s in case of corruption. Connection timeout %s, retry in %s',
+ exc, self.check_char, seconds_to_str(remaining), seconds_to_str(partial_timeout))
+ logger.debug("pattern: %s", self.prompt_str)
+ prompt_wait_count += 1
+ partial_timeout = remaining / 10
+ self.sendline(self.check_char)
+ continue
+ else:
+ # TODO: is someone expecting pexpect.TIMEOUT?
+ raise
+
+ def wait(self, max_end_time=None):
+ """
+ Simple wait without sendling blank lines as that causes the menu
+ to advance without data which can cause blank entries and can cause
+ the menu to exit to an unrecognised prompt.
+ """
+ if not max_end_time:
+ timeout = self.timeout.duration
+ else:
+ timeout = max_end_time - time.time()
+ if timeout < 0:
+ raise LAVABug("Invalid max_end_time value passed to wait()")
+ try:
+ return self.raw_connection.expect(self.prompt_str, timeout=timeout)
+ except (TestError, pexpect.TIMEOUT):
+ raise JobError("wait for prompt timed out")
+
+ def listen_feedback(self, timeout):
+ """
+ Listen to output and log as feedback
+ """
+ if timeout < 0:
+ raise LAVABug("Invalid timeout value passed to listen_feedback()")
+ try:
+ self.raw_connection.logfile.is_feedback = True
+ return self.raw_connection.expect([pexpect.EOF, pexpect.TIMEOUT],
+ timeout=timeout)
+ finally:
+ self.raw_connection.logfile.is_feedback = False
+
+
+class ExpectShellSession(Action):
+ """
+ Waits for a shell connection to the device for the current job.
+ The shell connection can be over any particular connection,
+ all that is needed is a prompt.
+ """
+ compatibility = 2
+
+ def __init__(self):
+ super(ExpectShellSession, self).__init__()
+ self.name = "expect-shell-connection"
+ self.summary = "Expect a shell prompt"
+ self.description = "Wait for a shell"
+ self.force_prompt = True
+
+ def validate(self):
+ super(ExpectShellSession, self).validate()
+ if 'prompts' not in self.parameters:
+ self.errors = "Unable to identify test image prompts from parameters."
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(ExpectShellSession, self).run(connection, max_end_time, args)
+ if not connection:
+ raise JobError("No connection available.")
+ if not connection.prompt_str:
+ self.logger.debug("Setting default test shell prompt")
+ connection.prompt_str = self.parameters['prompts']
+ connection.timeout = self.connection_timeout
+ self.wait(connection)
+ return connection