Skip to content
Draft
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## Unreleased

### Features

- Add `interactive_custom_command` to `InteractiveSSHConnection` and `Connection` base class
to support interactive tool execution with mid-command prompt handling (`Press <Enter>`, `Y or N?`)

## v7.12.0 (2025-07-10)

### Features
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,10 @@ This method starts a process on the remote machine. It returns an `InteractiveSS

This method waits for the host to become available. It tries to connect via SSH and raises a `TimeoutError` if the host does not wake up within the specified timeout.

`interactive_custom_command(self, additional_parameters, cwd, env, press_enter, confirm)`

This method executes an interactive command that requires user prompts mid-execution (e.g. `Press <Enter> to continue...` or `Y or N?`). When `press_enter=True`, it automatically responds to Enter prompts. When `confirm` is set (e.g. `"y"` or `"n"`), it sends that character in response to a Y/N confirmation prompt. Returns a `ConnectionCompletedProcess` object with both `stdout` and `raw_output` attributes containing the full command output.

#### InteractiveSSHProcess

`InteractiveSSHProcess` is a class that represents a process started on the remote machine using the `InteractiveSSHConnection` class. It provides methods to interact with the process.
Expand Down
23 changes: 23 additions & 0 deletions mfd_connect/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,29 @@ def disconnect(self) -> None:
"""Close connection with host."""
pass

def interactive_custom_command(
self,
additional_parameters: str,
cwd: "str | None" = None,
env: "dict | None" = None,
press_enter: bool = True,
confirm: "str | None" = None,
) -> "ConnectionCompletedProcess":
"""
Execute an interactive command that requires user prompts (e.g. Press <Enter>, Y/N confirmation).

:param additional_parameters: Full command string or additional parameters to execute interactively
:param cwd: Current working directory for command execution
:param env: Environment variables for command execution
:param press_enter: Whether to automatically respond to "Press <Enter> to continue..." prompts
:param confirm: Character to send in response to a Y/N prompt (e.g. "y" or "n")
:return: ConnectionCompletedProcess object with return_code and raw_output attributes
:raises NotImplementedError: if not implemented for this connection type
"""
raise NotImplementedError(
f"interactive_custom_command is not implemented for {self.__class__.__name__}"
)

def _apply_cpu_affinity_win(self, *, pid: int, affinity_mask: int) -> None:
"""
Apply calculated affinity_mask to given process ID under Windows OS using Windows API wrapper.
Expand Down
70 changes: 70 additions & 0 deletions mfd_connect/interactive_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,3 +805,73 @@ def cleanup_stdout(self, command: str, stdout: str, prompt: bool = True) -> str:
stdout = re.sub(rf"({re.escape(self.prompt)}\s*(\n|\r\n|\r)*)", "", stdout)
stdout = re.sub(NEW_LINE_PATTERN, "\n", stdout)
return stdout

def interactive_custom_command(
self,
additional_parameters: str,
cwd: str | None = None,
env: dict | None = None,
press_enter: bool = True,
confirm: str | None = None,
) -> "ConnectionCompletedProcess":
"""
Execute an interactive command that requires user prompts.

Handles two interactive patterns EEUpdate uses:
- "Press <Enter> to continue..." — responded to when press_enter=True
- "Continue (Y or N)?" — responded to with the confirm character when confirm is set

:param additional_parameters: Full command string to execute interactively
:param cwd: Current working directory for command execution
:param env: Environment variables for command execution
:param press_enter: Whether to automatically respond to "Press <Enter> to continue..." prompts
:param confirm: Character to send in response to a Y/N prompt (e.g. "y" or "n")
:return: ConnectionCompletedProcess with return_code and raw_output attributes
:raises TimeoutExpired: if the command does not finish within the timeout
"""
if cwd is not None:
self._start_process(f"cd {cwd}", environment=env)
self.refresh_prompt()

self._start_process(additional_parameters, environment=env)
time.sleep(2)

raw_output = ""
_timeout_value = self.default_timeout or IO_TIMEOUT
_timeout_counter = TimeoutCounter(_timeout_value)

while not _timeout_counter:
chan = self.read_channel()
raw_output += chan

if self.prompt in chan:
break

if press_enter and re.search(r"Press\s+<Enter>\s+to\s+continue", chan, re.IGNORECASE):
logger.log(level=log_levels.MODULE_DEBUG, msg="Responding to 'Press <Enter> to continue' prompt")
self.write_to_channel("", with_enter=True)

if confirm is not None and re.search(r"Y\s+or\s+N", chan, re.IGNORECASE):
logger.log(level=log_levels.MODULE_DEBUG, msg=f"Responding to Y/N prompt with '{confirm}'")
self.write_to_channel(confirm, with_enter=True)
confirm = None # send only once

time.sleep(1)
else:
raise TimeoutExpired(
f"Interactive command '{additional_parameters}' did not finish in {_timeout_value} seconds",
_timeout_value,
)

return_code = self._get_return_code(output=raw_output)

result = ConnectionCompletedProcess(
args=additional_parameters,
stdout=raw_output,
stderr="",
stdout_bytes=b"",
stderr_bytes=b"",
return_code=return_code,
)
result.raw_output = raw_output
return result