From a447689a86b8d00fcaf957e1858f73f2a20538bd Mon Sep 17 00:00:00 2001 From: Sascha Bischoff Date: Fri, 13 Nov 2015 11:42:21 +0000 Subject: [PATCH] Gem5Device: Refactor into BaseGem5Device, Gem5LinuxDevice and Gem5AndroidDevice - Refactored the Gem5Device to avoid duplicated code. There is now a BaseGem5Device which includes all of the shared functionality. The Gem5LinuxDevice and the Gem5AndroidDevice both inherit from BaseGem5Device, and LinuxDevice or AndroidDevice, respectively. --- wlauto/common/gem5/__init__.py | 16 + wlauto/common/gem5/device.py | 648 ++++++++++++++++++++++++ wlauto/devices/android/gem5/__init__.py | 579 +-------------------- wlauto/devices/linux/gem5/__init__.py | 112 ++++ 4 files changed, 796 insertions(+), 559 deletions(-) create mode 100644 wlauto/common/gem5/__init__.py create mode 100644 wlauto/common/gem5/device.py create mode 100644 wlauto/devices/linux/gem5/__init__.py diff --git a/wlauto/common/gem5/__init__.py b/wlauto/common/gem5/__init__.py new file mode 100644 index 00000000..6300329b --- /dev/null +++ b/wlauto/common/gem5/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2015 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + diff --git a/wlauto/common/gem5/device.py b/wlauto/common/gem5/device.py new file mode 100644 index 00000000..262b706b --- /dev/null +++ b/wlauto/common/gem5/device.py @@ -0,0 +1,648 @@ +# Copyright 2014-2015 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Original implementation by Rene de Jong. Updated by Sascha Bischoff. + +# pylint: disable=E1101 + +import logging +import os +import re +import shutil +import socket +import subprocess +import sys +import tarfile +import time +from pexpect import EOF, TIMEOUT + +from wlauto import settings, Parameter +from wlauto.core import signal as sig +from wlauto.exceptions import DeviceError +from wlauto.utils import ssh, types + + +class BaseGem5Device(object): + """ + Base implementation for a gem5-based device + + This class is used as the base class for OS-specific devices such as the + G3m5LinuxDevice and the Gem5AndroidDevice. The majority of the gem5-specific + functionality is included here. + + Note: When inheriting from this class, make sure to inherit from this class + prior to inheriting from the OS-specific class, i.e. LinuxDevice, to ensure + that the methods are correctly overridden. + """ + # gem5 can be very slow. Hence, we use some very long timeouts! + delay = 3600 + long_delay = 3 * delay + ready_timeout = long_delay + default_timeout = delay + + platform = None + path_module = 'posixpath' + + parameters = [ + Parameter('gem5_binary', kind=str, default='./build/ARM/gem5.fast', + mandatory=False, description="Command used to execute gem5. " + "Adjust according to needs."), + Parameter('gem5_args', kind=types.arguments, mandatory=True, + description="Command line passed to the gem5 simulation. This" + " command line is used to set up the simulated system, and " + "should be the same as used for a standard gem5 simulation " + "without workload automation. Note that this is simulation " + "script specific and will hence need to be tailored to each " + "particular use case."), + Parameter('gem5_vio_args', kind=types.arguments, mandatory=True, + constraint=lambda x: "{}" in str(x), + description="gem5 VirtIO command line used to enable the " + "VirtIO device in the simulated system. At the very least, " + "the root parameter of the VirtIO9PDiod device must be " + "exposed on the command line. Please set this root mount to " + "{}, as it will be replaced with the directory used by " + "Workload Automation at runtime."), + Parameter('temp_dir', kind=str, default='/tmp', + description="Temporary directory used to pass files into the " + "gem5 simulation. Workload Automation will automatically " + "create a directory in this folder, and will remove it again " + "once the simulation completes."), + Parameter('checkpoint', kind=bool, default=False, + mandatory=False, description="This parameter " + "tells Workload Automation to create a checkpoint of the " + "simulated system once the guest system has finished booting." + " This checkpoint can then be used at a later stage by other " + "WA runs to avoid booting the guest system a second time. Set" + " to True to take a checkpoint of the simulated system post " + "boot."), + Parameter('run_delay', kind=int, default=0, mandatory=False, + constraint=lambda x: x >= 0, + description="This sets the time that the " + "system should sleep in the simulated system prior to " + "running and workloads or taking checkpoints. This allows " + "the system to quieten down prior to running the workloads. " + "When this is combined with the checkpoint_post_boot" + " option, it allows the checkpoint to be created post-sleep," + " and therefore the set of workloads resuming from this " + "checkpoint will not be required to sleep.") + ] + + @property + def is_rooted(self): # pylint: disable=R0201 + # gem5 is always rooted + return True + + # pylint: disable=E0203 + def __init__(self, _): + self.logger = logging.getLogger('gem5Device') + + # The gem5 subprocess + self.gem5 = None + self.gem5_port = -1 + self.gem5outdir = os.path.join(settings.output_directory, "gem5") + self.stdout_file = None + self.stderr_file = None + self.stderr_filename = None + self.sckt = None + + # Find the first one that does not exist. Ensures that we do not re-use + # the directory used by someone else. + for i in xrange(sys.maxint): + directory = os.path.join(self.temp_dir, "wa_{}".format(i)) + try: + os.stat(directory) + continue + except OSError: + break + self.temp_dir = directory + self.logger.debug("Using {} as the temporary directory.".format(self.temp_dir)) + + # Start the gem5 simulation when WA starts a run using a signal. + sig.connect(self.init_gem5, sig.RUN_START) + + def validate(self): + # Assemble the virtio args + self.gem5_vio_args = str(self.gem5_vio_args).format(self.temp_dir) # pylint: disable=W0201 + self.logger.debug("gem5 VirtIO command: {}".format(self.gem5_vio_args)) + + def init_gem5(self, _): + """ + Start gem5, find out the telnet port and connect to the simulation. + + We first create the temporary directory used by VirtIO to pass files + into the simulation, as well as the gem5 output directory.We then create + files for the standard output and error for the gem5 process. The gem5 + process then is started. + """ + self.logger.info("Creating temporary directory: {}".format(self.temp_dir)) + os.mkdir(self.temp_dir) + os.mkdir(self.gem5outdir) + + # We need to redirect the standard output and standard error for the + # gem5 process to a file so that we can debug when things go wrong. + f = os.path.join(self.gem5outdir, 'stdout') + self.stdout_file = open(f, 'w') + f = os.path.join(self.gem5outdir, 'stderr') + self.stderr_file = open(f, 'w') + # We need to keep this so we can check which port to use for the telnet + # connection. + self.stderr_filename = f + + self.start_gem5() + + def start_gem5(self): + """ + Starts the gem5 simulator, and parses the output to get the telnet port. + """ + self.logger.info("Starting the gem5 simulator") + + command_line = "{} --outdir={}/gem5 {} {}".format(self.gem5_binary, + settings.output_directory, + self.gem5_args, + self.gem5_vio_args) + self.logger.debug("gem5 command line: {}".format(command_line)) + self.gem5 = subprocess.Popen(command_line.split(), + stdout=self.stdout_file, + stderr=self.stderr_file) + + while self.gem5_port == -1: + # Check that gem5 is running! + if self.gem5.poll(): + raise DeviceError("The gem5 process has crashed with error code {}!".format(self.gem5.poll())) + + # Open the stderr file + f = open(self.stderr_filename, 'r') + for line in f: + m = re.search(r"Listening\ for\ system\ connection\ on\ port\ (?P\d+)", line) + if m: + port = int(m.group('port')) + if port >= 3456 and port < 5900: + self.gem5_port = port + f.close() + break + else: + time.sleep(1) + f.close() + + def connect(self): # pylint: disable=R0912,W0201 + """ + Connect to the gem5 simulation and wait for Android to boot. Then, + create checkpoints, and mount the VirtIO device. + """ + self.connect_gem5() + + self.wait_for_boot() + + if self.run_delay: + self.logger.info("Sleeping for {} seconds in the guest".format(self.run_delay)) + self.gem5_shell("sleep {}".format(self.run_delay)) + + if self.checkpoint: + self.checkpoint_gem5() + + self.mount_virtio() + self.logger.info("Creating the working directory in the simulated system") + self.gem5_shell('mkdir -p {}'.format(self.working_directory)) + self._is_ready = True # pylint: disable=W0201 + + def wait_for_boot(self): + pass + + def connect_gem5(self): # pylint: disable=R0912 + """ + 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. + """ + self.logger.info("Connecting to the gem5 simulation on port {}".format(self.gem5_port)) + host = socket.gethostname() + port = self.gem5_port + + # Connect to the gem5 telnet port. Use a short timeout here. + self.sckt = ssh.TelnetConnection() + self.sckt.login(host, 'None', port=port, auto_prompt_reset=False, + login_timeout=10) + + self.logger.info("Connected! Waiting for prompt...") + + # 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.sckt.send('\n') + self.sckt.expect([r'# ', self.sckt.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60) + prompt_found = True + except TIMEOUT: + pass + + self.logger.info("Setting unique prompt...") + + self.sckt.set_unique_prompt() + self.sckt.prompt() + self.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.sckt.setecho(False) + self.sync_gem5_shell() + + # Try and avoid line wrapping as much as possible. Don't check the error + # codes from these command because some of them WILL fail. + self.gem5_shell('stty columns 1024', check_exit_code=False) + self.gem5_shell('busybox stty columns 1024', check_exit_code=False) + self.gem5_shell('stty cols 1024', check_exit_code=False) + self.gem5_shell('busybox stty cols 1024', check_exit_code=False) + self.gem5_shell('reset', check_exit_code=False) + + def get_properties(self, context): # pylint: disable=R0801 + """ Get the property files from the device """ + for propfile in self.property_files: + try: + normname = propfile.lstrip(self.path.sep).replace(self.path.sep, '.') + outfile = os.path.join(context.host_working_directory, normname) + if self.is_file(propfile): + self.execute('cat {} > {}'.format(propfile, normname)) + self.pull_file(normname, outfile) + elif self.is_directory(propfile): + self.get_directory(context, propfile) + continue + else: + continue + except DeviceError: + # We pull these files "opportunistically", so if a pull fails + # (e.g. we don't have permissions to read the file), just note + # it quietly (not as an error/warning) and move on. + self.logger.debug('Could not pull property file "{}"'.format(propfile)) + return {} + + def get_directory(self, context, directory): + """ Pull a directory from the device """ + normname = directory.lstrip(self.path.sep).replace(self.path.sep, '.') + outdir = os.path.join(context.host_working_directory, normname) + temp_file = os.path.join(context.host_working_directory, "{}.tar".format(normname)) + # Check that the folder exists + self.gem5_shell("ls -la {}".format(directory)) + # Compress the folder + try: + self.gem5_shell("{} tar -cvf {}.tar {}".format(self.busybox, normname, directory)) + except DeviceError: + self.logger.debug("Failed to run tar command on device! Not pulling {}".format(directory)) + return + self.pull_file(normname, temp_file) + f = tarfile.open(temp_file, 'r') + os.mkdir(outdir) + f.extractall(outdir) + os.remove(temp_file) + + def get_pids_of(self, process_name): + """ Returns a list of PIDs of all processes with the specified name. """ + result = self.gem5_shell('ps | busybox grep {}'.format(process_name), + check_exit_code=False).strip() + if result and 'not found' not in result and len(result.split('\n')) > 2: + return [int(x.split()[1]) for x in result.split('\n')] + else: + return [] + + def find_prompt(self): + prompt = r'\[PEXPECT\][\\\$\#]+ ' + synced = False + while not synced: + self.sckt.send('\n') + i = self.sckt.expect([prompt, self.sckt.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.delay) + if i == 0: + synced = True + elif i == 1: + prompt = self.sckt.UNIQUE_PROMPT + synced = True + else: + prompt = re.sub(r'\$', r'\\\$', self.sckt.before.strip() + self.sckt.after.strip()) + prompt = re.sub(r'\#', r'\\\#', prompt) + prompt = re.sub(r'\[', r'\[', prompt) + prompt = re.sub(r'\]', r'\]', prompt) + + self.sckt.PROMPT = prompt + + def login(self): + pass + + def close(self): + if self._logcat_poller: + self._logcat_poller.stop() + + def reset(self): + self.logger.warn("Attempt to restart the gem5 device. This is not " + "supported!") + + def init(self): + pass + + def push_file(self, source, dest, _): + """ + 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. + """ + filename = os.path.basename(source) + self.logger.debug("Pushing {} to device.".format(source)) + self.logger.debug("temp_dir: {}".format(self.temp_dir)) + self.logger.debug("dest: {}".format(dest)) + self.logger.debug("filename: {}".format(filename)) + + # We need to copy the file to copy to the temporary directory + self.move_to_temp_dir(source) + + # Back to the gem5 world + self.gem5_shell("ls -al /mnt/obb/{}".format(filename)) + self.gem5_shell("busybox cp /mnt/obb/{} {}".format(filename, dest)) + self.gem5_shell("busybox sync") + self.gem5_shell("ls -al {}".format(dest)) + self.gem5_shell("ls -al /mnt/obb/") + self.logger.debug("Push complete.") + + def pull_file(self, source, dest, _): + """ + Pull a file from the gem5 device using m5 writefile + + First, we check the extension of the file to be copied. If the file ends + in .gz, then gem5 wrongly assumes that it should create a gzipped output + stream, which results in a gem5 error. Therefore, we rename the file on + the local device prior to the writefile command when required. Next, 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. + """ + filename = os.path.basename(source) + self.logger.debug("Pulling {} from device.".format(filename)) + + # gem5 assumes that files ending in .gz are gzip-compressed. We need to + # work around this, else gem5 panics on us. Rename the file for use in + # gem5 + if filename[-3:] == '.gz': + filename += '.fugem5' + + self.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. + self.gem5_shell("busybox cp {} {}".format(source, filename), check_exit_code=False) + self.gem5_shell("busybox sync") + self.gem5_shell("ls -la {}".format(filename)) + self.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.gem5outdir, filename)): + time.sleep(1) + + # Perform the local move + shutil.move(os.path.join(self.gem5outdir, filename), dest) + self.logger.debug("Pull complete.") + + def delete_file(self, filepath, _): + """ Delete a file on the device """ + self._check_ready() + self.gem5_shell("rm '{}'".format(filepath)) + + def file_exists(self, filepath): + """ Check if a file exists """ + self._check_ready() + output = self.gem5_shell('if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath)) + try: + if int(output): + return True + else: + return False + except ValueError: + # If we cannot process the output, assume that there is no file + return False + + def disconnect(self): + """ + Close and disconnect from the gem5 simulation. Additionally, we remove + the temporary directory used to pass files into the simulation. + """ + self.logger.info("Gracefully terminating the gem5 simulation.") + try: + self.gem5_util("exit") + self.gem5.wait() + except EOF: + pass + self.logger.info("Removing the temporary directory") + try: + shutil.rmtree(self.temp_dir) + except OSError: + self.logger.warn("Failed to remove the temporary directory!") + + # gem5 might be slow. Hence, we need to make the ping timeout very long. + def ping(self): + self.logger.debug("Pinging gem5 to see if it is still alive") + self.gem5_shell('ls /', timeout=self.longdelay) + + # Additional Android-specific methods. + def forward_port(self, _): # pylint: disable=R0201 + raise DeviceError('we do not need forwarding') + + # gem5 should dump out a framebuffer. We can use this if it exists. Failing + # that, fall back to the parent class implementation. + def capture_screen(self, filepath): + file_list = os.listdir(self.gem5outdir) + screen_caps = [] + for f in file_list: + if '.bmp' in f: + screen_caps.append(f) + + if len(screen_caps) == 1: + # Bail out if we do not have image, and resort to the slower, built + # in method. + try: + import Image + gem5_image = os.path.join(self.gem5outdir, screen_caps[0]) + temp_image = os.path.join(self.gem5outdir, "file.png") + im = Image.open(gem5_image) + im.save(temp_image, "PNG") + shutil.copy(temp_image, filepath) + os.remove(temp_image) + self.logger.debug("capture_screen: using gem5 screencap") + return True + except (shutil.Error, ImportError, IOError): + pass + return False + + # pylint: disable=W0613 + def execute(self, command, timeout=1000, check_exit_code=True, background=False, + as_root=False, busybox=False, **kwargs): + self._check_ready() + if as_root and not self.is_rooted: + raise DeviceError('Attempting to execute "{}" as root on unrooted device.'.format(command)) + if busybox: + if not self.is_rooted: + raise DeviceError('Attempting to execute "{}" with busybox. '.format(command) + + 'Busybox can only be deployed to rooted devices.') + command = ' '.join([self.busybox, command]) + if background: + self.logger.debug("Attempt to execute in background. Not supported " + "in gem5, hence ignored.") + return self.gem5_shell(command, as_root=as_root) + + # Internal methods: do not use outside of the class. + + def _check_ready(self): + """ + Check if the device is ready. + + As this is gem5, we just assume that the device is ready once we have + connected to the gem5 simulation, and updated the prompt. + """ + if not self._is_ready: + raise DeviceError('Device not ready.') + + 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 DeviceError. + """ + conn = self.sckt + if sync: + self.sync_gem5_shell() + + self.logger.debug("gem5_shell command: {}".format(command)) + + # Send the actual command + 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 conn.prompt(): + output = re.sub(r' \r([^\n])', r'\1', 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: + self.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() + + self.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.sckt.expect([self.sckt.UNIQUE_PROMPT, self.sckt.PROMPT], timeout=self.delay) + + 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 DeviceError(message.format(exit_code, command, output)) + except (ValueError, IndexError): + self.logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text)) + + return output + + def gem5_util(self, command): + """ Execute a gem5 utility command using the m5 binary on the device """ + self.gem5_shell('/sbin/m5 ' + command) + + 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. + """ + self.logger.debug("Sending Sync") + self.sckt.send("echo \*\*sync\*\*\n") + self.sckt.expect(r"\*\*sync\*\*", timeout=self.delay) + self.sckt.expect([self.sckt.UNIQUE_PROMPT, self.sckt.PROMPT], timeout=self.delay) + self.sckt.expect([self.sckt.UNIQUE_PROMPT, self.sckt.PROMPT], timeout=self.delay) + + 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.temp_dir) + self.logger.debug("Local copy command: {}".format(command)) + subprocess.call(command.split()) + subprocess.call("sync".split()) + + def checkpoint_gem5(self, end_simulation=False): + """ Checkpoint the gem5 simulation, storing all system state """ + self.logger.info("Taking a post-boot checkpoint") + self.gem5_util("checkpoint") + if end_simulation: + self.disconnect() + + def mount_virtio(self): + """ + Mount the VirtIO device in the simulated system. + """ + self.logger.info("Mounting VirtIO device in simulated system") + + self.gem5_shell('busybox mkdir -p /mnt/obb') + + mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 /mnt/obb".format(self.temp_dir) + if self.platform == 'linux': + self.gem5_shell(mount_command) + else: + self.gem5_shell('busybox {}'.format(mount_command)) diff --git a/wlauto/devices/android/gem5/__init__.py b/wlauto/devices/android/gem5/__init__.py index 7eb3303e..cbc60de8 100644 --- a/wlauto/devices/android/gem5/__init__.py +++ b/wlauto/devices/android/gem5/__init__.py @@ -17,22 +17,14 @@ import logging import os -import re -import shutil -import socket -import subprocess -import sys -import tarfile import time -from pexpect import EOF, TIMEOUT -from wlauto import AndroidDevice, settings, Parameter -from wlauto.core import signal as sig -from wlauto.utils import ssh, types -from wlauto.exceptions import DeviceError, ConfigError +from wlauto import AndroidDevice, Parameter +from wlauto.common.gem5.device import BaseGem5Device +from wlauto.exceptions import DeviceError -class Gem5Device(AndroidDevice): +class Gem5AndroidDevice(BaseGem5Device, AndroidDevice): """ Implements gem5 Android device. @@ -84,176 +76,31 @@ class Gem5Device(AndroidDevice): """ name = 'gem5_android' - has_gpu = False platform = 'android' - path_module = 'posixpath' - - # gem5 can be very slow. Hence, we use some very long timeouts! - delay = 3600 - long_delay = 3 * delay - ready_timeout = long_delay - default_timeout = delay parameters = [ Parameter('core_names', default=[], override=True), Parameter('core_clusters', default=[], override=True), - Parameter('gem5_binary', kind=str, default='./build/ARM/gem5.fast', - mandatory=False, description="Command used to execute gem5. " - "Adjust according to needs."), - Parameter('gem5_args', kind=types.arguments, mandatory=True, - description="Command line passed to the gem5 simulation. This" - " command line is used to set up the simulated system, and " - "should be the same as used for a standard gem5 simulation " - "without workload automation. Note that this is simulation " - "script specific and will hence need to be tailored to each " - "particular use case."), - Parameter('gem5_vio_args', kind=types.arguments, mandatory=True, - constraint=lambda x: "{}" in str(x), - description="gem5 VirtIO command line used to enable the " - "VirtIO device in the simulated system. At the very least, " - "the root parameter of the VirtIO9PDiod device must be " - "exposed on the command line. Please set this root mount to " - "{}, as it will be replaced with the directory used by " - "Workload Automation at runtime."), - Parameter('temp_dir', kind=str, default='/tmp', - description="Temporary directory used to pass files into the " - "gem5 simulation. Workload Automation will automatically " - "create a directory in this folder, and will remove it again " - "once the simulation completes."), - Parameter('checkpoint', kind=bool, default=False, - mandatory=False, description="This parameter " - "tells Workload Automation to create a checkpoint of the " - "simulated system once the guest system has finished booting." - " This checkpoint can then be used at a later stage by other " - "WA runs to avoid booting the guest system a second time. Set" - " to True to take a checkpoint of the simulated system post " - "boot."), - Parameter('run_delay', kind=int, default=0, mandatory=False, - constraint=lambda x: x >= 0, - description="This sets the time that the " - "system should sleep in the simulated system prior to " - "running and workloads or taking checkpoints. This allows " - "the system to quieten down prior to running the workloads. " - "When this is combined with the checkpoint_post_boot" - " option, it allows the checkpoint to be created post-sleep," - " and therefore the set of workloads resuming from this " - "checkpoint will not be required to sleep.") ] # Overwritten from Device. For documentation, see corresponding method in # Device. - @property - def is_rooted(self): - # gem5 is always rooted - return True - def __init__(self, **kwargs): - self.logger = logging.getLogger('gem5Device') + self.logger = logging.getLogger('Gem5AndroidDevice') + AndroidDevice.__init__(self, **kwargs) + BaseGem5Device.__init__(self, **kwargs) - super(Gem5Device, self).__init__(**kwargs) - - # The gem5 subprocess - self.gem5 = None - self.gem5_port = -1 - self.gem5outdir = os.path.join(settings.output_directory, "gem5") - self.stdout_file = None - self.stderr_file = None - self.stderr_filename = None - self.checkpoint = False - self.sckt = None - - # Find the first one that does not exist. Ensures that we do not re-use - # the directory used by someone else. - for i in xrange(sys.maxint): - directory = os.path.join(self.temp_dir, "wa_{}".format(i)) - try: - os.stat(directory) - continue - except OSError: - break - self.temp_dir = directory - self.logger.debug("Using {} as the temporary directory.".format(self.temp_dir)) - - # Start the gem5 simulation when WA starts a run using a signal. - sig.connect(self.init_gem5, sig.RUN_START) - - def validate(self): - # Assemble the virtio args - self.gem5_vio_args = str(self.gem5_vio_args).format(self.temp_dir) - self.logger.debug("gem5 VirtIO command: {}".format(self.gem5_vio_args)) - - def init_gem5(self, _): - """ - Start gem5, find out the telnet port and connect to the simulation. - - We first create the temporary directory used by VirtIO to pass files - into the simulation, as well as the gem5 output directory.We then create - files for the standard output and error for the gem5 process. The gem5 - process then is started. - """ - self.logger.info("Creating temporary directory: {}".format(self.temp_dir)) - os.mkdir(self.temp_dir) - os.mkdir(self.gem5outdir) - - # We need to redirect the standard output and standard error for the - # gem5 process to a file so that we can debug when things go wrong. - f = os.path.join(self.gem5outdir, 'stdout') - self.stdout_file = open(f, 'w') - f = os.path.join(self.gem5outdir, 'stderr') - self.stderr_file = open(f, 'w') - # We need to keep this so we can check which port to use for the telnet - # connection. - self.stderr_filename = f - - self.start_gem5() - - def start_gem5(self): - """ - Starts the gem5 simulator, and parses the output to get the telnet port. - """ - self.logger.info("Starting the gem5 simulator") - - command_line = "{} --outdir={}/gem5 {} {}".format(self.gem5_binary, - settings.output_directory, - self.gem5_args, - self.gem5_vio_args) - self.logger.debug("gem5 command line: {}".format(command_line)) - self.gem5 = subprocess.Popen(command_line.split(), - stdout=self.stdout_file, - stderr=self.stderr_file) - - while self.gem5_port == -1: - # Check that gem5 is running! - if self.gem5.poll(): - raise DeviceError("The gem5 process has crashed with error code {}!".format(self.gem5.poll())) - - # Open the stderr file - f = open(self.stderr_filename, 'r') - for line in f: - m = re.search(r"Listening\ for\ system\ connection\ on\ port\ (?P\d+)", line) - if m: - port = int(m.group('port')) - if port >= 3456 and port < 5900: - self.gem5_port = port - f.close() - break - else: - time.sleep(1) - f.close() - - def connect(self): # pylint: disable=R0912 - """ - Connect to the gem5 simulation and wait for Android to boot. Then, - create checkpoints, and mount the VirtIO device. - """ - self.connect_gem5() + def login_to_device(self): + pass + def wait_for_boot(self): self.logger.info("Waiting for Android to boot...") while True: try: - available = (1 == int('0' + self.gem5_shell('getprop sys.boot_completed', check_exit_code=False))) - if available: + booted = (1 == int('0' + self.gem5_shell('getprop sys.boot_completed', check_exit_code=False))) + anim_finished = (1 == int('0' + self.gem5_shell('getprop service.bootanim.exit', check_exit_code=False))) + if booted and anim_finished: break except (DeviceError, ValueError): pass @@ -261,176 +108,7 @@ class Gem5Device(AndroidDevice): self.logger.info("Android booted") - if self.run_delay: - self.logger.info("Sleeping for {} seconds in the guest".format(self.run_delay)) - self.gem5_shell("sleep {}".format(self.run_delay)) - - if self.checkpoint: - self.checkpoint_gem5() - - self.mount_virtio() - self.logger.info("Creating the working directory in the simulated system") - self.gem5_shell('mkdir -p {}'.format(self.working_directory)) - self._is_ready = True - - def connect_gem5(self): # pylint: disable=R0912 - """ - 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. - """ - self.logger.info("Connecting to the gem5 simulation on port {}".format(self.gem5_port)) - host = socket.gethostname() - port = self.gem5_port - - # Connect to the gem5 telnet port. Use a short timeout here. - self.sckt = ssh.TelnetConnection() - self.sckt.login(host, 'None', port=port, auto_prompt_reset=False, - login_timeout=10) - - self.logger.info("Connected! Waiting for prompt...") - - # 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: - # Try and force a prompt to be shown - self.sckt.send('\n') - self.sckt.expect([r'# ', self.sckt.UNIQUE_PROMPT], timeout=self.delay) - prompt_found = True - except TIMEOUT: - pass - - self.logger.info("Setting unique prompt...") - - self.sckt.set_unique_prompt() - self.sckt.prompt() - self.logger.info("Prompt found and replaced with a unique string") - - self.sckt.setecho(False) - self.sync_gem5_shell() - - def disconnect(self): - """ - Close and disconnect from the gem5 simulation. Additionally, we remove - the temporary directory used to pass files into the simulation. - """ - self.logger.info("Gracefully terminating the gem5 simulation.") - try: - self.gem5_util("exit") - self.gem5.wait() - except EOF: - pass - self.logger.info("Removing the temporary directory") - try: - shutil.rmtree(self.temp_dir) - except OSError: - self.logger.warn("Failed to remove the temporary directory!") - - def close(self): - if self._logcat_poller: - self._logcat_poller.stop() - - def reset(self): - self.logger.warn("Attempt to restart the gem5 device. This is not " - "supported!") - - def init(self): - pass - - def push_file(self, source, dest, as_root=False): # pylint: disable=W0221 - """ - 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. - """ - filename = os.path.basename(source) - self.logger.debug("Pushing {} to device.".format(source)) - self.logger.debug("temp_dir: {}".format(self.temp_dir)) - self.logger.debug("dest: {}".format(dest)) - self.logger.debug("filename: {}".format(filename)) - - # We need to copy the file to copy to the temporary directory - self.move_to_temp_dir(source) - - # Back to the gem5 world - self.gem5_shell("ls -al /mnt/obb/{}".format(filename)) - self.gem5_shell("busybox cp /mnt/obb/{} {}".format(filename, dest)) - self.gem5_shell("busybox sync") - self.gem5_shell("ls -al {}".format(dest)) - self.gem5_shell("ls -al /mnt/obb/") - self.logger.debug("Push complete.") - - def pull_file(self, source, dest, as_root=False): # pylint: disable=W0221 - """ - Pull a file from the gem5 device using m5 writefile - - First, we check the extension of the file to be copied. If the file ends - in .gz, then gem5 wrongly assumes that it should create a gzipped output - stream, which results in a gem5 error. Therefore, we rename the file on - the local device prior to the writefile command when required. Next, 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. - """ - filename = source.split('/')[-1] - self.logger.debug("Pulling {} from device.".format(filename)) - - # gem5 assumes that files ending in .gz are gzip-compressed. We need to - # work around this, else gem5 panics on us. Rename the file for use in - # gem5 - if filename[-3:] == '.gz': - filename += '.fugem5' - - self.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. - self.gem5_shell("busybox cp {} {}".format(source, filename), check_exit_code=False) - self.gem5_shell("busybox sync") - self.gem5_shell("ls -la {}".format(filename)) - self.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.gem5outdir, filename)): - time.sleep(1) - - # Perform the local move - shutil.move(os.path.join(self.gem5outdir, filename), dest) - self.logger.debug("Pull complete.") - - def delete_file(self, filepath, as_root=False): # pylint: disable=W0221 - """ Delete a file on the device """ - self._check_ready() - self.gem5_shell("rm '{}'".format(filepath)) - - def file_exists(self, filepath): - """ Check if a file exists """ - self._check_ready() - output = self.gem5_shell('if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath)) - try: - if int(output): - return True - else: - return False - except ValueError: - # If we cannot process the output, assume that there is no file - return False - - def install(self, filepath, timeout=default_timeout): # pylint: disable=W0221 + def install(self, filepath, timeout=3 * 3600): # pylint: disable=W0221 """ Install an APK or a normal executable """ ext = os.path.splitext(filepath)[1].lower() if ext == '.apk': @@ -438,7 +116,7 @@ class Gem5Device(AndroidDevice): else: return self.install_executable(filepath) - def install_apk(self, filepath, timeout=default_timeout): # pylint: disable=W0221 + def install_apk(self, filepath, timeout=3 * 3600): # pylint: disable=W0221 """ Install an APK on the gem5 device @@ -476,21 +154,6 @@ class Gem5Device(AndroidDevice): self._check_ready() self.gem5_shell("pm uninstall {}".format(package)) - def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False, - as_root=False, busybox=False, **kwargs): - self._check_ready() - if as_root and not self.is_rooted: - raise DeviceError('Attempting to execute "{}" as root on unrooted device.'.format(command)) - if busybox: - if not self.is_rooted: - raise DeviceError('Attempting to execute "{}" with busybox. '.format(command) + - 'Busybox can only be deployed to rooted devices.') - command = ' '.join([self.busybox, command]) - if background: - self.logger.debug("Attempt to execute in background. Not supported " - "in gem5, hence ignored.") - return self.gem5_shell(command, as_root=as_root) - def dump_logcat(self, outfile, filter_spec=None): """ Extract logcat from simulation """ self.logger.info("Extracting logcat from the simulated system") @@ -519,55 +182,9 @@ class Gem5Device(AndroidDevice): def get_properties(self, context): # pylint: disable=R0801 """ Get the property files from the device """ - for propfile in self.property_files: - try: - normname = propfile.lstrip(self.path.sep).replace(self.path.sep, '.') - outfile = os.path.join(context.host_working_directory, normname) - if self.is_file(propfile): - self.execute('cat {} > {}'.format(propfile, normname)) - self.pull_file(normname, outfile) - elif self.is_directory(propfile): - self.get_directory(context, propfile) - continue - else: - continue - except DeviceError: - # We pull these files "opportunistically", so if a pull fails - # (e.g. we don't have permissions to read the file), just note - # it quietly (not as an error/warning) and move on. - self.logger.debug('Could not pull property file "{}"'.format(propfile)) - - # As we do not call the super class, we explicitly need to - # process the Android properties. + BaseGem5Device.get_properties(self, context) props = self._get_android_properties(context) - - def get_directory(self, context, directory): - """ Pull a directory from the device """ - normname = directory.lstrip(self.path.sep).replace(self.path.sep, '.') - outdir = os.path.join(context.host_working_directory, normname) - temp_file = os.path.join(context.host_working_directory, "{}.tar".format(normname)) - # Check that the folder exists - self.gem5_shell("ls -la {}".format(directory)) - # Compress the folder - try: - self.gem5_shell("{} tar -cvf {}.tar {}".format(self.busybox, normname, directory)) - except DeviceError: - self.logger.debug("Failed to run tar command on device! Not pulling {}".format(directory)) - return - self.pull_file(normname, temp_file) - f = tarfile.open(temp_file, 'r') - os.mkdir(outdir) - f.extractall(outdir) - os.remove(temp_file) - - def get_pids_of(self, process_name): - """ Returns a list of PIDs of all processes with the specified name. """ - result = self.gem5_shell('ps | busybox grep {}'.format(process_name), - check_exit_code=False).strip() - if result and 'not found' not in result and len(result.split('\n')) > 2: - return [int(x.split()[1]) for x in result.split('\n')] - else: - return [] + return props def disable_screen_lock(self): """ @@ -579,166 +196,10 @@ class Gem5Device(AndroidDevice): sqlcommand = "update locksettings set value=\'0\' where name=\'screenlock.disabled\';" self.execute('sqlite3 {} "{}"'.format(lockdb, sqlcommand), as_root=True) - # gem5 should dump out a framebuffer. We can use this if it exists. Failing - # that, fall back to the parent class implementation. def capture_screen(self, filepath): - file_list = os.listdir(self.gem5outdir) - screen_caps = [] - for f in file_list: - if '.bmp' in f: - screen_caps.append(f) - - if len(screen_caps) == 1: - # Bail out if we do not have image, and resort to the slower, built - # in method. - try: - import Image - gem5_image = os.path.join(self.gem5outdir, screen_caps[0]) - temp_image = os.path.join(self.gem5outdir, "file.png") - im = Image.open(gem5_image) - im.save(temp_image, "PNG") - shutil.copy(temp_image, filepath) - os.remove(temp_image) - self.logger.debug("capture_screen: using gem5 screencap") - return - except (shutil.Error, ImportError, IOError): - pass + if BaseGem5Device.capture_screen(self, filepath): + return # If we didn't manage to do the above, call the parent class. self.logger.warning("capture_screen: falling back to parent class implementation") - super(Gem5Device, self).capture_screen(filepath) - - # gem5 might be slow. Hence, we need to make the ping timeout very long. - def ping(self): - self.logger.debug("Pinging gem5 to see if it is still alive") - self.gem5_shell('ls /', timeout=self.longdelay) - - # Additional Android-specific methods. - def forward_port(self, from_port, to_port): - self.logger.warning('Tried to forward port. We do not need any forwarding.') - - # Internal methods: do not use outside of the class. - - def _check_ready(self): - """ - Check if the device is ready. - - As this is gem5, we just assume that the device is ready once we have - connected to the gem5 simulation, and updated the prompt. - """ - if not self._is_ready: - raise DeviceError('Device not ready.') - - 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 DeviceError. - """ - conn = self.sckt - if sync: - self.sync_gem5_shell() - - self.logger.debug("gem5_shell command: {}".format(command)) - - # Send the actual command - 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 conn.prompt(): - output = re.sub(r' \r([^\n])', r'\1', 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: - self.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() - - self.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.sckt.expect(self.sckt.UNIQUE_PROMPT, timeout=10000) - - 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 DeviceError(message.format(exit_code, command, output)) - except (ValueError, IndexError): - self.logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text)) - - return output - - def gem5_util(self, command): - """ Execute a gem5 utility command using the m5 binary on the device """ - self.gem5_shell('/sbin/m5 ' + command) - - 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. - """ - self.sckt.send("echo \*\*sync\*\*\n") - self.sckt.expect(r"\*\*sync\*\*", timeout=self.delay) - self.sckt.expect(self.sckt.UNIQUE_PROMPT, timeout=self.delay) - self.sckt.expect(self.sckt.UNIQUE_PROMPT, timeout=self.delay) - - 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.temp_dir) - self.logger.debug("Local copy command: {}".format(command)) - subprocess.call(command.split()) - subprocess.call("sync".split()) - - def checkpoint_gem5(self, end_simulation=False): - """ Checkpoint the gem5 simulation, storing all system state """ - self.logger.info("Taking a post-boot checkpoint") - self.gem5_util("checkpoint") - if end_simulation: - self.disconnect() - - def mount_virtio(self): - """ - Mount the VirtIO device in the simulated system. - """ - self.logger.info("Mounting VirtIO device in simulated system") - - mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 /mnt/obb".format(self.temp_dir) - self.gem5_shell('busybox {}'.format(mount_command)) + AndroidDevice.capture_screen(self, filepath) diff --git a/wlauto/devices/linux/gem5/__init__.py b/wlauto/devices/linux/gem5/__init__.py new file mode 100644 index 00000000..9a8a76c7 --- /dev/null +++ b/wlauto/devices/linux/gem5/__init__.py @@ -0,0 +1,112 @@ +# Copyright 2014-2015 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Original implementation by Rene de Jong. Updated by Sascha Bischoff. + +import logging + +from wlauto import LinuxDevice, Parameter +from wlauto.common.gem5.device import BaseGem5Device + + +class Gem5LinuxDevice(BaseGem5Device, LinuxDevice): + """ + Implements gem5 Linux device. + + This class allows a user to connect WA to a simulation using gem5. The + connection to the device is made using the telnet connection of the + simulator, and is used for all commands. The simulator does not have ADB + support, and therefore we need to fall back to using standard shell + commands. + + Files are copied into the simulation using a VirtIO 9P device in gem5. Files + are copied out of the simulated environment using the m5 writefile command + within the simulated system. + + When starting the workload run, the simulator is automatically started by + Workload Automation, and a connection to the simulator is established. WA + will then wait for Android to boot on the simulated system (which can take + hours), prior to executing any other commands on the device. It is also + possible to resume from a checkpoint when starting the simulation. To do + this, please append the relevant checkpoint commands from the gem5 + simulation script to the gem5_discription argument in the agenda. + + Host system requirements: + * VirtIO support. We rely on diod on the host system. This can be + installed on ubuntu using the following command: + + sudo apt-get install diod + + Guest requirements: + * VirtIO support. We rely on VirtIO to move files into the simulation. + Please make sure that the following are set in the kernel + configuration: + + CONFIG_NET_9P=y + + CONFIG_NET_9P_VIRTIO=y + + CONFIG_9P_FS=y + + CONFIG_9P_FS_POSIX_ACL=y + + CONFIG_9P_FS_SECURITY=y + + CONFIG_VIRTIO_BLK=y + + * m5 binary. Please make sure that the m5 binary is on the device and + can by found in the path. + * Busybox. Due to restrictions, we assume that busybox is installed in + the guest system, and can be found in the path. + """ + + name = 'gem5_linux' + platform = 'linux' + + parameters = [ + Parameter('core_names', default=[], override=True), + Parameter('core_clusters', default=[], override=True), + Parameter('host', default='localhost', override=True, + description='Host name or IP address for the device.'), + ] + + # Overwritten from Device. For documentation, see corresponding method in + # Device. + + def __init__(self, **kwargs): + self.logger = logging.getLogger('Gem5LinuxDevice') + LinuxDevice.__init__(self, **kwargs) + BaseGem5Device.__init__(self, **kwargs) + + def login_to_device(self): + # Wait for the login prompt + i = self.sckt.expect([r'login:', r'username:', self.sckt.UNIQUE_PROMPT], + timeout=10) + # Check if we are already at a prompt, or if we need to log in. + if i < 2: + self.sckt.sendline("{}".format(self.username)) + j = self.sckt.expect([r'password:', r'# ', self.sckt.UNIQUE_PROMPT], + timeout=self.delay) + if j == 2: + self.sckt.sendline("{}".format(self.password)) + self.sckt.expect([r'# ', self.sckt.UNIQUE_PROMPT], timeout=self.delay) + + def capture_screen(self, filepath): + if BaseGem5Device.capture_screen(self, filepath): + return + + # If we didn't manage to do the above, call the parent class. + self.logger.warning("capture_screen: falling back to parent class implementation") + LinuxDevice.capture_screen(self, filepath)