Skip to content
Closed
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
33 changes: 33 additions & 0 deletions tests/tools/test_ssh_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ def test_user_host_suffix(self):
env = SSHEnvironment(host="h", user="u")
assert env._build_ssh_command()[-1] == "u@h"

def test_hidden_console_kwargs_are_empty_when_not_needed(self, monkeypatch):
monkeypatch.setattr(ssh_env, "windows_hide_flags", lambda: 0)

assert ssh_env._hidden_console_kwargs() == {}

def test_run_bash_hides_ssh_console_when_windows_flag_available(self, monkeypatch):
captured = {}

monkeypatch.setattr(ssh_env, "windows_hide_flags", lambda: 0x08000000)

def fake_popen_bash(cmd, stdin_data=None, **kwargs):
captured["cmd"] = cmd
captured["stdin_data"] = stdin_data
captured["kwargs"] = kwargs
return MagicMock(stdout=iter([]), stderr=iter([]), stdin=MagicMock())

monkeypatch.setattr(ssh_env, "_popen_bash", fake_popen_bash)

env = SSHEnvironment.__new__(SSHEnvironment)
env.host = "h"
env.user = "u"
env.port = 22
env.key_path = ""
env.controlmaster = False
env.control_socket = None

env._run_bash("echo ok", stdin_data="input")

assert captured["stdin_data"] == "input"
assert captured["kwargs"] == {"creationflags": 0x08000000}
assert captured["cmd"][0] == "ssh"
assert "bash" in captured["cmd"]


class TestControlSocketPath:
"""Regression tests for issue #11840.
Expand Down
19 changes: 18 additions & 1 deletion tools/environments/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import tempfile
from pathlib import Path

from hermes_cli._subprocess_compat import windows_hide_flags
from tools.environments.base import BaseEnvironment, _popen_bash
from tools.environments.file_sync import (
FileSyncManager,
Expand All @@ -21,6 +22,11 @@
logger = logging.getLogger(__name__)


def _hidden_console_kwargs() -> dict:
flags = windows_hide_flags()
return {"creationflags": flags} if flags else {}


def _ensure_ssh_available() -> None:
"""Fail fast with a clear error when the SSH client is unavailable."""
if not shutil.which("ssh"):
Expand Down Expand Up @@ -107,6 +113,7 @@ def _establish_connection(self):
text=True,
timeout=15,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)
if result.returncode != 0:
error_msg = result.stderr.strip() or result.stdout.strip()
Expand All @@ -125,6 +132,7 @@ def _detect_remote_home(self) -> str:
text=True,
timeout=10,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)
home = result.stdout.strip()
if home and result.returncode == 0:
Expand Down Expand Up @@ -152,6 +160,7 @@ def _ensure_remote_dirs(self) -> None:
text=True,
timeout=10,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)

# _get_sync_files provided via iter_sync_files in FileSyncManager init
Expand All @@ -167,6 +176,7 @@ def _scp_upload(self, host_path: str, remote_path: str) -> None:
text=True,
timeout=10,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)

scp_cmd = ["scp", "-o", f"ControlPath={self.control_socket}"]
Expand All @@ -181,6 +191,7 @@ def _scp_upload(self, host_path: str, remote_path: str) -> None:
text=True,
timeout=30,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)
if result.returncode != 0:
raise RuntimeError(f"scp failed: {result.stderr.strip()}")
Expand Down Expand Up @@ -210,6 +221,7 @@ def _ssh_bulk_upload(self, files: list[tuple[str, str]]) -> None:
text=True,
timeout=30,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)
if result.returncode != 0:
raise RuntimeError(f"remote mkdir failed: {result.stderr.strip()}")
Expand Down Expand Up @@ -257,11 +269,13 @@ def _ssh_bulk_upload(self, files: list[tuple[str, str]]) -> None:
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**_hidden_console_kwargs(),
)
try:
ssh_proc = subprocess.Popen(
ssh_cmd, stdin=tar_proc.stdout, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**_hidden_console_kwargs(),
)
except Exception:
tar_proc.kill()
Expand Down Expand Up @@ -314,6 +328,7 @@ def _ssh_bulk_download(self, dest: Path) -> None:
stdout=f,
stderr=subprocess.PIPE,
timeout=120,
**_hidden_console_kwargs(),
)
if result.returncode != 0:
raise RuntimeError(f"SSH bulk download failed: {result.stderr.decode(errors='replace').strip()}")
Expand All @@ -328,6 +343,7 @@ def _ssh_delete(self, remote_paths: list[str]) -> None:
text=True,
timeout=10,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)
if result.returncode != 0:
raise RuntimeError(f"remote rm failed: {result.stderr.strip()}")
Expand All @@ -350,7 +366,7 @@ def _run_bash(self, cmd_string: str, *, login: bool = False,
else:
cmd.extend(["bash", "-c", shlex.quote(cmd_string)])

return _popen_bash(cmd, stdin_data)
return _popen_bash(cmd, stdin_data, **_hidden_console_kwargs())

def cleanup(self):
if self._sync_manager:
Expand All @@ -366,6 +382,7 @@ def cleanup(self):
capture_output=True,
timeout=5,
stdin=subprocess.DEVNULL,
**_hidden_console_kwargs(),
)
except (OSError, subprocess.SubprocessError):
pass
Expand Down