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.vizza...@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepa...@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

diff --git a/dts/framework/remote_session/dpdk_app.py 
b/dts/framework/remote_session/dpdk_app.py
new file mode 100644
index 0000000000..c9945f302d
--- /dev/null
+++ b/dts/framework/remote_session/dpdk_app.py
@@ -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()
diff --git a/dts/framework/remote_session/dpdk_shell.py 
b/dts/framework/remote_session/dpdk_shell.py
index 0962414876..f7ea2588ca 100644
--- a/dts/framework/remote_session/dpdk_shell.py
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -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`.
diff --git a/dts/framework/remote_session/single_active_interactive_shell.py 
b/dts/framework/remote_session/single_active_interactive_shell.py
index c1369ef77e..2257b6156b 100644
--- a/dts/framework/remote_session/single_active_interactive_shell.py
+++ b/dts/framework/remote_session/single_active_interactive_shell.py
@@ -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(
diff --git a/dts/framework/remote_session/testpmd_shell.py 
b/dts/framework/remote_session/testpmd_shell.py
index 1f291fcb68..db1bfaa9d1 100644
--- a/dts/framework/remote_session/testpmd_shell.py
+++ b/dts/framework/remote_session/testpmd_shell.py
@@ -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
 
-- 
2.43.0

Reply via email to