Skip to content

Commit 0987d24

Browse files
committed
Fixed bugs and added functional tests to ssh
1 parent eaa33cc commit 0987d24

File tree

3 files changed

+124
-48
lines changed

3 files changed

+124
-48
lines changed

conan/internal/runner/output.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from conan.api.output import Color, ConanOutput
2+
3+
class RunnerOutput(ConanOutput):
4+
def __init__(self, hostname: str):
5+
super().__init__()
6+
self.set_warnings_as_errors(True) # Make log errors blocker
7+
self._prefix = f"{hostname} | "
8+
9+
def _write_message(self, msg, fg=None, bg=None, newline=True):
10+
super()._write_message(self._prefix, Color.BLACK, Color.BRIGHT_YELLOW, newline=False)
11+
super()._write_message(msg, fg, bg, newline)
12+
13+
@property
14+
def padding(self):
15+
return len(self._prefix) + 1
16+

conan/internal/runner/ssh.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
import tempfile
55

66
from conan.api.conan_api import ConanAPI
7-
from conan.api.output import Color, ConanOutput
7+
from conan.api.output import Color
88
from conan.errors import ConanException
99

1010
import os
11+
import re
1112
from io import BytesIO
1213
import sys
1314

1415
from conan import conan_version
1516
from conan.internal.model.version import Version
1617
from conan.internal.model.profile import Profile
18+
from conan.internal.runner.output import RunnerOutput
1719

