aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--devlib/__init__.py2
-rw-r--r--devlib/host.py3
-rw-r--r--devlib/utils/android.py5
-rw-r--r--devlib/utils/ssh.py522
4 files changed, 519 insertions, 13 deletions
diff --git a/devlib/__init__.py b/devlib/__init__.py
index 86949d3..f4ff0f3 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -19,4 +19,4 @@ from devlib.trace.ftrace import FtraceCollector
from devlib.host import LocalConnection
from devlib.utils.android import AdbConnection
-from devlib.utils.ssh import SshConnection, TelnetConnection
+from devlib.utils.ssh import SshConnection, TelnetConnection, Gem5Connection
diff --git a/devlib/host.py b/devlib/host.py
index 121e5b4..3e42c0f 100644
--- a/devlib/host.py
+++ b/devlib/host.py
@@ -49,7 +49,8 @@ class LocalConnection(object):
else:
shutil.copy(source, dest)
- def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
+ def execute(self, command, timeout=None, check_exit_code=True,
+ as_root=False, strip_colors=True):
self.logger.debug(command)
if as_root:
if self.unrooted:
diff --git a/devlib/utils/android.py b/devlib/utils/android.py
index b3479a8..ad4c324 100644
--- a/devlib/utils/android.py
+++ b/devlib/utils/android.py
@@ -186,7 +186,7 @@ class AdbConnection(object):
self.ls_command = 'ls'
logger.info("ls command is set to {}".format(self.ls_command))
- def __init__(self, device=None, timeout=None):
+ def __init__(self, device=None, timeout=None, platform=None):
self.timeout = timeout if timeout is not None else self.default_timeout
if device is None:
device = adb_get_device(timeout=timeout)
@@ -216,7 +216,8 @@ class AdbConnection(object):
command = "pull '{}' '{}'".format(source, dest)
return adb_command(self.device, command, timeout=timeout)
- def execute(self, command, timeout=None, check_exit_code=False, as_root=False):
+ def execute(self, command, timeout=None, check_exit_code=False,
+ as_root=False, strip_colors=True):
return adb_shell(self.device, command, timeout, check_exit_code,
as_root, self.newline_separator)
diff --git a/devlib/utils/ssh.py b/devlib/utils/ssh.py
index 448f9e7..ff30346 100644
--- a/devlib/utils/ssh.py
+++ b/devlib/utils/ssh.py
@@ -22,6 +22,7 @@ import re
import threading
import tempfile
import shutil
+import socket
import time
import pexpect
@@ -34,14 +35,16 @@ from pexpect import EOF, TIMEOUT, spawn
from devlib.exception import HostError, TargetError, TimeoutError
from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output
+from devlib.utils.types import boolean
ssh = None
scp = None
sshpass = None
-logger = logging.getLogger('ssh')
+logger = logging.getLogger('ssh')
+gem5_logger = logging.getLogger('gem5-connection')
def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None):
_check_env()
@@ -91,18 +94,20 @@ class TelnetPxssh(pxssh.pxssh):
spawn._spawn(self, cmd) # pylint: disable=protected-access
- if password is None:
- i = self.expect([self.original_prompt, 'Login timed out'], timeout=login_timeout)
- else:
+ try:
i = self.expect('(?i)(?:password)', timeout=login_timeout)
if i == 0:
self.sendline(password)
i = self.expect([self.original_prompt, 'Login incorrect'], timeout=login_timeout)
- else:
- raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt')
-
if i:
raise pxssh.ExceptionPxssh('could not log in: password was incorrect')
+ except TIMEOUT:
+ if not password:
+ # No password promt before TIMEOUT & no password provided
+ # so assume everything is okay
+ pass
+ else:
+ raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt')
if not self.sync_original_prompt(sync_multiplier):
self.close()
@@ -155,6 +160,7 @@ class SshConnection(object):
telnet=False,
password_prompt=None,
original_prompt=None,
+ platform=None
):
self.host = host
self.username = username
@@ -175,7 +181,8 @@ class SshConnection(object):
source = '{}@{}:{}'.format(self.username, self.host, source)
return self._scp(source, dest, timeout)
- def execute(self, command, timeout=None, check_exit_code=True, as_root=False, strip_colors=True):
+ def execute(self, command, timeout=None, check_exit_code=True,
+ as_root=False, strip_colors=True): #pylint: disable=unused-argument
try:
with self.lock:
output = self._execute_and_wait_for_prompt(command, timeout, as_root, strip_colors)
@@ -286,7 +293,7 @@ class TelnetConnection(SshConnection):
timeout=None,
password_prompt=None,
original_prompt=None,
- ):
+ platform=None):
self.host = host
self.username = username
self.password = password
@@ -299,6 +306,503 @@ class TelnetConnection(SshConnection):
self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt)
+class Gem5Connection(TelnetConnection):
+
+ def __init__(self,
+ platform,
+ host=None,
+ username=None,
+ password=None,
+ port=None,
+ timeout=None,
+ password_prompt=None,
+ original_prompt=None,
+ ):
+ if host is not None:
+ host_system = socket.gethostname()
+ if host_system != host:
+ raise TargetError("Gem5Connection can only connect to gem5 "
+ "simulations on your current host, which "
+ "differs from the one given {}!"
+ .format(host_system, host))
+ if username is not None and username != 'root':
+ raise ValueError('User should be root in gem5!')
+ if password is not None and password != '':
+ raise ValueError('No password needed in gem5!')
+ self.username = 'root'
+ self.is_rooted = True
+ self.password = None
+ self.port = None
+ # Long timeouts to account for gem5 being slow
+ # Can be overriden if the given timeout is longer
+ self.default_timeout = 3600
+ if timeout is not None:
+ if timeout > self.default_timeout:
+ logger.info('Overwriting the default timeout of gem5 ({})'
+ ' to {}'.format(self.default_timeout, timeout))
+ self.default_timeout = timeout
+ else:
+ logger.info('Ignoring the given timeout --> gem5 needs longer timeouts')
+ self.ready_timeout = self.default_timeout * 3
+ # Counterpart in gem5_interact_dir
+ self.gem5_input_dir = '/mnt/host/'
+ # Location of m5 binary in the gem5 simulated system
+ self.m5_path = None
+ # Actual telnet connection to gem5 simulation
+ self.conn = None
+ # Flag to indicate the gem5 device is ready to interact with the
+ # outer world
+ self.ready = False
+ # Lock file to prevent multiple connections to same gem5 simulation
+ # (gem5 does not allow this)
+ self.lock_directory = '/tmp/'
+ self.lock_file_name = None # Will be set once connected to gem5
+
+ # These parameters will be set by either the method to connect to the
+ # gem5 platform or directly to the gem5 simulation
+ # Intermediate directory to push things to gem5 using VirtIO
+ self.gem5_interact_dir = None
+ # Directory to store output from gem5 on the host
+ self.gem5_out_dir = None
+ # Actual gem5 simulation
+ self.gem5simulation = None
+
+ # Connect to gem5
+ if platform:
+ self._connect_gem5_platform(platform)
+
+ # Wait for boot
+ self._wait_for_boot()
+
+ # Mount the virtIO to transfer files in/out gem5 system
+ self._mount_virtio()
+
+ def set_hostinteractdir(self, indir):
+ logger.info('Setting hostinteractdir from {} to {}'
+ .format(self.gem5_input_dir, indir))
+ self.gem5_input_dir = indir
+
+ def push(self, source, dest, timeout=None):
+ """
+ Push a file to the gem5 device using VirtIO
+
+ The file to push to the device is copied to the temporary directory on
+ the host, before being copied within the simulation to the destination.
+ Checks, in the form of 'ls' with error code checking, are performed to
+ ensure that the file is copied to the destination.
+ """
+ # First check if the connection is set up to interact with gem5
+ self._check_ready()
+
+ filename = os.path.basename(source)
+ logger.debug("Pushing {} to device.".format(source))
+ logger.debug("gem5interactdir: {}".format(self.gem5_interact_dir))
+ logger.debug("dest: {}".format(dest))
+ logger.debug("filename: {}".format(filename))
+
+ # We need to copy the file to copy to the temporary directory
+ self._move_to_temp_dir(source)
+
+ # Dest in gem5 world is a file rather than directory
+ if os.path.basename(dest) != filename:
+ dest = os.path.join(dest, filename)
+ # Back to the gem5 world
+ self._gem5_shell("ls -al {}{}".format(self.gem5_input_dir, filename))
+ self._gem5_shell("cat '{}''{}' > '{}'".format(self.gem5_input_dir,
+ filename,
+ dest))
+ self._gem5_shell("sync")
+ self._gem5_shell("ls -al {}".format(dest))
+ self._gem5_shell("ls -al {}".format(self.gem5_input_dir))
+ logger.debug("Push complete.")
+
+ def pull(self, source, dest, timeout=0): #pylint: disable=unused-argument
+ """
+ Pull a file from the gem5 device using m5 writefile
+
+ The file is copied to the local directory within the guest as the m5
+ writefile command assumes that the file is local. The file is then
+ written out to the host system using writefile, prior to being moved to
+ the destination on the host.
+ """
+ # First check if the connection is set up to interact with gem5
+ self._check_ready()
+
+ filename = os.path.basename(source)
+
+ logger.debug("pull_file {} {}".format(source, filename))
+ # We don't check the exit code here because it is non-zero if the source
+ # and destination are the same. The ls below will cause an error if the
+ # file was not where we expected it to be.
+ if os.path.dirname(source) != os.getcwd():
+ self._gem5_shell("cat '{}' > '{}'".format(source, filename))
+ self._gem5_shell("sync")
+ self._gem5_shell("ls -la {}".format(filename))
+ logger.debug('Finished the copy in the simulator')
+ self._gem5_util("writefile {}".format(filename))
+
+ if 'cpu' not in filename:
+ while not os.path.exists(os.path.join(self.gem5_out_dir, filename)):
+ time.sleep(1)
+
+ # Perform the local move
+ shutil.move(os.path.join(self.gem5_out_dir, filename), dest)
+ logger.debug("Pull complete.")
+
+ def execute(self, command, timeout=1000, check_exit_code=True,
+ as_root=False, strip_colors=True):
+ """
+ Execute a command on the gem5 platform
+ """
+ # First check if the connection is set up to interact with gem5
+ self._check_ready()
+
+ output = self._gem5_shell(command, as_root=as_root)
+ if strip_colors:
+ output = strip_bash_colors(output)
+ return output
+
+ def background(self, command, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, as_root=False):
+ # First check if the connection is set up to interact with gem5
+ self._check_ready()
+
+ # Create the logfile for stderr/stdout redirection
+ command_name = command.split(' ')[0].split('/')[-1]
+ redirection_file = 'BACKGROUND_{}.log'.format(command_name)
+ trial = 0
+ while os.path.isfile(redirection_file):
+ # Log file already exists so add to name
+ redirection_file = 'BACKGROUND_{}{}.log'.format(command_name, trial)
+ trial += 1
+
+ # Create the command to pass on to gem5 shell
+ complete_command = '{} >> {} 2>&1 &'.format(command, redirection_file)
+ output = self._gem5_shell(complete_command, as_root=as_root)
+ output = strip_bash_colors(output)
+ gem5_logger.info('STDERR/STDOUT of background command will be '
+ 'redirected to {}. Use target.pull() to '
+ 'get this file'.format(redirection_file))
+ return output
+
+ def close(self):
+ """
+ Close and disconnect from the gem5 simulation. Additionally, we remove
+ the temporary directory used to pass files into the simulation.
+ """
+ gem5_logger.info("Gracefully terminating the gem5 simulation.")
+ try:
+ self._gem5_util("exit")
+ self.gem5simulation.wait()
+ except EOF:
+ pass
+ gem5_logger.info("Removing the temporary directory")
+ try:
+ shutil.rmtree(self.gem5_interact_dir)
+ except OSError:
+ gem5_logger.warn("Failed to remove the temporary directory!")
+
+ # Delete the lock file
+ os.remove(self.lock_file_name)
+
+ # Functions only to be called by the Gem5 connection itself
+ def _connect_gem5_platform(self, platform):
+ port = platform.gem5_port
+ gem5_simulation = platform.gem5
+ gem5_interact_dir = platform.gem5_interact_dir
+ gem5_out_dir = platform.gem5_out_dir
+
+ self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir)
+
+ # This function connects to the gem5 simulation
+ def connect_gem5(self, port, gem5_simulation, gem5_interact_dir,
+ gem5_out_dir):
+ """
+ Connect to the telnet port of the gem5 simulation.
+
+ We connect, and wait for the prompt to be found. We do not use a timeout
+ for this, and wait for the prompt in a while loop as the gem5 simulation
+ can take many hours to reach a prompt when booting the system. We also
+ inject some newlines periodically to try and force gem5 to show a
+ prompt. Once the prompt has been found, we replace it with a unique
+ prompt to ensure that we are able to match it properly. We also disable
+ the echo as this simplifies parsing the output when executing commands
+ on the device.
+ """
+ host = socket.gethostname()
+ gem5_logger.info("Connecting to the gem5 simulation on port {}".format(port))
+
+ # Check if there is no on-going connection yet
+ lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port)
+ if os.path.isfile(lock_file_name):
+ # There is already a connection to this gem5 simulation
+ raise TargetError('There is already a connection to the gem5 '
+ 'simulation using port {} on {}!'
+ .format(port, host))
+
+ # Connect to the gem5 telnet port. Use a short timeout here.
+ attempts = 0
+ while attempts < 10:
+ attempts += 1
+ try:
+ self.conn = TelnetPxssh(original_prompt=None)
+ self.conn.login(host, self.username, port=port,
+ login_timeout=10, auto_prompt_reset=False)
+ break
+ except pxssh.ExceptionPxssh:
+ pass
+ else:
+ gem5_simulation.kill()
+ raise TargetError("Failed to connect to the gem5 telnet session.")
+
+ gem5_logger.info("Connected! Waiting for prompt...")
+
+ # Create the lock file
+ self.lock_file_name = lock_file_name
+ open(self.lock_file_name, 'w').close() # Similar to touch
+ gem5_logger.info("Created lock file {} to prevent reconnecting to "
+ "same simulation".format(self.lock_file_name))
+
+ # We need to find the prompt. It might be different if we are resuming
+ # from a checkpoint. Therefore, we test multiple options here.
+ prompt_found = False
+ while not prompt_found:
+ try:
+ self._login_to_device()
+ except TIMEOUT:
+ pass
+ try:
+ # Try and force a prompt to be shown
+ self.conn.send('\n')
+ self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60)
+ prompt_found = True
+ except TIMEOUT:
+ pass
+
+ gem5_logger.info("Successfully logged in")
+ gem5_logger.info("Setting unique prompt...")
+
+ self.conn.set_unique_prompt()
+ self.conn.prompt()
+ gem5_logger.info("Prompt found and replaced with a unique string")
+
+ # We check that the prompt is what we think it should be. If not, we
+ # need to update the regex we use to match.
+ self._find_prompt()
+
+ self.conn.setecho(False)
+ self._sync_gem5_shell()
+
+ # Fully connected to gem5 simulation
+ self.gem5_interact_dir = gem5_interact_dir
+ self.gem5_out_dir = gem5_out_dir
+ self.gem5simulation = gem5_simulation
+
+ # Ready for interaction now
+ self.ready = True
+
+ def _login_to_device(self):
+ """
+ Login to device, will be overwritten if there is an actual login
+ """
+ pass
+
+ def _find_prompt(self):
+ prompt = r'\[PEXPECT\][\\\$\#]+ '
+ synced = False
+ while not synced:
+ self.conn.send('\n')
+ i = self.conn.expect([prompt, self.conn.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.default_timeout)
+ if i == 0:
+ synced = True
+ elif i == 1:
+ prompt = self.conn.UNIQUE_PROMPT
+ synced = True
+ else:
+ prompt = re.sub(r'\$', r'\\\$', self.conn.before.strip() + self.conn.after.strip())
+ prompt = re.sub(r'\#', r'\\\#', prompt)
+ prompt = re.sub(r'\[', r'\[', prompt)
+ prompt = re.sub(r'\]', r'\]', prompt)
+
+ self.conn.PROMPT = prompt
+
+ def _sync_gem5_shell(self):
+ """
+ Synchronise with the gem5 shell.
+
+ Write some unique text to the gem5 device to allow us to synchronise
+ with the shell output. We actually get two prompts so we need to match
+ both of these.
+ """
+ gem5_logger.debug("Sending Sync")
+ self.conn.send("echo \*\*sync\*\*\n")
+ self.conn.expect(r"\*\*sync\*\*", timeout=self.default_timeout)
+ self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
+ self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
+
+ def _gem5_util(self, command):
+ """ Execute a gem5 utility command using the m5 binary on the device """
+ if self.m5_path is None:
+ raise TargetError('Path to m5 binary on simulated system is not set!')
+ self._gem5_shell('{} {}'.format(self.m5_path, command))
+
+ def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True): # pylint: disable=R0912
+ """
+ Execute a command in the gem5 shell
+
+ This wraps the telnet connection to gem5 and processes the raw output.
+
+ This method waits for the shell to return, and then will try and
+ separate the output from the command from the command itself. If this
+ fails, warn, but continue with the potentially wrong output.
+
+ The exit code is also checked by default, and non-zero exit codes will
+ raise a TargetError.
+ """
+ if sync:
+ self._sync_gem5_shell()
+
+ gem5_logger.debug("gem5_shell command: {}".format(command))
+
+ # Send the actual command
+ self.conn.send("{}\n".format(command))
+
+ # Wait for the response. We just sit here and wait for the prompt to
+ # appear, as gem5 might take a long time to provide the output. This
+ # avoids timeout issues.
+ command_index = -1
+ while command_index == -1:
+ if self.conn.prompt():
+ output = re.sub(r' \r([^\n])', r'\1', self.conn.before)
+ output = re.sub(r'[\b]', r'', output)
+ # Deal with line wrapping
+ output = re.sub(r'[\r].+?<', r'', output)
+ command_index = output.find(command)
+
+ # If we have -1, then we cannot match the command, but the
+ # prompt has returned. Hence, we have a bit of an issue. We
+ # warn, and return the whole output.
+ if command_index == -1:
+ gem5_logger.warn("gem5_shell: Unable to match command in "
+ "command output. Expect parsing errors!")
+ command_index = 0
+
+ output = output[command_index + len(command):].strip()
+
+ # It is possible that gem5 will echo the command. Therefore, we need to
+ # remove that too!
+ command_index = output.find(command)
+ if command_index != -1:
+ output = output[command_index + len(command):].strip()
+
+ gem5_logger.debug("gem5_shell output: {}".format(output))
+
+ # We get a second prompt. Hence, we need to eat one to make sure that we
+ # stay in sync. If we do not do this, we risk getting out of sync for
+ # slower simulations.
+ self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
+
+ if check_exit_code:
+ exit_code_text = self._gem5_shell('echo $?', as_root=as_root,
+ timeout=timeout, check_exit_code=False,
+ sync=False)
+ try:
+ exit_code = int(exit_code_text.split()[0])
+ if exit_code:
+ message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
+ raise TargetError(message.format(exit_code, command, output))
+ except (ValueError, IndexError):
+ gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
+
+ return output
+
+ def _mount_virtio(self):
+ """
+ Mount the VirtIO device in the simulated system.
+ """
+ gem5_logger.info("Mounting VirtIO device in simulated system")
+
+ self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir))
+ mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir)
+ self._gem5_shell(mount_command)
+
+ def _move_to_temp_dir(self, source):
+ """
+ Move a file to the temporary directory on the host for copying to the
+ gem5 device
+ """
+ command = "cp {} {}".format(source, self.gem5_interact_dir)
+ gem5_logger.debug("Local copy command: {}".format(command))
+ subprocess.call(command.split())
+ subprocess.call("sync".split())
+
+ def _check_ready(self):
+ """
+ Check if the gem5 platform is ready
+ """
+ if not self.ready:
+ raise TargetError('Gem5 is not ready to interact yet')
+
+ def _wait_for_boot(self):
+ pass
+
+ def _probe_file(self, filepath):
+ """
+ Internal method to check if the target has a certain file
+ """
+ command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
+ output = self.execute(command.format(filepath), as_root=self.is_rooted)
+ return boolean(output.strip())
+
+
+class LinuxGem5Connection(Gem5Connection):
+
+ def _login_to_device(self):
+ gem5_logger.info("Trying to log in to gem5 device")
+ login_prompt = ['login:', 'AEL login:', 'username:', 'aarch64-gem5 login:']
+ login_password_prompt = ['password:']
+ # Wait for the login prompt
+ prompt = login_prompt + [self.conn.UNIQUE_PROMPT]
+ i = self.conn.expect(prompt, timeout=10)
+ # Check if we are already at a prompt, or if we need to log in.
+ if i < len(prompt) - 1:
+ self.conn.sendline("{}".format(self.username))
+ password_prompt = login_password_prompt + [r'# ', self.conn.UNIQUE_PROMPT]
+ j = self.conn.expect(password_prompt, timeout=self.default_timeout)
+ if j < len(password_prompt) - 2:
+ self.conn.sendline("{}".format(self.password))
+ self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT], timeout=self.default_timeout)
+
+
+
+class AndroidGem5Connection(Gem5Connection):
+
+ def _wait_for_boot(self):
+ """
+ Wait for the system to boot
+
+ We monitor the sys.boot_completed and service.bootanim.exit system
+ properties to determine when the system has finished booting. In the
+ event that we cannot coerce the result of service.bootanim.exit to an
+ integer, we assume that the boot animation was disabled and do not wait
+ for it to finish.
+
+ """
+ gem5_logger.info("Waiting for Android to boot...")
+ while True:
+ booted = False
+ anim_finished = True # Assume boot animation was disabled on except
+ try:
+ booted = (int('0' + self._gem5_shell('getprop sys.boot_completed', check_exit_code=False).strip()) == 1)
+ anim_finished = (int(self._gem5_shell('getprop service.bootanim.exit', check_exit_code=False).strip()) == 1)
+ except ValueError:
+ pass
+ if booted and anim_finished:
+ break
+ time.sleep(60)
+
+ gem5_logger.info("Android booted")
+
def _give_password(password, command):
if not sshpass:
raise HostError('Must have sshpass installed on the host in order to use password-based auth.')