Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,32 @@ if code cannot establish connection, it will retry using deploy port (+1 for def

If code cannot establish connection, it will start deployment of python using [Deployment setup tool](#deployment-setup-tool) and establish connection again.

## RShell connection

`RShellConnection` is a connection type that leverages `httplib.client` library to establish and manage connections using RESTful API over HTTP protocol in EFI Shell environment. `rshell_client.py` must be present on the EFI Shell target system.

`rshell_server.py` is a server script that needs to be executed on the host machine to facilitate communication between the host and the EFI Shell target system. Server works on queue with address ip -> command to execute, it provides a RESTful API that allows the host to send commands and receive responses from the EFI Shell:

* `/execute_command` - Endpoint to execute commands on the EFI Shell target system:
Form fields:
* `timeout` - Timeout for command execution.
* `command` - Command to be executed.
* `ip` - IP address of the EFI Shell target system.
* `/post_result` - Endpoint to post results back to the host.
Headers fields:
* `CommandID` - Unique identifier for the command.
* `rc` - Return code of the executed command.
Body:
* Command output.
* `/exception` - Endpoint to handle exceptions that may occur during communication.
Headers fields:
* `CommandID` - Unique identifier for the command.
Body:
* Exception details.
* `/getCommandToExecute` - Endpoint to retrieve commands to be executed on the EFI Shell target system. Returns commandline with generated CommandID.
* `/health/<ip>` - Endpoint to check the health status of the connection.

`rshell.py` is a Connection class that calls RESTful API endpoints provided by `rshell_server.py` to execute commands on the EFI Shell target system. If required, starts `rshell_server.py` on the host machine.

## OS supported:
* LNX
Expand Down
11 changes: 11 additions & 0 deletions examples/rshell_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: MIT
import logging
logging.basicConfig(level=logging.DEBUG)
from mfd_connect.rshell import RShellConnection
Comment on lines +4 to +5
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import statement should appear before code execution. Move the import of RShellConnection to line 4, before the logging.basicConfig() call.

Suggested change
logging.basicConfig(level=logging.DEBUG)
from mfd_connect.rshell import RShellConnection
from mfd_connect.rshell import RShellConnection
logging.basicConfig(level=logging.DEBUG)

Copilot uses AI. Check for mistakes.

# LINUX
conn = RShellConnection(ip="10.10.10.10") # start and connect to rshell server
# conn = RShellConnection(ip="10.10.10.10", server_ip="10.10.10.11") # connect to rshell server
conn.execute_command("ls")
conn.disconnect(True)
233 changes: 233 additions & 0 deletions mfd_connect/rshell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: MIT
"""RShell Connection Class."""

import logging
import sys
import time
import typing
from ipaddress import IPv4Address, IPv6Address
from subprocess import CalledProcessError

import requests
from mfd_common_libs import add_logging_level, log_levels, TimeoutCounter
from mfd_typing.cpu_values import CPUArchitecture
from mfd_typing.os_values import OSBitness, OSName, OSType

from mfd_connect.local import LocalConnection
from mfd_connect.pathlib.path import CustomPath, custom_path_factory
from mfd_connect.process.base import RemoteProcess

from .base import Connection, ConnectionCompletedProcess

if typing.TYPE_CHECKING:
from pydantic import (
BaseModel, # from pytest_mfd_config.models.topology import ConnectionModel
)


logger = logging.getLogger(__name__)
add_logging_level(level_name="MODULE_DEBUG", level_value=log_levels.MODULE_DEBUG)
add_logging_level(level_name="CMD", level_value=log_levels.CMD)
add_logging_level(level_name="OUT", level_value=log_levels.OUT)


class RShellConnection(Connection):
"""RShell Connection Class."""

def __init__(
self,
ip: str | IPv4Address | IPv6Address,
server_ip: str | IPv4Address | IPv6Address | None = "127.0.0.1",
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default value '127.0.0.1' has type str but parameter accepts None. The logic at line 56 checks if server_ip which treats empty string as falsy. Consider using None as default and explicitly setting to '127.0.0.1' when None, or adjust the conditional to if server_ip is None.

Copilot uses AI. Check for mistakes.
model: "BaseModel | None" = None,
cache_system_data: bool = True,
connection_timeout: int = 60,
):
"""
Initialize RShellConnection.

:param ip: The IP address of the RShell server.
:param server_ip: The IP address of the server to connect to (optional).
:param model: The Pydantic model to use for the connection (optional).
:param cache_system_data: Whether to cache system data (default: True).
"""
super().__init__(model=model, cache_system_data=cache_system_data)
self._ip = ip
self.server_ip = server_ip if server_ip else "127.0.0.1"
self.server_process = None
if server_ip == "127.0.0.1":
# start Rshell server
self.server_process = self._run_server()
time.sleep(5)
timeout = TimeoutCounter(connection_timeout)
while not timeout:
logger.log(level=log_levels.MODULE_DEBUG, msg="Checking RShell server health")
status_code = requests.get(
f"http://{self.server_ip}/health/{self._ip}", proxies={"no_proxy": "*"}
).status_code
if status_code == 200:
logger.log(level=log_levels.MODULE_DEBUG, msg="RShell server is healthy")
break
time.sleep(5)
else:
raise TimeoutError("Connection of Client to RShell server timed out")

def disconnect(self, stop_client: bool = False) -> None:
"""
Disconnect connection.

Stop local RShell server if established.

:param stop_client: Whether to stop the RShell client (default: False).
"""
if stop_client:
logger.log(level=log_levels.MODULE_DEBUG, msg="Stopping RShell client")
self.execute_command("end")
if self.server_process:
logger.log(level=log_levels.MODULE_DEBUG, msg="Stopping RShell server")
self.server_process.kill()
logger.log(level=log_levels.MODULE_DEBUG, msg="RShell server stopped")
logger.log(level=log_levels.MODULE_DEBUG, msg=self.server_process.stdout_text)

def _run_server(self) -> RemoteProcess:
"""Run RShell server locally."""
conn = LocalConnection()
server_file = conn.path(__file__).parent / "rshell_server.py"
return conn.start_process(f"{conn.modules().sys.executable} {server_file}")

def execute_command(
self,
command: str,
*,
input_data: str | None = None,
cwd: str | None = None,
timeout: int | None = None,
env: dict | None = None,
stderr_to_stdout: bool = False,
discard_stdout: bool = False,
discard_stderr: bool = False,
skip_logging: bool = False,
expected_return_codes: list[int] | None = None,
shell: bool = False,
custom_exception: type[CalledProcessError] | None = None,
) -> ConnectionCompletedProcess:
"""
Execute a command on the remote server.

:param command: The command to execute.
:param timeout: The timeout for the command execution (optional).
:return: The result of the command execution.
"""
if input_data is not None:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Input data is not supported for RShellConnection and will be ignored.",
)

if cwd is not None:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="CWD is not supported for RShellConnection and will be ignored.",
)

