diff --git a/tests/tools/test_ssh_environment.py b/tests/tools/test_ssh_environment.py index 09f090297a274..687068a2e70c0 100644 --- a/tests/tools/test_ssh_environment.py +++ b/tests/tools/test_ssh_environment.py @@ -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. diff --git a/tools/environments/ssh.py b/tools/environments/ssh.py index 1a06ad9b82c0a..152e5a6702126 100644 --- a/tools/environments/ssh.py +++ b/tools/environments/ssh.py @@ -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, @@ -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"): @@ -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() @@ -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: @@ -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 @@ -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}"] @@ -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()}") @@ -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()}") @@ -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() @@ -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()}") @@ -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()}") @@ -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: @@ -366,6 +382,7 @@ def cleanup(self): capture_output=True, timeout=5, stdin=subprocess.DEVNULL, + **_hidden_console_kwargs(), ) except (OSError, subprocess.SubprocessError): pass