mirror of
https://github.com/ARM-software/workload-automation.git
synced 2025-02-20 20:09:11 +00:00
Gem5Device: Add a gem5 device for Android
- Implementation of a gem5 device which allows simulated systems to be used in the place of a real device. Currently, only Android is supported. - The gem5 simulation is started automatically based on a command line passed in via the agenda. The correct telnet port to connect on is extracted from the standard error from the gem5 process. - Resuming from gem5 checkpoints is supported, and can be specified as part of the gem5 system description. Additionally, the agenda option checkpoint_post_boot can be used to create a checkpoint automatically once the system has booted. This can then by used for subsequent runs to avoid booting the system a second time. - The Gem5Device waits for Android to finish booting, before sending commands to the simulated device. Additionally, the device supports a sleep option, which will sleep in the simulated system for a number of seconds, prior to running the workload. This ensures that the system can quieten down, prior to running the workload. - The Gem5Device relies of VirtIO to pull files into the simulated environment, and therefire diod support is required on the host system. Additionally, VirtIO 9P support is required in the guest system kernel. - The m5 writefile binary and gem5 pseudo instruction are used to extract files from the simulated environment.
This commit is contained in:
parent
a6382b730b
commit
96a6179355
801
wlauto/devices/android/gem5/__init__.py
Normal file
801
wlauto/devices/android/gem5/__init__.py
Normal file
@ -0,0 +1,801 @@
|
||||
# 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
|
||||
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
|
||||
from wlauto.exceptions import DeviceError, ConfigError
|
||||
|
||||
|
||||
class Gem5Device(AndroidDevice):
|
||||
"""
|
||||
Implements gem5 Android 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_android'
|
||||
default_working_directory = '/data/'
|
||||
has_gpu = False
|
||||
platform = 'android'
|
||||
path_module = 'posixpath'
|
||||
default_package_data_directory = '/data/data/'
|
||||
default_binaries_directory = '/system/bin'
|
||||
default_external_storage_directory = '/data/data/'
|
||||
default_adb_name = None
|
||||
|
||||
# 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_description', kind=str, default='', override=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('virtio_command', kind=str, default='', override=True,
|
||||
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='', override=True,
|
||||
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_post_boot', kind=bool, default=False, mandatory=False, override=True,
|
||||
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, override=True,
|
||||
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')
|
||||
|
||||
super(Gem5Device, self).__init__(**kwargs)
|
||||
self.adb_name = kwargs.get('adb_name') or self.default_adb_name
|
||||
self.binaries_directory = kwargs.get('binaries_directory', self.default_binaries_directory)
|
||||
self.package_data_directory = kwargs.get('package_data_directory', self.default_package_data_directory)
|
||||
self.external_storage_directory = kwargs.get('external_storage_directory',
|
||||
self.default_external_storage_directory)
|
||||
self.logcat_poll_period = kwargs.get('logcat_poll_period')
|
||||
|
||||
# The gem5 subprocess
|
||||
self.gem5 = None
|
||||
|
||||
self.gem5_port = -1
|
||||
self.busybox = None
|
||||
self._is_initialized = False
|
||||
self._is_ready = False
|
||||
self._just_rebooted = False
|
||||
self._is_rooted = None
|
||||
self._available_frequencies = {}
|
||||
self._available_governors = {}
|
||||
self._available_governor_tunables = {}
|
||||
self._number_of_cores = None
|
||||
self._logcat_poller = None
|
||||
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
|
||||
|
||||
if not kwargs.get('gem5_description'):
|
||||
raise ConfigError('Please specify the system configuration with gem5_description')
|
||||
self.gem5_args = kwargs.get('gem5_description')
|
||||
|
||||
if kwargs.get('temp_dir'):
|
||||
self.temp_dir = kwargs.get('temp_dir')
|
||||
else:
|
||||
self.logger.info("No temporary directory passed in. Defaulting to /tmp")
|
||||
self.temp_dir = '/tmp'
|
||||
|
||||
if kwargs.get('checkpoint_post_boot'):
|
||||
self.checkpoint = True
|
||||
|
||||
if kwargs.get('run_delay'):
|
||||
self.run_delay = kwargs.get('run_delay')
|
||||
|
||||
# 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.info("Using {} as the temporary directory.".format(self.temp_dir))
|
||||
|
||||
if not kwargs.get('virtio_command'):
|
||||
raise ConfigError('Please specify the VirtIO command specific to your script, ending with the root parameter of the device.')
|
||||
|
||||
self.gem5_vio_arg = kwargs.get('virtio_command').format(self.temp_dir)
|
||||
self.logger.debug("gem5 VirtIO command: {}".format(self.gem5_vio_arg))
|
||||
|
||||
# Start the gem5 simulation when WA starts a run using a signal.
|
||||
sig.connect(self.init_gem5, sig.RUN_START)
|
||||
|
||||
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 = "./build/ARM/gem5.fast --outdir={}/gem5 {} {}".format(settings.output_directory,
|
||||
self.gem5_args,
|
||||
self.gem5_vio_arg)
|
||||
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<port>\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()
|
||||
|
||||
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:
|
||||
break
|
||||
except (DeviceError, ValueError):
|
||||
pass
|
||||
time.sleep(60)
|
||||
|
||||
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 " + str(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'# ', r'\[PEXPECT\]\$'], 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 mount_virtio(self):
|
||||
"""
|
||||
Mount the VirtIO device in the simulated system.
|
||||
|
||||
We cannot assume any state for the VirtIO device in gem5 as it is not
|
||||
serialised when checkpointing the system. Therefore, we unbind and
|
||||
rebind the VirtIO device to force the driver to re-initialize the
|
||||
device, prior to using it. We then mount the folder on the host system
|
||||
using the VirtIo device.
|
||||
"""
|
||||
self.logger.info("Mounting VirtIO device in simulated system")
|
||||
|
||||
# We always unbind, then re-bind the device. This ensures that the
|
||||
# driver is re-loaded and that the device is re-initialized. Hence, this
|
||||
# should work for both checkpointed and non-checkpointed gem5 systems.
|
||||
vio_info = self.gem5_shell('ls /sys/bus/pci/drivers/virtio-pci/').strip().split()
|
||||
mounts = []
|
||||
for f in vio_info:
|
||||
if len(f.split(':')) > 1:
|
||||
mounts.append(f.strip())
|
||||
|
||||
# Unbind and rebind all of the
|
||||
for mount in mounts:
|
||||
self.gem5_shell('echo -n "{}" > /sys/bus/pci/drivers/virtio-pci/unbind'.format(mount))
|
||||
self.gem5_shell('echo -n "{}" > /sys/bus/pci/drivers/virtio-pci/bind'.format(mount))
|
||||
|
||||
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))
|
||||
|
||||
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 = source.split('/')[-1]
|
||||
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 output:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def install(self, filepath, timeout=default_timeout): # pylint: disable=W0221
|
||||
""" Install an APK or a normal executable """
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
if ext == '.apk':
|
||||
return self.install_apk(filepath, timeout)
|
||||
else:
|
||||
return self.install_executable(filepath)
|
||||
|
||||
def install_apk(self, filepath, timeout=default_timeout): # pylint: disable=W0221
|
||||
"""
|
||||
Install an APK on the gem5 device
|
||||
|
||||
The APK is pushed to the device. Then the file and folder permissions
|
||||
are changed to ensure that the APK can be correctly installed. The APK
|
||||
is then installed on the device using 'pm'.
|
||||
"""
|
||||
self._check_ready()
|
||||
self.logger.info("Installing {}".format(filepath))
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
if ext == '.apk':
|
||||
filename = os.path.basename(filepath)
|
||||
on_device_path = os.path.join('/data/local/tmp', filename)
|
||||
self.push_file(filepath, on_device_path)
|
||||
# We need to make sure that the folder permissions are set
|
||||
# correctly, else the APK does not install correctly.
|
||||
self.gem5_shell('busybox chmod 775 /data/local/tmp')
|
||||
self.gem5_shell('busybox chmod 774 {}'.format(on_device_path))
|
||||
self.logger.debug("Actually installing the APK: {}".format(on_device_path))
|
||||
return self.gem5_shell("pm install {}".format(on_device_path))
|
||||
else:
|
||||
raise DeviceError('Can\'t install {}: unsupported format.'.format(filepath))
|
||||
|
||||
def install_executable(self, filepath, with_name=None):
|
||||
""" Install an executable """
|
||||
executable_name = os.path.basename(filepath)
|
||||
on_device_file = self.path.join(self.working_directory, executable_name)
|
||||
on_device_executable = self.path.join(self.binaries_directory, executable_name)
|
||||
self.push_file(filepath, on_device_file)
|
||||
self.execute('busybox cp {} {}'.format(on_device_file, on_device_executable))
|
||||
self.execute('busybox chmod 0777 {}'.format(on_device_executable))
|
||||
return on_device_executable
|
||||
|
||||
def uninstall(self, package):
|
||||
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")
|
||||
filename = outfile.split('/')[-1]
|
||||
command = 'logcat -d > {}'.format(filename)
|
||||
self.gem5_shell(command)
|
||||
self.pull_file("{}".format(filename), outfile)
|
||||
|
||||
def clear_logcat(self):
|
||||
"""Clear (flush) logcat log."""
|
||||
if self._logcat_poller:
|
||||
return self._logcat_poller.clear_buffer()
|
||||
else:
|
||||
return self.gem5_shell('logcat -c')
|
||||
|
||||
def disable_selinux(self):
|
||||
""" Disable SELinux. Overridden as parent implementation uses ADB """
|
||||
api_level = int(self.gem5_shell('getprop ro.build.version.sdk').strip())
|
||||
|
||||
# SELinux was added in Android 4.3 (API level 18). Trying to
|
||||
# 'getenforce' in earlier versions will produce an error.
|
||||
if api_level >= 18:
|
||||
se_status = self.execute('getenforce', as_root=True).strip()
|
||||
if se_status == 'Enforcing':
|
||||
self.execute('setenforce 0', as_root=True)
|
||||
|
||||
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))
|
||||
|
||||
# This is duplicated from the AndroidDevice class, as we want to run
|
||||
# this code without executing the BaseLinuxDevice implementation
|
||||
props = {}
|
||||
props['android_id'] = self.get_android_id()
|
||||
buildprop_file = os.path.join(context.host_working_directory, 'build.prop')
|
||||
if not os.path.isfile(buildprop_file):
|
||||
self.pull_file('/system/build.prop', context.host_working_directory)
|
||||
self._update_build_properties(buildprop_file, props)
|
||||
context.add_run_artifact('build_properties', buildprop_file, 'export')
|
||||
|
||||
dumpsys_window_file = os.path.join(context.host_working_directory, 'window.dumpsys')
|
||||
self.execute('{} > {}'.format('dumpsys window', 'window.dumpsys'))
|
||||
self.pull_file('window.dumpsys', dumpsys_window_file)
|
||||
context.add_run_artifact('dumpsys_window', dumpsys_window_file, 'meta')
|
||||
return props
|
||||
|
||||
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 disable_screen_lock(self):
|
||||
"""
|
||||
Attempts to disable he screen lock on the device.
|
||||
|
||||
Overridden here as otherwise we have issues with too many backslashes.
|
||||
"""
|
||||
lockdb = '/data/system/locksettings.db'
|
||||
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 we didn't manage to do the above, call the parent class.
|
||||
self.logger.debug("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):
|
||||
raise DeviceError('we do not need 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)
|
||||
command_index = output.find(command)
|
||||
# The command was probably too long and wrapped. This bit of
|
||||
# code is a total hack!
|
||||
if command_index == -1:
|
||||
# We need to remove the backspace characters!
|
||||
output = re.sub(r'[\b]', r'', output)
|
||||
while True:
|
||||
first_cr = output.find('\r')
|
||||
first_lt = output.find('<')
|
||||
if first_cr == -1 or first_lt == -1:
|
||||
break
|
||||
if first_cr < first_lt:
|
||||
new_output = output[:first_cr]
|
||||
new_output += output[first_lt + 1:]
|
||||
output = new_output
|
||||
command_index = output.find(command)
|
||||
else:
|
||||
break
|
||||
|
||||
# If we STILL 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(r'\[PEXPECT\]\$', 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(r'\[PEXPECT\]\$', timeout=self.delay)
|
||||
self.sckt.expect(r'\[PEXPECT\]\$', 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()
|
Loading…
x
Reference in New Issue
Block a user