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