1820
class SSHRunner:
1921
def __init__(
@@ -36,7 +38,7 @@ def __init__(
3638
hostname = self._create_ssh_connection()
3739
except Exception as e:
3840
raise ConanException(f"Error creating SSH connection: {e}")
39-
self.logger = SSHOutput(hostname)
41+
self.logger = RunnerOutput(hostname)
4042
self.logger.status(f"Connected to {hostname}", fg=Color.BRIGHT_MAGENTA)
4143
self.remote_conn = RemoteConnection(self.client, self.logger)
4244

@@ -48,25 +50,24 @@ def run(self):
4850

4951
def _create_ssh_connection(self) -> str:
5052
from paramiko.config import SSHConfig
51-
from paramiko.client import SSHClient
53+
from paramiko.client import SSHClient, AutoAddPolicy
5254

5355
hostname = self.host_profile.runner.get("ssh.host")
5456
if not hostname:
5557
raise ConanException("Host not specified in runner.ssh configuration")
56-
configfile = self.host_profile.runner.get("configfile", False)
57-
if configfile:
58-
if isinstance(configfile, bool):
58+
configfile = self.host_profile.runner.get("ssh.configfile", False)
59+
if configfile and configfile not in ["False", "false", "0"]:
60+
if configfile in ["True", "true", "1"]:
5961
ssh_config_file = Path.home() / ".ssh" / "config"
60-
elif isinstance(configfile, str):
61-
ssh_config_file = Path(configfile)
6262
else:
63-
raise ConanException("Invalid value for runner.ssh.configfile. Should be a string or boolean")
63+
ssh_config_file = Path(configfile)
6464
if not ssh_config_file.exists():
6565
raise ConanException(f"SSH config file not found at {ssh_config_file}")
6666
ssh_config = SSHConfig.from_file(open(ssh_config_file))
6767
if ssh_config and ssh_config.lookup(hostname):
6868
hostname = ssh_config.lookup(hostname)['hostname']
6969
self.client = SSHClient()
70+
self.client.set_missing_host_key_policy(AutoAddPolicy()) # Auto accept unknown keys
7071
self.client.load_system_host_keys()
7172
self.client.connect(hostname)
7273
return hostname
@@ -265,20 +266,8 @@ def _update_local_cache(self, json_result: str):
265266
self.conan_api.cache.restore(local_cache_tgz)
266267

267268

268-
class SSHOutput(ConanOutput):
269-
def __init__(self, hostname: str):
270-
super().__init__()
271-
self.hostname = hostname
272-
self.set_warnings_as_errors(True) # Make log errors blocker
273-
274-
def _write_message(self, msg, fg=None, bg=None, newline=True):
275-
# super()._write_message(f"===> SSH Runner ({self.hostname}): ", Color.BLACK, Color.BRIGHT_YELLOW, newline=False)
276-
super()._write_message(f"({self.hostname}) | ", Color.BLACK, Color.BRIGHT_YELLOW, newline=False)
277-
super()._write_message(msg, fg, bg, newline)
278-
279-
280269
class RemoteConnection:
281-
def __init__(self, client, logger: SSHOutput):
270+
def __init__(self, client, logger: RunnerOutput):
282271
from paramiko.client import SSHClient
283272
self.client: SSHClient = client
284273
self.logger = logger
@@ -348,36 +337,35 @@ def run_interactive_command(self, command: str, is_remote_windows: bool) -> bool
348337
width, height = os.get_terminal_size()
349338
else:
350339
width, height = 80, 24
340+
width -= self.logger.padding
351341
channel.get_pty(width=width, height=height)
352342

353343
channel.exec_command(command)
354344
stdout = channel.makefile("r")
355-
356-
log = []
357345
first_line = True
346+
347+
cursor_movement_pattern = re.compile(r'(\x1b\[(\d+);(\d+)H)')
348+
def remove_cursor_movements(data):
349+
"""Replace cursor movements with newline if column is 1, or empty otherwise."""
350+
def replace_cursor_match(match):
351+
column = int(match.group(3))
352+
if column == 1:
353+
return "\n" # Replace with newline if column is 1
354+
return "" # Otherwise, replace with empty string
355+
return cursor_movement_pattern.sub(replace_cursor_match, data)
356+
357+
358358
while not stdout.channel.exit_status_ready():
359-
line = stdout.channel.recv(1024)
359+
line = stdout.channel.recv(2048)
360360
if first_line and is_remote_windows:
361361
# Avoid clearing and moving the cursor when the remote server is Windows
362362
# https://github.com/PowerShell/Win32-OpenSSH/issues/1738#issuecomment-789434169
363-
line = line.replace(b"\x1b[2J\x1b[m\x1b[H",b"")
363+
line = line.replace(b"\x1b[2J\x1b[m\x1b[H",b"").replace(b"\r\n", b"")
364364
first_line = False
365+
# This is the easiest and better working approach but not testable
365366
# sys.stdout.buffer.write(line)
366367
# sys.stdout.buffer.flush()
367-
line = line.decode('utf-8', errors='ignore')
368-
line = remove_cursor_movements(line)
368+
line = remove_cursor_movements(line.replace(b'\r', b'').decode(errors='ignore').strip())
369369
for l in line.splitlines():
370-
log.append(l)
371-
# if l != "" and l != "\r" and l != "\r\n":
372370
self.logger.status(l)
373-
# for l in log:
374-
# self.logger.status(l)
375371
return stdout.channel.recv_exit_status() == 0
376-
377-
import re
378-
def remove_cursor_movements(text):
379-
# Regex pattern to match cursor movement escape sequences (e.g., \x1b[13;1H)
380-
cursor_movement_pattern = re.compile(r'\x1b\[\d+;\d+H')
381-
382-
# Remove cursor movements from the text
383-
return cursor_movement_pattern.sub('\n', text)

test/functional/command/ssh_runner_test.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import subprocess
21
import pytest
32
import textwrap
43
from conan.test.utils.tools import TestClient
5-
from conan.test.assets.cmake import gen_cmakelists
6-
from conan.test.assets.sources import gen_function_h, gen_function_cpp
7-
import sys
4+
import os
85

96
@pytest.mark.ssh_runner
10-
def test_create_ssh_runner():
7+
def test_create_ssh_runner_only_host():
118
"""
12-
Tests the ``conan create . ``
9+
Tests the ``conan create . `` with ssh runner using only ssh.host
1310
"""
1411
client = TestClient()
1512
profile_build = textwrap.dedent(f"""\
@@ -35,11 +32,86 @@ def test_create_ssh_runner():
3532
[runner]
3633
type=ssh
3734
ssh.host=localhost
38-
# ssh.configfile=True
3935
""")
4036

4137
client.save({"host": profile_host, "build": profile_build})
42-
client.run("new cmake_lib -d name=pkg -d version=0.2")
38+
client.run("new cmake_lib -d name=pkg -d version=1.0")
39+
client.run("create . -pr:h host -pr:b build")
40+
41+
assert "[100%] Built target example" in client.out
42+
assert "Restore: pkg/1.0 in pkgc8bc87152b946" in client.out
43+
assert "Restore: pkg/1.0:746e4557a2789d2071a4b9fb6b4960d7d548ced9 in b/pkg8070ba4308584/p" in client.out
44+
assert "Restore: pkg/1.0:746e4557a2789d2071a4b9fb6b4960d7d548ced9 metadata in b/pkg8070ba4308584/d/metadata" in client.out
45+
46+
@pytest.mark.ssh_runner
47+
def test_create_ssh_runner_with_config():
48+
"""
49+
Tests the ``conan create . ``
50+
"""
51+
client = TestClient()
52+
53+
ssh_config = textwrap.dedent(f"""\
54+
Host local-machine
55+
HostName localhost
56+
""")
57+
client.save({"ssh_config": ssh_config})
58+
59+
profile_build = textwrap.dedent(f"""\
60+
[settings]
61+
arch={{{{ detect_api.detect_arch() }}}}
62+
build_type=Release
63+
compiler=gcc
64+
compiler.cppstd=gnu17
65+
compiler.libcxx=libstdc++11
66+
compiler.version=11
67+
os=Linux
68+
""")
69+
70+
profile_host = textwrap.dedent(f"""\
71+
[settings]
72+
arch={{{{ detect_api.detect_arch() }}}}
73+
build_type=Release
74+
compiler=gcc
75+
compiler.cppstd=gnu17
76+
compiler.libcxx=libstdc++11
77+
compiler.version=11
78+
os=Linux
79+
[runner]
80+
type=ssh
81+
ssh.host=local-machine
82+
ssh.configfile={os.path.join(client.current_folder, 'ssh_config')}
83+
""")
84+
85+
client.save({"host": profile_host, "build": profile_build})
86+
client.run("new cmake_lib -d name=pkg -d version=2.0")
87+
client.run("create . -pr:h host -pr:b build")
88+
89+
assert "[100%] Built target example" in client.out
90+
assert "Restore: pkg/2.0 in pkgc6abef0178849" in client.out
91+
assert "Restore: pkg/2.0:746e4557a2789d2071a4b9fb6b4960d7d548ced9 in b/pkgc1542b12b96fb/p" in client.out
92+
assert "Restore: pkg/2.0:746e4557a2789d2071a4b9fb6b4960d7d548ced9 metadata in b/pkgc1542b12b96fb/d/metadata" in client.out
93+
94+
95+
client.save({".ssh/config": ssh_config}, path='~')
96+
profile_host = textwrap.dedent(f"""\
97+
[settings]
98+
arch={{{{ detect_api.detect_arch() }}}}
99+
build_type=Release
100+
compiler=gcc
101+
compiler.cppstd=gnu17
102+
compiler.libcxx=libstdc++11
103+
compiler.version=11
104+
os=Linux
105+
[runner]
106+
type=ssh
107+
ssh.host=local-machine
108+
# Let the runner find default config file
109+
ssh.configfile=True
110+
""")
111+
client.save({"host": profile_host})
43112
client.run("create . -pr:h host -pr:b build")
44113

45114
assert "[100%] Built target example" in client.out
115+
assert "Restore: pkg/2.0 in pkgc6abef0178849" in client.out
116+
assert "Restore: pkg/2.0:746e4557a2789d2071a4b9fb6b4960d7d548ced9 in b/pkgc1542b12b96fb/p" in client.out
117+
assert "Restore: pkg/2.0:746e4557a2789d2071a4b9fb6b4960d7d548ced9 metadata in b/pkgc1542b12b96fb/d/metadata" in client.out

0 commit comments

Comments
 (0)