Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 1 | # Copyright 2014-2015 ARM Limited |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | # |
| 15 | |
| 16 | |
| 17 | import os |
| 18 | import stat |
| 19 | import logging |
| 20 | import subprocess |
| 21 | import re |
| 22 | import threading |
| 23 | import tempfile |
| 24 | import shutil |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 25 | import socket |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 26 | import time |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 27 | |
Patrick Bellasi | c1d7cfa | 2015-10-12 15:25:12 +0100 | [diff] [blame] | 28 | import pexpect |
| 29 | from distutils.version import StrictVersion as V |
| 30 | if V(pexpect.__version__) < V('4.0.0'): |
| 31 | import pxssh |
| 32 | else: |
| 33 | from pexpect import pxssh |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 34 | from pexpect import EOF, TIMEOUT, spawn |
| 35 | |
| 36 | from devlib.exception import HostError, TargetError, TimeoutError |
| 37 | from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 38 | from devlib.utils.types import boolean |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 39 | |
| 40 | |
| 41 | ssh = None |
| 42 | scp = None |
| 43 | sshpass = None |
| 44 | |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 45 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 46 | logger = logging.getLogger('ssh') |
| 47 | gem5_logger = logging.getLogger('gem5-connection') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 48 | |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 49 | def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None): |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 50 | _check_env() |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 51 | start_time = time.time() |
| 52 | while True: |
| 53 | if telnet: |
| 54 | if keyfile: |
| 55 | raise ValueError('keyfile may not be used with a telnet connection.') |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 56 | conn = TelnetPxssh(original_prompt=original_prompt) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 57 | else: # ssh |
| 58 | conn = pxssh.pxssh() |
| 59 | |
| 60 | try: |
| 61 | if keyfile: |
| 62 | conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout) |
| 63 | else: |
| 64 | conn.login(host, username, password, port=port, login_timeout=timeout) |
| 65 | break |
| 66 | except EOF: |
| 67 | timeout -= time.time() - start_time |
| 68 | if timeout <= 0: |
| 69 | message = 'Could not connect to {}; is the host name correct?' |
| 70 | raise TargetError(message.format(host)) |
| 71 | time.sleep(5) |
| 72 | |
Patrick Bellasi | 5d288ef | 2015-11-10 11:26:31 +0000 | [diff] [blame] | 73 | conn.setwinsize(500,200) |
| 74 | conn.sendline('') |
| 75 | conn.prompt() |
Patrick Bellasi | 10c80a9 | 2015-11-10 11:27:25 +0000 | [diff] [blame] | 76 | conn.setecho(False) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 77 | return conn |
| 78 | |
| 79 | |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 80 | class TelnetPxssh(pxssh.pxssh): |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 81 | # pylint: disable=arguments-differ |
| 82 | |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 83 | def __init__(self, original_prompt): |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 84 | super(TelnetPxssh, self).__init__() |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 85 | self.original_prompt = original_prompt or r'[#$]' |
| 86 | |
| 87 | def login(self, server, username, password='', login_timeout=10, |
Sebastian Goscik | 73f2e28 | 2016-07-29 15:14:41 +0100 | [diff] [blame] | 88 | auto_prompt_reset=True, sync_multiplier=1, port=23): |
Gareth Stockwell | f244936 | 2016-07-29 15:49:41 +0100 | [diff] [blame] | 89 | args = ['telnet'] |
| 90 | if username is not None: |
| 91 | args += ['-l', username] |
| 92 | args += [server, str(port)] |
| 93 | cmd = ' '.join(args) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 94 | |
| 95 | spawn._spawn(self, cmd) # pylint: disable=protected-access |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 96 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 97 | try: |
Gareth Stockwell | f244936 | 2016-07-29 15:49:41 +0100 | [diff] [blame] | 98 | i = self.expect('(?i)(?:password)', timeout=login_timeout) |
| 99 | if i == 0: |
| 100 | self.sendline(password) |
| 101 | i = self.expect([self.original_prompt, 'Login incorrect'], timeout=login_timeout) |
Gareth Stockwell | f244936 | 2016-07-29 15:49:41 +0100 | [diff] [blame] | 102 | if i: |
| 103 | raise pxssh.ExceptionPxssh('could not log in: password was incorrect') |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 104 | except TIMEOUT: |
| 105 | if not password: |
| 106 | # No password promt before TIMEOUT & no password provided |
| 107 | # so assume everything is okay |
| 108 | pass |
| 109 | else: |
| 110 | raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 111 | |
| 112 | if not self.sync_original_prompt(sync_multiplier): |
| 113 | self.close() |
| 114 | raise pxssh.ExceptionPxssh('could not synchronize with original prompt') |
| 115 | |
| 116 | if auto_prompt_reset: |
| 117 | if not self.set_unique_prompt(): |
| 118 | self.close() |
| 119 | message = 'could not set shell prompt (recieved: {}, expected: {}).' |
| 120 | raise pxssh.ExceptionPxssh(message.format(self.before, self.PROMPT)) |
| 121 | return True |
| 122 | |
| 123 | |
| 124 | def check_keyfile(keyfile): |
| 125 | """ |
| 126 | keyfile must have the right access premissions in order to be useable. If the specified |
| 127 | file doesn't, create a temporary copy and set the right permissions for that. |
| 128 | |
| 129 | Returns either the ``keyfile`` (if the permissions on it are correct) or the path to a |
| 130 | temporary copy with the right permissions. |
| 131 | """ |
| 132 | desired_mask = stat.S_IWUSR | stat.S_IRUSR |
| 133 | actual_mask = os.stat(keyfile).st_mode & 0xFF |
| 134 | if actual_mask != desired_mask: |
| 135 | tmp_file = os.path.join(tempfile.gettempdir(), os.path.basename(keyfile)) |
| 136 | shutil.copy(keyfile, tmp_file) |
| 137 | os.chmod(tmp_file, desired_mask) |
| 138 | return tmp_file |
| 139 | else: # permissions on keyfile are OK |
| 140 | return keyfile |
| 141 | |
| 142 | |
| 143 | class SshConnection(object): |
| 144 | |
| 145 | default_password_prompt = '[sudo] password' |
| 146 | max_cancel_attempts = 5 |
Sergei Trofimov | 89256fd | 2016-05-17 14:00:01 +0100 | [diff] [blame] | 147 | default_timeout=10 |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 148 | |
| 149 | @property |
| 150 | def name(self): |
| 151 | return self.host |
| 152 | |
| 153 | def __init__(self, |
| 154 | host, |
| 155 | username, |
| 156 | password=None, |
| 157 | keyfile=None, |
| 158 | port=None, |
Sergei Trofimov | 89256fd | 2016-05-17 14:00:01 +0100 | [diff] [blame] | 159 | timeout=None, |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 160 | telnet=False, |
| 161 | password_prompt=None, |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 162 | original_prompt=None, |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame] | 163 | platform=None, |
| 164 | sudo_cmd="sudo -- sh -c '{}'" |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 165 | ): |
| 166 | self.host = host |
| 167 | self.username = username |
| 168 | self.password = password |
| 169 | self.keyfile = check_keyfile(keyfile) if keyfile else keyfile |
| 170 | self.port = port |
| 171 | self.lock = threading.Lock() |
| 172 | self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame] | 173 | self.sudo_cmd = sudo_cmd |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 174 | logger.debug('Logging in {}@{}'.format(username, host)) |
Sergei Trofimov | 89256fd | 2016-05-17 14:00:01 +0100 | [diff] [blame] | 175 | timeout = timeout if timeout is not None else self.default_timeout |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 176 | self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 177 | |
| 178 | def push(self, source, dest, timeout=30): |
| 179 | dest = '{}@{}:{}'.format(self.username, self.host, dest) |
| 180 | return self._scp(source, dest, timeout) |
| 181 | |
| 182 | def pull(self, source, dest, timeout=30): |
| 183 | source = '{}@{}:{}'.format(self.username, self.host, source) |
| 184 | return self._scp(source, dest, timeout) |
| 185 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 186 | def execute(self, command, timeout=None, check_exit_code=True, |
| 187 | as_root=False, strip_colors=True): #pylint: disable=unused-argument |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 188 | if command == '': |
| 189 | # Empty command is valid but the __devlib_ec stuff below will |
| 190 | # produce a syntax error with bash. Treat as a special case. |
| 191 | return '' |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 192 | try: |
| 193 | with self.lock: |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 194 | _command = '({}); __devlib_ec=$?; echo; echo $__devlib_ec'.format(command) |
| 195 | raw_output = self._execute_and_wait_for_prompt( |
| 196 | _command, timeout, as_root, strip_colors) |
| 197 | output, exit_code_text, _ = raw_output.rsplit('\r\n', 2) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 198 | if check_exit_code: |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 199 | try: |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 200 | exit_code = int(exit_code_text) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 201 | if exit_code: |
| 202 | message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}' |
| 203 | raise TargetError(message.format(exit_code, command, output)) |
| 204 | except (ValueError, IndexError): |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 205 | logger.warning( |
| 206 | 'Could not get exit code for "{}",\ngot: "{}"'\ |
| 207 | .format(command, exit_code_text)) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 208 | return output |
| 209 | except EOF: |
| 210 | raise TargetError('Connection lost.') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 211 | |
Michele Di Giorgio | 3bf3017 | 2016-03-01 11:12:10 +0000 | [diff] [blame] | 212 | def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False): |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 213 | try: |
| 214 | port_string = '-p {}'.format(self.port) if self.port else '' |
| 215 | keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else '' |
Michele Di Giorgio | 3bf3017 | 2016-03-01 11:12:10 +0000 | [diff] [blame] | 216 | if as_root: |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame] | 217 | command = self.sudo_cmd.format(command) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 218 | command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command) |
| 219 | logger.debug(command) |
| 220 | if self.password: |
| 221 | command = _give_password(self.password, command) |
| 222 | return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True) |
| 223 | except EOF: |
| 224 | raise TargetError('Connection lost.') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 225 | |
| 226 | def close(self): |
| 227 | logger.debug('Logging out {}@{}'.format(self.username, self.host)) |
| 228 | self.conn.logout() |
| 229 | |
| 230 | def cancel_running_command(self): |
| 231 | # simulate impatiently hitting ^C until command prompt appears |
| 232 | logger.debug('Sending ^C') |
| 233 | for _ in xrange(self.max_cancel_attempts): |
| 234 | self.conn.sendline(chr(3)) |
| 235 | if self.conn.prompt(0.1): |
| 236 | return True |
| 237 | return False |
| 238 | |
| 239 | def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True): |
| 240 | self.conn.prompt(0.1) # clear an existing prompt if there is one. |
Sergei Trofimov | bd27de1 | 2017-04-20 17:00:17 +0100 | [diff] [blame] | 241 | if self.username == 'root': |
| 242 | # As we're already root, there is no need to use sudo. |
| 243 | as_root = False |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 244 | if as_root: |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame] | 245 | command = self.sudo_cmd.format(escape_single_quotes(command)) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 246 | if log: |
| 247 | logger.debug(command) |
| 248 | self.conn.sendline(command) |
| 249 | if self.password: |
| 250 | index = self.conn.expect_exact([self.password_prompt, TIMEOUT], timeout=0.5) |
| 251 | if index == 0: |
| 252 | self.conn.sendline(self.password) |
| 253 | else: # not as_root |
| 254 | if log: |
| 255 | logger.debug(command) |
| 256 | self.conn.sendline(command) |
| 257 | timed_out = self._wait_for_prompt(timeout) |
| 258 | # the regex removes line breaks potential introduced when writing |
| 259 | # command to shell. |
| 260 | output = process_backspaces(self.conn.before) |
| 261 | output = re.sub(r'\r([^\n])', r'\1', output) |
| 262 | if '\r\n' in output: # strip the echoed command |
| 263 | output = output.split('\r\n', 1)[1] |
| 264 | if timed_out: |
| 265 | self.cancel_running_command() |
| 266 | raise TimeoutError(command, output) |
| 267 | if strip_colors: |
| 268 | output = strip_bash_colors(output) |
| 269 | return output |
| 270 | |
| 271 | def _wait_for_prompt(self, timeout=None): |
| 272 | if timeout: |
| 273 | return not self.conn.prompt(timeout) |
| 274 | else: # cannot timeout; wait forever |
| 275 | while not self.conn.prompt(1): |
| 276 | pass |
| 277 | return False |
| 278 | |
| 279 | def _scp(self, source, dest, timeout=30): |
| 280 | # NOTE: the version of scp in Ubuntu 12.04 occasionally (and bizarrely) |
| 281 | # fails to connect to a device if port is explicitly specified using -P |
| 282 | # option, even if it is the default port, 22. To minimize this problem, |
| 283 | # only specify -P for scp if the port is *not* the default. |
| 284 | port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else '' |
| 285 | keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else '' |
| 286 | command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest) |
Brendan Jackman | 93b39a7 | 2017-08-09 15:59:24 +0100 | [diff] [blame] | 287 | command_redacted = command |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 288 | logger.debug(command) |
| 289 | if self.password: |
| 290 | command = _give_password(self.password, command) |
Brendan Jackman | 93b39a7 | 2017-08-09 15:59:24 +0100 | [diff] [blame] | 291 | command_redacted = command.replace(self.password, '<redacted>') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 292 | try: |
| 293 | check_output(command, timeout=timeout, shell=True) |
| 294 | except subprocess.CalledProcessError as e: |
Brendan Jackman | 380ad05 | 2017-08-09 16:00:46 +0100 | [diff] [blame] | 295 | raise HostError("Failed to copy file with '{}'. Output:\n{}".format( |
| 296 | command_redacted, e.output)) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 297 | except TimeoutError as e: |
Brendan Jackman | 93b39a7 | 2017-08-09 15:59:24 +0100 | [diff] [blame] | 298 | raise TimeoutError(command_redacted, e.output) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 299 | |
| 300 | |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 301 | class TelnetConnection(SshConnection): |
| 302 | |
| 303 | def __init__(self, |
| 304 | host, |
| 305 | username, |
| 306 | password=None, |
| 307 | port=None, |
| 308 | timeout=None, |
| 309 | password_prompt=None, |
| 310 | original_prompt=None, |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 311 | platform=None): |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 312 | self.host = host |
| 313 | self.username = username |
| 314 | self.password = password |
| 315 | self.port = port |
| 316 | self.keyfile = None |
| 317 | self.lock = threading.Lock() |
| 318 | self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt |
| 319 | logger.debug('Logging in {}@{}'.format(username, host)) |
| 320 | timeout = timeout if timeout is not None else self.default_timeout |
| 321 | self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt) |
| 322 | |
| 323 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 324 | class Gem5Connection(TelnetConnection): |
| 325 | |
| 326 | def __init__(self, |
| 327 | platform, |
| 328 | host=None, |
| 329 | username=None, |
| 330 | password=None, |
| 331 | port=None, |
| 332 | timeout=None, |
| 333 | password_prompt=None, |
| 334 | original_prompt=None, |
| 335 | ): |
| 336 | if host is not None: |
| 337 | host_system = socket.gethostname() |
| 338 | if host_system != host: |
| 339 | raise TargetError("Gem5Connection can only connect to gem5 " |
| 340 | "simulations on your current host, which " |
| 341 | "differs from the one given {}!" |
| 342 | .format(host_system, host)) |
| 343 | if username is not None and username != 'root': |
| 344 | raise ValueError('User should be root in gem5!') |
| 345 | if password is not None and password != '': |
| 346 | raise ValueError('No password needed in gem5!') |
| 347 | self.username = 'root' |
| 348 | self.is_rooted = True |
| 349 | self.password = None |
| 350 | self.port = None |
| 351 | # Long timeouts to account for gem5 being slow |
| 352 | # Can be overriden if the given timeout is longer |
| 353 | self.default_timeout = 3600 |
| 354 | if timeout is not None: |
| 355 | if timeout > self.default_timeout: |
| 356 | logger.info('Overwriting the default timeout of gem5 ({})' |
| 357 | ' to {}'.format(self.default_timeout, timeout)) |
| 358 | self.default_timeout = timeout |
| 359 | else: |
| 360 | logger.info('Ignoring the given timeout --> gem5 needs longer timeouts') |
| 361 | self.ready_timeout = self.default_timeout * 3 |
| 362 | # Counterpart in gem5_interact_dir |
| 363 | self.gem5_input_dir = '/mnt/host/' |
| 364 | # Location of m5 binary in the gem5 simulated system |
| 365 | self.m5_path = None |
| 366 | # Actual telnet connection to gem5 simulation |
| 367 | self.conn = None |
| 368 | # Flag to indicate the gem5 device is ready to interact with the |
| 369 | # outer world |
| 370 | self.ready = False |
| 371 | # Lock file to prevent multiple connections to same gem5 simulation |
| 372 | # (gem5 does not allow this) |
| 373 | self.lock_directory = '/tmp/' |
| 374 | self.lock_file_name = None # Will be set once connected to gem5 |
| 375 | |
| 376 | # These parameters will be set by either the method to connect to the |
| 377 | # gem5 platform or directly to the gem5 simulation |
| 378 | # Intermediate directory to push things to gem5 using VirtIO |
| 379 | self.gem5_interact_dir = None |
| 380 | # Directory to store output from gem5 on the host |
| 381 | self.gem5_out_dir = None |
| 382 | # Actual gem5 simulation |
| 383 | self.gem5simulation = None |
| 384 | |
| 385 | # Connect to gem5 |
| 386 | if platform: |
| 387 | self._connect_gem5_platform(platform) |
| 388 | |
| 389 | # Wait for boot |
| 390 | self._wait_for_boot() |
| 391 | |
| 392 | # Mount the virtIO to transfer files in/out gem5 system |
| 393 | self._mount_virtio() |
| 394 | |
| 395 | def set_hostinteractdir(self, indir): |
| 396 | logger.info('Setting hostinteractdir from {} to {}' |
| 397 | .format(self.gem5_input_dir, indir)) |
| 398 | self.gem5_input_dir = indir |
| 399 | |
| 400 | def push(self, source, dest, timeout=None): |
| 401 | """ |
| 402 | Push a file to the gem5 device using VirtIO |
| 403 | |
| 404 | The file to push to the device is copied to the temporary directory on |
| 405 | the host, before being copied within the simulation to the destination. |
| 406 | Checks, in the form of 'ls' with error code checking, are performed to |
| 407 | ensure that the file is copied to the destination. |
| 408 | """ |
| 409 | # First check if the connection is set up to interact with gem5 |
| 410 | self._check_ready() |
| 411 | |
| 412 | filename = os.path.basename(source) |
| 413 | logger.debug("Pushing {} to device.".format(source)) |
| 414 | logger.debug("gem5interactdir: {}".format(self.gem5_interact_dir)) |
| 415 | logger.debug("dest: {}".format(dest)) |
| 416 | logger.debug("filename: {}".format(filename)) |
| 417 | |
| 418 | # We need to copy the file to copy to the temporary directory |
| 419 | self._move_to_temp_dir(source) |
| 420 | |
| 421 | # Dest in gem5 world is a file rather than directory |
| 422 | if os.path.basename(dest) != filename: |
| 423 | dest = os.path.join(dest, filename) |
| 424 | # Back to the gem5 world |
| 425 | self._gem5_shell("ls -al {}{}".format(self.gem5_input_dir, filename)) |
| 426 | self._gem5_shell("cat '{}''{}' > '{}'".format(self.gem5_input_dir, |
| 427 | filename, |
| 428 | dest)) |
| 429 | self._gem5_shell("sync") |
| 430 | self._gem5_shell("ls -al {}".format(dest)) |
| 431 | self._gem5_shell("ls -al {}".format(self.gem5_input_dir)) |
| 432 | logger.debug("Push complete.") |
| 433 | |
| 434 | def pull(self, source, dest, timeout=0): #pylint: disable=unused-argument |
| 435 | """ |
| 436 | Pull a file from the gem5 device using m5 writefile |
| 437 | |
| 438 | The file is copied to the local directory within the guest as the m5 |
| 439 | writefile command assumes that the file is local. The file is then |
| 440 | written out to the host system using writefile, prior to being moved to |
| 441 | the destination on the host. |
| 442 | """ |
| 443 | # First check if the connection is set up to interact with gem5 |
| 444 | self._check_ready() |
| 445 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 446 | result = self._gem5_shell("ls {}".format(source)) |
Sergei Trofimov | 1072a1a | 2017-10-06 16:20:33 +0100 | [diff] [blame] | 447 | files = strip_bash_colors(result).split() |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 448 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 449 | for filename in files: |
| 450 | dest_file = os.path.basename(filename) |
| 451 | logger.debug("pull_file {} {}".format(filename, dest_file)) |
| 452 | # writefile needs the file to be copied to be in the current |
| 453 | # working directory so if needed, copy to the working directory |
| 454 | # We don't check the exit code here because it is non-zero if the |
| 455 | # source and destination are the same. The ls below will cause an |
| 456 | # error if the file was not where we expected it to be. |
| 457 | if os.path.isabs(source): |
| 458 | if os.path.dirname(source) != self.execute('pwd', |
| 459 | check_exit_code=False): |
| 460 | self._gem5_shell("cat '{}' > '{}'".format(filename, |
| 461 | dest_file)) |
| 462 | self._gem5_shell("sync") |
| 463 | self._gem5_shell("ls -la {}".format(dest_file)) |
| 464 | logger.debug('Finished the copy in the simulator') |
| 465 | self._gem5_util("writefile {}".format(dest_file)) |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 466 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 467 | if 'cpu' not in filename: |
| 468 | while not os.path.exists(os.path.join(self.gem5_out_dir, |
| 469 | dest_file)): |
| 470 | time.sleep(1) |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 471 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 472 | # Perform the local move |
| 473 | if os.path.exists(os.path.join(dest, dest_file)): |
| 474 | logger.warning( |
| 475 | 'Destination file {} already exists!'\ |
| 476 | .format(dest_file)) |
| 477 | else: |
| 478 | shutil.move(os.path.join(self.gem5_out_dir, dest_file), dest) |
| 479 | logger.debug("Pull complete.") |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 480 | |
| 481 | def execute(self, command, timeout=1000, check_exit_code=True, |
| 482 | as_root=False, strip_colors=True): |
| 483 | """ |
| 484 | Execute a command on the gem5 platform |
| 485 | """ |
| 486 | # First check if the connection is set up to interact with gem5 |
| 487 | self._check_ready() |
| 488 | |
Sascha Bischoff | f5906cb | 2017-05-11 10:28:05 +0100 | [diff] [blame] | 489 | output = self._gem5_shell(command, |
| 490 | check_exit_code=check_exit_code, |
| 491 | as_root=as_root) |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 492 | if strip_colors: |
| 493 | output = strip_bash_colors(output) |
| 494 | return output |
| 495 | |
| 496 | def background(self, command, stdout=subprocess.PIPE, |
| 497 | stderr=subprocess.PIPE, as_root=False): |
| 498 | # First check if the connection is set up to interact with gem5 |
| 499 | self._check_ready() |
| 500 | |
| 501 | # Create the logfile for stderr/stdout redirection |
| 502 | command_name = command.split(' ')[0].split('/')[-1] |
| 503 | redirection_file = 'BACKGROUND_{}.log'.format(command_name) |
| 504 | trial = 0 |
| 505 | while os.path.isfile(redirection_file): |
| 506 | # Log file already exists so add to name |
| 507 | redirection_file = 'BACKGROUND_{}{}.log'.format(command_name, trial) |
| 508 | trial += 1 |
| 509 | |
| 510 | # Create the command to pass on to gem5 shell |
| 511 | complete_command = '{} >> {} 2>&1 &'.format(command, redirection_file) |
| 512 | output = self._gem5_shell(complete_command, as_root=as_root) |
| 513 | output = strip_bash_colors(output) |
| 514 | gem5_logger.info('STDERR/STDOUT of background command will be ' |
| 515 | 'redirected to {}. Use target.pull() to ' |
| 516 | 'get this file'.format(redirection_file)) |
| 517 | return output |
| 518 | |
| 519 | def close(self): |
| 520 | """ |
| 521 | Close and disconnect from the gem5 simulation. Additionally, we remove |
| 522 | the temporary directory used to pass files into the simulation. |
| 523 | """ |
| 524 | gem5_logger.info("Gracefully terminating the gem5 simulation.") |
| 525 | try: |
| 526 | self._gem5_util("exit") |
| 527 | self.gem5simulation.wait() |
| 528 | except EOF: |
| 529 | pass |
| 530 | gem5_logger.info("Removing the temporary directory") |
| 531 | try: |
| 532 | shutil.rmtree(self.gem5_interact_dir) |
| 533 | except OSError: |
| 534 | gem5_logger.warn("Failed to remove the temporary directory!") |
| 535 | |
| 536 | # Delete the lock file |
| 537 | os.remove(self.lock_file_name) |
| 538 | |
| 539 | # Functions only to be called by the Gem5 connection itself |
| 540 | def _connect_gem5_platform(self, platform): |
| 541 | port = platform.gem5_port |
| 542 | gem5_simulation = platform.gem5 |
| 543 | gem5_interact_dir = platform.gem5_interact_dir |
| 544 | gem5_out_dir = platform.gem5_out_dir |
| 545 | |
| 546 | self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir) |
| 547 | |
| 548 | # This function connects to the gem5 simulation |
| 549 | def connect_gem5(self, port, gem5_simulation, gem5_interact_dir, |
| 550 | gem5_out_dir): |
| 551 | """ |
| 552 | Connect to the telnet port of the gem5 simulation. |
| 553 | |
| 554 | We connect, and wait for the prompt to be found. We do not use a timeout |
| 555 | for this, and wait for the prompt in a while loop as the gem5 simulation |
| 556 | can take many hours to reach a prompt when booting the system. We also |
| 557 | inject some newlines periodically to try and force gem5 to show a |
| 558 | prompt. Once the prompt has been found, we replace it with a unique |
| 559 | prompt to ensure that we are able to match it properly. We also disable |
| 560 | the echo as this simplifies parsing the output when executing commands |
| 561 | on the device. |
| 562 | """ |
| 563 | host = socket.gethostname() |
| 564 | gem5_logger.info("Connecting to the gem5 simulation on port {}".format(port)) |
| 565 | |
| 566 | # Check if there is no on-going connection yet |
| 567 | lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port) |
| 568 | if os.path.isfile(lock_file_name): |
| 569 | # There is already a connection to this gem5 simulation |
| 570 | raise TargetError('There is already a connection to the gem5 ' |
| 571 | 'simulation using port {} on {}!' |
| 572 | .format(port, host)) |
| 573 | |
| 574 | # Connect to the gem5 telnet port. Use a short timeout here. |
| 575 | attempts = 0 |
| 576 | while attempts < 10: |
| 577 | attempts += 1 |
| 578 | try: |
| 579 | self.conn = TelnetPxssh(original_prompt=None) |
| 580 | self.conn.login(host, self.username, port=port, |
| 581 | login_timeout=10, auto_prompt_reset=False) |
| 582 | break |
| 583 | except pxssh.ExceptionPxssh: |
| 584 | pass |
| 585 | else: |
| 586 | gem5_simulation.kill() |
| 587 | raise TargetError("Failed to connect to the gem5 telnet session.") |
| 588 | |
| 589 | gem5_logger.info("Connected! Waiting for prompt...") |
| 590 | |
| 591 | # Create the lock file |
| 592 | self.lock_file_name = lock_file_name |
| 593 | open(self.lock_file_name, 'w').close() # Similar to touch |
| 594 | gem5_logger.info("Created lock file {} to prevent reconnecting to " |
| 595 | "same simulation".format(self.lock_file_name)) |
| 596 | |
| 597 | # We need to find the prompt. It might be different if we are resuming |
| 598 | # from a checkpoint. Therefore, we test multiple options here. |
| 599 | prompt_found = False |
| 600 | while not prompt_found: |
| 601 | try: |
| 602 | self._login_to_device() |
| 603 | except TIMEOUT: |
| 604 | pass |
| 605 | try: |
| 606 | # Try and force a prompt to be shown |
| 607 | self.conn.send('\n') |
| 608 | self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60) |
| 609 | prompt_found = True |
| 610 | except TIMEOUT: |
| 611 | pass |
| 612 | |
| 613 | gem5_logger.info("Successfully logged in") |
| 614 | gem5_logger.info("Setting unique prompt...") |
| 615 | |
| 616 | self.conn.set_unique_prompt() |
| 617 | self.conn.prompt() |
| 618 | gem5_logger.info("Prompt found and replaced with a unique string") |
| 619 | |
| 620 | # We check that the prompt is what we think it should be. If not, we |
| 621 | # need to update the regex we use to match. |
| 622 | self._find_prompt() |
| 623 | |
| 624 | self.conn.setecho(False) |
| 625 | self._sync_gem5_shell() |
| 626 | |
| 627 | # Fully connected to gem5 simulation |
| 628 | self.gem5_interact_dir = gem5_interact_dir |
| 629 | self.gem5_out_dir = gem5_out_dir |
| 630 | self.gem5simulation = gem5_simulation |
| 631 | |
| 632 | # Ready for interaction now |
| 633 | self.ready = True |
| 634 | |
| 635 | def _login_to_device(self): |
| 636 | """ |
| 637 | Login to device, will be overwritten if there is an actual login |
| 638 | """ |
| 639 | pass |
| 640 | |
| 641 | def _find_prompt(self): |
| 642 | prompt = r'\[PEXPECT\][\\\$\#]+ ' |
| 643 | synced = False |
| 644 | while not synced: |
| 645 | self.conn.send('\n') |
| 646 | i = self.conn.expect([prompt, self.conn.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.default_timeout) |
| 647 | if i == 0: |
| 648 | synced = True |
| 649 | elif i == 1: |
| 650 | prompt = self.conn.UNIQUE_PROMPT |
| 651 | synced = True |
| 652 | else: |
| 653 | prompt = re.sub(r'\$', r'\\\$', self.conn.before.strip() + self.conn.after.strip()) |
| 654 | prompt = re.sub(r'\#', r'\\\#', prompt) |
| 655 | prompt = re.sub(r'\[', r'\[', prompt) |
| 656 | prompt = re.sub(r'\]', r'\]', prompt) |
| 657 | |
| 658 | self.conn.PROMPT = prompt |
| 659 | |
| 660 | def _sync_gem5_shell(self): |
| 661 | """ |
| 662 | Synchronise with the gem5 shell. |
| 663 | |
| 664 | Write some unique text to the gem5 device to allow us to synchronise |
| 665 | with the shell output. We actually get two prompts so we need to match |
| 666 | both of these. |
| 667 | """ |
| 668 | gem5_logger.debug("Sending Sync") |
| 669 | self.conn.send("echo \*\*sync\*\*\n") |
| 670 | self.conn.expect(r"\*\*sync\*\*", timeout=self.default_timeout) |
| 671 | self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout) |
| 672 | self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout) |
| 673 | |
| 674 | def _gem5_util(self, command): |
| 675 | """ Execute a gem5 utility command using the m5 binary on the device """ |
| 676 | if self.m5_path is None: |
| 677 | raise TargetError('Path to m5 binary on simulated system is not set!') |
| 678 | self._gem5_shell('{} {}'.format(self.m5_path, command)) |
| 679 | |
| 680 | def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True): # pylint: disable=R0912 |
| 681 | """ |
| 682 | Execute a command in the gem5 shell |
| 683 | |
| 684 | This wraps the telnet connection to gem5 and processes the raw output. |
| 685 | |
| 686 | This method waits for the shell to return, and then will try and |
| 687 | separate the output from the command from the command itself. If this |
| 688 | fails, warn, but continue with the potentially wrong output. |
| 689 | |
| 690 | The exit code is also checked by default, and non-zero exit codes will |
| 691 | raise a TargetError. |
| 692 | """ |
| 693 | if sync: |
| 694 | self._sync_gem5_shell() |
| 695 | |
| 696 | gem5_logger.debug("gem5_shell command: {}".format(command)) |
| 697 | |
| 698 | # Send the actual command |
| 699 | self.conn.send("{}\n".format(command)) |
| 700 | |
| 701 | # Wait for the response. We just sit here and wait for the prompt to |
| 702 | # appear, as gem5 might take a long time to provide the output. This |
| 703 | # avoids timeout issues. |
| 704 | command_index = -1 |
| 705 | while command_index == -1: |
| 706 | if self.conn.prompt(): |
| 707 | output = re.sub(r' \r([^\n])', r'\1', self.conn.before) |
| 708 | output = re.sub(r'[\b]', r'', output) |
| 709 | # Deal with line wrapping |
| 710 | output = re.sub(r'[\r].+?<', r'', output) |
| 711 | command_index = output.find(command) |
| 712 | |
| 713 | # If we have -1, then we cannot match the command, but the |
| 714 | # prompt has returned. Hence, we have a bit of an issue. We |
| 715 | # warn, and return the whole output. |
| 716 | if command_index == -1: |
| 717 | gem5_logger.warn("gem5_shell: Unable to match command in " |
| 718 | "command output. Expect parsing errors!") |
| 719 | command_index = 0 |
| 720 | |
| 721 | output = output[command_index + len(command):].strip() |
| 722 | |
| 723 | # It is possible that gem5 will echo the command. Therefore, we need to |
| 724 | # remove that too! |
| 725 | command_index = output.find(command) |
| 726 | if command_index != -1: |
| 727 | output = output[command_index + len(command):].strip() |
| 728 | |
| 729 | gem5_logger.debug("gem5_shell output: {}".format(output)) |
| 730 | |
| 731 | # We get a second prompt. Hence, we need to eat one to make sure that we |
| 732 | # stay in sync. If we do not do this, we risk getting out of sync for |
| 733 | # slower simulations. |
| 734 | self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout) |
| 735 | |
| 736 | if check_exit_code: |
| 737 | exit_code_text = self._gem5_shell('echo $?', as_root=as_root, |
| 738 | timeout=timeout, check_exit_code=False, |
| 739 | sync=False) |
| 740 | try: |
| 741 | exit_code = int(exit_code_text.split()[0]) |
| 742 | if exit_code: |
| 743 | message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}' |
| 744 | raise TargetError(message.format(exit_code, command, output)) |
| 745 | except (ValueError, IndexError): |
| 746 | gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text)) |
| 747 | |
| 748 | return output |
| 749 | |
| 750 | def _mount_virtio(self): |
| 751 | """ |
| 752 | Mount the VirtIO device in the simulated system. |
| 753 | """ |
| 754 | gem5_logger.info("Mounting VirtIO device in simulated system") |
| 755 | |
| 756 | self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir)) |
| 757 | mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir) |
| 758 | self._gem5_shell(mount_command) |
| 759 | |
| 760 | def _move_to_temp_dir(self, source): |
| 761 | """ |
| 762 | Move a file to the temporary directory on the host for copying to the |
| 763 | gem5 device |
| 764 | """ |
| 765 | command = "cp {} {}".format(source, self.gem5_interact_dir) |
| 766 | gem5_logger.debug("Local copy command: {}".format(command)) |
| 767 | subprocess.call(command.split()) |
| 768 | subprocess.call("sync".split()) |
| 769 | |
| 770 | def _check_ready(self): |
| 771 | """ |
| 772 | Check if the gem5 platform is ready |
| 773 | """ |
| 774 | if not self.ready: |
| 775 | raise TargetError('Gem5 is not ready to interact yet') |
| 776 | |
| 777 | def _wait_for_boot(self): |
| 778 | pass |
| 779 | |
| 780 | def _probe_file(self, filepath): |
| 781 | """ |
| 782 | Internal method to check if the target has a certain file |
| 783 | """ |
| 784 | command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi' |
| 785 | output = self.execute(command.format(filepath), as_root=self.is_rooted) |
| 786 | return boolean(output.strip()) |
| 787 | |
| 788 | |
| 789 | class LinuxGem5Connection(Gem5Connection): |
| 790 | |
| 791 | def _login_to_device(self): |
| 792 | gem5_logger.info("Trying to log in to gem5 device") |
| 793 | login_prompt = ['login:', 'AEL login:', 'username:', 'aarch64-gem5 login:'] |
| 794 | login_password_prompt = ['password:'] |
| 795 | # Wait for the login prompt |
| 796 | prompt = login_prompt + [self.conn.UNIQUE_PROMPT] |
| 797 | i = self.conn.expect(prompt, timeout=10) |
| 798 | # Check if we are already at a prompt, or if we need to log in. |
| 799 | if i < len(prompt) - 1: |
| 800 | self.conn.sendline("{}".format(self.username)) |
| 801 | password_prompt = login_password_prompt + [r'# ', self.conn.UNIQUE_PROMPT] |
| 802 | j = self.conn.expect(password_prompt, timeout=self.default_timeout) |
| 803 | if j < len(password_prompt) - 2: |
| 804 | self.conn.sendline("{}".format(self.password)) |
| 805 | self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT], timeout=self.default_timeout) |
| 806 | |
| 807 | |
| 808 | |
| 809 | class AndroidGem5Connection(Gem5Connection): |
| 810 | |
| 811 | def _wait_for_boot(self): |
| 812 | """ |
| 813 | Wait for the system to boot |
| 814 | |
| 815 | We monitor the sys.boot_completed and service.bootanim.exit system |
| 816 | properties to determine when the system has finished booting. In the |
| 817 | event that we cannot coerce the result of service.bootanim.exit to an |
| 818 | integer, we assume that the boot animation was disabled and do not wait |
| 819 | for it to finish. |
| 820 | |
| 821 | """ |
| 822 | gem5_logger.info("Waiting for Android to boot...") |
| 823 | while True: |
| 824 | booted = False |
| 825 | anim_finished = True # Assume boot animation was disabled on except |
| 826 | try: |
| 827 | booted = (int('0' + self._gem5_shell('getprop sys.boot_completed', check_exit_code=False).strip()) == 1) |
| 828 | anim_finished = (int(self._gem5_shell('getprop service.bootanim.exit', check_exit_code=False).strip()) == 1) |
| 829 | except ValueError: |
| 830 | pass |
| 831 | if booted and anim_finished: |
| 832 | break |
| 833 | time.sleep(60) |
| 834 | |
| 835 | gem5_logger.info("Android booted") |
| 836 | |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 837 | def _give_password(password, command): |
| 838 | if not sshpass: |
| 839 | raise HostError('Must have sshpass installed on the host in order to use password-based auth.') |
| 840 | pass_string = "sshpass -p '{}' ".format(password) |
| 841 | return pass_string + command |
| 842 | |
| 843 | |
| 844 | def _check_env(): |
| 845 | global ssh, scp, sshpass # pylint: disable=global-statement |
| 846 | if not ssh: |
| 847 | ssh = which('ssh') |
| 848 | scp = which('scp') |
| 849 | sshpass = which('sshpass') |
| 850 | if not (ssh and scp): |
| 851 | raise HostError('OpenSSH must be installed on the host.') |
| 852 | |
| 853 | |
| 854 | def process_backspaces(text): |
| 855 | chars = [] |
| 856 | for c in text: |
| 857 | if c == chr(8) and chars: # backspace |
| 858 | chars.pop() |
| 859 | else: |
| 860 | chars.append(c) |
| 861 | return ''.join(chars) |