if env is not None:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Environment variables are not supported for RShellConnection and will be ignored.",
)

if stderr_to_stdout:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Redirecting stderr to stdout is not supported for RShellConnection and will be ignored.",
)

if discard_stdout:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Discarding stdout is not supported for RShellConnection and will be ignored.",
)

if discard_stderr:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Discarding stderr is not supported for RShellConnection and will be ignored.",
)

if skip_logging:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Skipping logging is not supported for RShellConnection and will be ignored.",
)

if expected_return_codes is not None:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Expected return codes are not supported for RShellConnection and will be ignored.",
)

if shell:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Shell execution is not supported for RShellConnection and will be ignored.",
)

if custom_exception:
logger.log(
level=log_levels.MODULE_DEBUG,
msg="Custom exceptions are not supported for RShellConnection and will be ignored.",
)
timeout_string = f" with timeout {timeout} seconds" if timeout is not None else ""
logger.log(level=log_levels.CMD, msg=f"Executing >{self._ip}> '{command}',{timeout_string}")

response = requests.post(
f"http://{self.server_ip}/execute_command",
data={"command": command, "timeout": timeout, "ip": self._ip},
proxies={"no_proxy": "*"},
)
completed_process = ConnectionCompletedProcess(
args=command,
stdout=response.text,
return_code=int(response.headers.get("rc", -1)),
)
logger.log(
level=log_levels.MODULE_DEBUG,
msg=f"Finished executing '{command}', rc={completed_process.return_code}",
)
if skip_logging:
return completed_process

