From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 6173F440E3; Wed, 29 May 2024 21:49:35 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 368B84068A; Wed, 29 May 2024 21:49:34 +0200 (CEST) Received: from mail-ot1-f99.google.com (mail-ot1-f99.google.com [209.85.210.99]) by mails.dpdk.org (Postfix) with ESMTP id A6850402F1 for ; Wed, 29 May 2024 21:49:29 +0200 (CEST) Received: by mail-ot1-f99.google.com with SMTP id 46e09a7af769-6f120ae1ca3so100051a34.1 for ; Wed, 29 May 2024 12:49:29 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1717012169; x=1717616969; darn=dpdk.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=AwC+vGXBV22hFKrlBdKNJE9FM6A18WpuOreuWKdYlHk=; b=gXYZHlT4GYmbt5o3Dm5O/MXp4q2btxnq09Lx7ruTrlB9EUe+PHCRWuDf9ZrAIGXta5 od22tPqDessu2KeWnvS9onQcjWuxm5gYmyfx2dkwyYF1ML6dIiMHzxK+5nC3soX6TumA Y9bqjKVUGpiPqzSpY6bZdX1hIjVDNVZVFAMGo= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1717012169; x=1717616969; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=AwC+vGXBV22hFKrlBdKNJE9FM6A18WpuOreuWKdYlHk=; b=TMBWl17KvTGHB/zL7h1cbrtFMpVQDE0+SyCCJaxhvro2w5kEyPiHLAbUzMl7qK0/xb RJTT0rtTOfvo1xoqgSEozNOWK5MY2ka0xqjkEmfMfnIGirT84mmG7gnzkGVifAdv4Aoj 5MMOws4fPF3nT6Pt2LKVZKovyIXXKVA/zW+r4NlfLRSc9W5lPxNIhqOMtYrUq+6m9kd4 ibrFJYr6bOqYnFJdXq1DKJDh5WQa0NvxviFPWE2PXSDfNO8gVLoy19fEz14Wym4N/n3R GizwqdnLEIQnNFiDrvPNz4FiLVCjfeiwJPfkS9t6GDIDOM5jk+C3wrrLXCHnVKCXD+Fp LGAw== X-Gm-Message-State: AOJu0Yy7amnfZaIwT8tnbe2hVl5xm0kLWRTpkw2cEroOpoObLQ5pKogT IpHCSjQf2aWbT2hGe2WzvJ+FWiudvdaha5dkpr6/HXLRzRb4aKMwZCn1rnWJjxN7il3BNVO/bTB +RCcgW87Bx4UCF2NPLFGkwZ9BP7/z+Dk33fm4h7/uiUyVl53o X-Google-Smtp-Source: AGHT+IFQej+p1RbHmt0nCQ2vS3TaOX7+THxUUyAzVlscXA1hPzMpxWGNI8OLTT9ElQL+jXrcGte8YPWXmpta X-Received: by 2002:a05:6830:1e23:b0:6f1:33da:3cec with SMTP id 46e09a7af769-6f90af0fc9amr157703a34.29.1717012168920; Wed, 29 May 2024 12:49:28 -0700 (PDT) Received: from postal.iol.unh.edu (postal.iol.unh.edu. [2606:4100:3880:1234::84]) by smtp-relay.gmail.com with ESMTPS id d75a77b69052e-43fb17c877fsm8468201cf.8.2024.05.29.12.49.28 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 29 May 2024 12:49:28 -0700 (PDT) X-Relaying-Domain: iol.unh.edu Received: from iol.unh.edu (unknown [IPv6:2606:4100:3880:1257::1083]) by postal.iol.unh.edu (Postfix) with ESMTP id 266E5605C373; Wed, 29 May 2024 15:49:28 -0400 (EDT) From: jspewock@iol.unh.edu To: paul.szczepanek@arm.com, wathsala.vithanage@arm.com, probb@iol.unh.edu, npratte@iol.unh.edu, Luca.Vizzarro@arm.com, thomas@monjalon.net, juraj.linkes@pantheon.tech, Honnappa.Nagarahalli@arm.com, yoan.picchi@foss.arm.com Cc: dev@dpdk.org, Jeremy Spewock Subject: [PATCH v3 1/3] dts: Improve output gathering in interactive shells Date: Wed, 29 May 2024 15:49:08 -0400 Message-ID: <20240529194910.26803-2-jspewock@iol.unh.edu> X-Mailer: git-send-email 2.45.1 In-Reply-To: <20240529194910.26803-1-jspewock@iol.unh.edu> References: <20240501161623.26672-1-jspewock@iol.unh.edu> <20240529194910.26803-1-jspewock@iol.unh.edu> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org From: Jeremy Spewock The current implementation of consuming output from interactive shells relies on being able to find an expected prompt somewhere within the output buffer after sending the command. This is useful in situations where the prompt does not appear in the output itself, but in some practical cases (such as the starting of an XML-RPC server for scapy) the prompt exists in one of the commands sent to the shell and this can cause the command to exit early and creates a race condition between the server starting and the first command being sent to the server. This patch addresses this problem by searching for a line that strictly ends with the provided prompt, rather than one that simply contains it, so that the detection that a command is finished is more consistent. It also adds a catch to detect when a command times out before finding the prompt or the underlying SSH session dies so that the exception can be wrapped into a more explicit one and be more consistent with the non-interactive shells. Bugzilla ID: 1359 Fixes: 88489c0501af ("dts: add smoke tests") Signed-off-by: Jeremy Spewock --- dts/framework/exception.py | 66 ++++++++++++------- .../remote_session/interactive_shell.py | 51 ++++++++++---- 2 files changed, 81 insertions(+), 36 deletions(-) diff --git a/dts/framework/exception.py b/dts/framework/exception.py index cce1e0231a..627190c781 100644 --- a/dts/framework/exception.py +++ b/dts/framework/exception.py @@ -48,26 +48,6 @@ class DTSError(Exception): severity: ClassVar[ErrorSeverity] = ErrorSeverity.GENERIC_ERR -class SSHTimeoutError(DTSError): - """The SSH execution of a command timed out.""" - - #: - severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR - _command: str - - def __init__(self, command: str): - """Define the meaning of the first argument. - - Args: - command: The executed command. - """ - self._command = command - - def __str__(self) -> str: - """Add some context to the string representation.""" - return f"{self._command} execution timed out." - - class SSHConnectionError(DTSError): """An unsuccessful SSH connection.""" @@ -95,8 +75,42 @@ def __str__(self) -> str: return message -class SSHSessionDeadError(DTSError): - """The SSH session is no longer alive.""" +class _SSHTimeoutError(DTSError): + """The execution of a command via SSH timed out. + + This class is private and meant to be raised as its interactive and non-interactive variants. + """ + + #: + severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR + _command: str + + def __init__(self, command: str): + """Define the meaning of the first argument. + + Args: + command: The executed command. + """ + self._command = command + + def __str__(self) -> str: + """Add some context to the string representation.""" + return f"{self._command} execution timed out." + + +class SSHTimeoutError(_SSHTimeoutError): + """The execution of a command on a non-interactive SSH session timed out.""" + + +class InteractiveSSHTimeoutError(_SSHTimeoutError): + """The execution of a command on an interactive SSH session timed out.""" + + +class _SSHSessionDeadError(DTSError): + """The SSH session is no longer alive. + + This class is private and meant to be raised as its interactive and non-interactive variants. + """ #: severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR @@ -115,6 +129,14 @@ def __str__(self) -> str: return f"SSH session with {self._host} has died." +class SSHSessionDeadError(_SSHSessionDeadError): + """Non-interactive SSH session has died.""" + + +class InteractiveSSHSessionDeadError(_SSHSessionDeadError): + """Interactive SSH session as died.""" + + class ConfigurationError(DTSError): """An invalid configuration.""" diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py index 5cfe202e15..148907f645 100644 --- a/dts/framework/remote_session/interactive_shell.py +++ b/dts/framework/remote_session/interactive_shell.py @@ -18,11 +18,17 @@ from pathlib import PurePath from typing import Callable, ClassVar -from paramiko import Channel, SSHClient, channel # type: ignore[import] +from paramiko import Channel, channel # type: ignore[import] +from framework.exception import ( + InteractiveSSHSessionDeadError, + InteractiveSSHTimeoutError, +) from framework.logger import DTSLogger from framework.settings import SETTINGS +from .interactive_remote_session import InteractiveRemoteSession + class InteractiveShell(ABC): """The base class for managing interactive shells. @@ -34,7 +40,7 @@ class InteractiveShell(ABC): session. """ - _interactive_session: SSHClient + _interactive_session: InteractiveRemoteSession _stdin: channel.ChannelStdinFile _stdout: channel.ChannelFile _ssh_channel: Channel @@ -48,7 +54,10 @@ class InteractiveShell(ABC): #: Extra characters to add to the end of every command #: before sending them. This is often overridden by subclasses and is - #: most commonly an additional newline character. + #: most commonly an additional newline character. This additional newline + #: character is used to force the line that is currently awaiting input + #: into the stdout buffer so that it can be consumed and checked against + #: the expected prompt. _command_extra_chars: ClassVar[str] = "" #: Path to the executable to start the interactive application. @@ -60,7 +69,7 @@ class InteractiveShell(ABC): def __init__( self, - interactive_session: SSHClient, + interactive_session: InteractiveRemoteSession, logger: DTSLogger, get_privileged_command: Callable[[str], str] | None, app_args: str = "", @@ -80,7 +89,7 @@ def __init__( and no output is gathered within the timeout, an exception is thrown. """ self._interactive_session = interactive_session - self._ssh_channel = self._interactive_session.invoke_shell() + self._ssh_channel = self._interactive_session.session.invoke_shell() self._stdin = self._ssh_channel.makefile_stdin("w") self._stdout = self._ssh_channel.makefile("r") self._ssh_channel.settimeout(timeout) @@ -124,20 +133,34 @@ def send_command(self, command: str, prompt: str | None = None) -> str: Returns: All output in the buffer before expected string. + + Raises: + InteractiveSSHSessionDeadError: The session died while executing the command. + InteractiveSSHTimeoutError: If command was sent but prompt could not be found in + the output before the timeout. """ self._logger.info(f"Sending: '{command}'") if prompt is None: prompt = self._default_prompt - self._stdin.write(f"{command}{self._command_extra_chars}\n") - self._stdin.flush() out: str = "" - for line in self._stdout: - out += line - if prompt in line and not line.rstrip().endswith( - command.rstrip() - ): # ignore line that sent command - break - self._logger.debug(f"Got output: {out}") + try: + self._stdin.write(f"{command}{self._command_extra_chars}\n") + self._stdin.flush() + for line in self._stdout: + out += line + if line.rstrip().endswith(prompt): + break + except TimeoutError as e: + self._logger.exception(e) + self._logger.debug( + f"Prompt ({prompt}) was not found in output from command before timeout." + ) + raise InteractiveSSHTimeoutError(command) from e + except OSError as e: + self._logger.exception(e) + raise InteractiveSSHSessionDeadError(self._interactive_session.hostname) from e + finally: + self._logger.debug(f"Got output: {out}") return out def close(self) -> None: -- 2.45.1