Add BlockingDPDKApp class. Some non-interactive applications are
blocking and run until the user interrupts them. As their main intended
usage is to be kept running in the background, this class exploits
InteractiveShell to spawn a dedicated shell to keep the blocking
application running, while detaching from it.
This class works by providing the `wait_until_ready` and `close`
methods. The former starts up the application and returns only when the
application readiness output ends in the string provided as an argument
to the same method. Whereas the latter works by simulating a Ctrl+C
keystroke, therefore sending a SIGINT to the app.
Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
dts/framework/remote_session/dpdk_app.py | 73 +++++++++++++++++++
dts/framework/remote_session/dpdk_shell.py | 3 +-
.../single_active_interactive_shell.py | 12 ++-
dts/framework/remote_session/testpmd_shell.py | 2 +-
4 files changed, 85 insertions(+), 5 deletions(-)
create mode 100644 dts/framework/remote_session/dpdk_app.py
new file mode 100644
@@ -0,0 +1,73 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2025 Arm Limited
+
+"""Class to run blocking DPDK apps in the background.
+
+The class won't automatically start the app. The start-up is done as part of the
+:meth:`BlockingDPDKApp.wait_until_ready` method, which will return execution to the caller only
+when the desired stdout has been returned by the app. Usually this is used to detect when the app
+has been loaded and ready to be used.
+
+Example:
+ ..code:: python
+
+ pdump = BlockingDPDKApp(
+ PurePath("app/dpdk-pdump"),
+ app_params="--pdump 'port=0,queue=*,rx-dev=/tmp/rx-dev.pcap'"
+ )
+ pdump.wait_until_ready("65535") # start app
+
+ # pdump is now ready to capture
+
+ pdump.close() # stop/close app
+"""
+
+from pathlib import PurePath
+
+from framework.params.eal import EalParams
+from framework.remote_session.dpdk_shell import DPDKShell
+
+
+class BlockingDPDKApp(DPDKShell):
+ """Class to manage blocking DPDK apps."""
+
+ def __init__(
+ self,
+ path: PurePath,
+ name: str | None = None,
+ privileged: bool = True,
+ app_params: EalParams | str = "",
+ ) -> None:
+ """Constructor.
+
+ Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`.
+
+ Args:
+ path: Path relative to the DPDK build to the executable.
+ name: Name to identify this application.
+ privileged: Run as privileged user.
+ app_params: The application parameters. If a string or an incomplete :class:`EalParams`
+ object are passed, the EAL params are computed based on the current context.
+ """
+ if isinstance(app_params, str):
+ eal_params = EalParams()
+ eal_params.append_str(app_params)
+ app_params = eal_params
+
+ super().__init__(name, privileged, path, app_params)
+
+ def wait_until_ready(self, end_token: str) -> None:
+ """Start app and wait until ready.
+
+ Args:
+ end_token: The string at the end of a line that indicates the app is ready.
+ """
+ self._start_application(end_token)
+
+ def close(self) -> None:
+ """Close the application.
+
+ Sends a SIGINT to close the application.
+ """
+ self.send_command("\x03")
+ self._close()
@@ -65,13 +65,14 @@ def __init__(
self,
name: str | None = None,
privileged: bool = True,
+ path: PurePath | None = None,
app_params: EalParams = EalParams(),
) -> None:
"""Extends :meth:`~.interactive_shell.InteractiveShell.__init__`."""
app_params = compute_eal_params(app_params)
node = get_ctx().sut_node
- super().__init__(node, name, privileged, app_params)
+ super().__init__(node, name, privileged, path, app_params)
def _update_real_path(self, path: PurePath) -> None:
"""Extends :meth:`~.interactive_shell.InteractiveShell._update_real_path`.
@@ -92,6 +92,7 @@ def __init__(
node: Node,
name: str | None = None,
privileged: bool = False,
+ path: PurePath | None = None,
app_params: Params = Params(),
**kwargs,
) -> None:
@@ -105,6 +106,7 @@ def __init__(
name: Name for the interactive shell to use for logging. This name will be appended to
the name of the underlying node which it is running on.
privileged: Enables the shell to run as superuser.
+ path: Path to the executable. If :data:`None`, then the class' path attribute is used.
app_params: The command line parameters to be passed to the application on startup.
**kwargs: Any additional arguments if any.
"""
@@ -116,7 +118,7 @@ def __init__(
self._privileged = privileged
self._timeout = SETTINGS.timeout
# Ensure path is properly formatted for the host
- self._update_real_path(self.path)
+ self._update_real_path(path or self.path)
super().__init__(**kwargs)
def _setup_ssh_channel(self):
@@ -133,7 +135,7 @@ def _make_start_command(self) -> str:
start_command = self._node.main_session._get_privileged_command(start_command)
return start_command
- def _start_application(self) -> None:
+ def _start_application(self, prompt: str | None = None) -> None:
"""Starts a new interactive application based on the path to the app.
This method is often overridden by subclasses as their process for starting may look
@@ -141,6 +143,10 @@ def _start_application(self) -> None:
`self._init_attempts` - 1 times. This is done because some DPDK applications need slightly
more time after exiting their script to clean up EAL before others can start.
+ Args:
+ prompt: When starting up the application, expect this string at the end of stdout when
+ the application is ready. If :data:`None`, the class' default prompt will be used.
+
Raises:
InteractiveCommandExecutionError: If the application fails to start within the allotted
number of retries.
@@ -151,7 +157,7 @@ def _start_application(self) -> None:
self.is_alive = True
for attempt in range(self._init_attempts):
try:
- self.send_command(start_command)
+ self.send_command(start_command, prompt)
break
except InteractiveSSHTimeoutError:
self._logger.info(
@@ -1540,7 +1540,7 @@ def __init__(
"""Overrides :meth:`~.dpdk_shell.DPDKShell.__init__`. Changes app_params to kwargs."""
if "port_topology" not in app_params and get_ctx().topology.type is TopologyType.one_link:
app_params["port_topology"] = PortTopology.loop
- super().__init__(name, privileged, TestPmdParams(**app_params))
+ super().__init__(name, privileged, app_params=TestPmdParams(**app_params))
self.ports_started = not self._app_params.disable_device_start
self._ports = None