stdout = completed_process.stdout
if stdout:
logger.log(level=log_levels.OUT, msg=f"stdout>>\n{stdout}")

return completed_process

def path(self, *args, **kwargs) -> CustomPath:
"""Path represents a filesystem path."""
if sys.version_info >= (3, 12):
kwargs["owner"] = self
return custom_path_factory(*args, **kwargs)

return CustomPath(*args, owner=self, **kwargs)

def get_os_name(self) -> OSName: # noqa: D102
raise NotImplementedError

def get_os_type(self) -> OSType: # noqa: D102
raise NotImplementedError

def get_os_bitness(self) -> OSBitness: # noqa: D102
raise NotImplementedError

def get_cpu_architecture(self) -> CPUArchitecture: # noqa: D102
raise NotImplementedError

def restart_platform(self) -> None: # noqa: D102
raise NotImplementedError

def shutdown_platform(self) -> None: # noqa: D102
raise NotImplementedError

def wait_for_host(self, timeout: int = 60) -> None: # noqa: D102
raise NotImplementedError
119 changes: 119 additions & 0 deletions mfd_connect/rshell_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: MIT
"""
RShell Client Script.

Make sure that the Python UEFI interpreter is compiled with
Socket module support.
"""

__version__ = "1.0.0"

try:
import httplib as client
except ImportError:
from http import client
import sys
import os
import time

# get http server ip
http_server = sys.argv[1]
if len(sys.argv) > 2:
source_address = sys.argv[2]
else:
source_address = None

os_name = os.name


def _sleep(interval): # noqa: ANN001, ANN202
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The busy-wait implementation of _sleep will consume 100% CPU during sleep intervals. This is inefficient even in UEFI environments. Consider using a less aggressive polling interval or documenting this performance trade-off.

Copilot uses AI. Check for mistakes.
"""
Simulate the sleep function for EFI shell as the sleep API from time module is not working on EFI shell.

:param interval time period the system to be in idle
"""
start_ts = time.time()
while time.time() < start_ts + interval:
pass


time.sleep = _sleep


def _get_command(): # noqa: ANN202
"""Get the command from server to execute on client machine."""
# construct the list of tests by interacting with server
conn.request("GET", "getCommandToExecute")
rsp = conn.getresponse()
status = rsp.status
_id = rsp.getheader("CommandID")
if status == 204:
return None

print("Waiting for command from server: ")
data_received = rsp.read()
print(data_received)
test_list = data_received.split(b",")

return test_list[0], _id # return only the first command


while True:
# Connect to server
source_address_parameter = (source_address, 80) if source_address else None
conn = client.HTTPConnection(http_server, source_address=source_address_parameter)
# get the command from server
_command = _get_command()
if not _command:
conn.close()
time.sleep(5)
continue
cmd_str, _id = _command
cmd_str = cmd_str.decode("utf-8")
cmd_name = cmd_str.split(" ")[0]
if cmd_name == "end":
print("No more commands available to run")
conn.close()
exit(0)

print("Executing", cmd_str)

out = cmd_name + ".txt"
cmd = cmd_str + " > " + out

time.sleep(5)
rc = os.system(cmd) # execute command on machine
print("Executed the command")
time.sleep(5)

print("Posting the results to server")
# send response to server
try:
if os_name == "edk2":
encoding = "utf-16"
else:
encoding = "utf-8"

f = open(out, "r", encoding=encoding)

conn.request(
"POST",
"post_result",
body=f.read(),
headers={"Content-Type": "text/plain", "Connection": "keep-alive", "CommandID": _id, "rc": rc},
)
f.close()
os.system("del " + out)
except Exception as exp:
conn.request(
"POST",
"exception",
body=cmd + str(exp),
headers={"Content-Type": "text/plain", "Connection": "keep-alive", "CommandID": _id},
)

print("output posted to server")
conn.close()
print("closed the connection")
time.sleep(1)
Loading