44import tempfile
55
66from conan .api .conan_api import ConanAPI
7- from conan .api .output import Color , ConanOutput
7+ from conan .api .output import Color
88from conan .errors import ConanException
99
1010import os
11+ import re
1112from io import BytesIO
1213import sys
1314
1415from conan import conan_version
1516from conan .internal .model .version import Version
1617from conan .internal .model .profile import Profile
18+ from conan .internal .runner .output import RunnerOutput
1719
1820class 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-
280269class 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 )
0 commit comments