mirror of
				https://github.com/ARM-software/workload-automation.git
				synced 2025-10-30 22:54:18 +00:00 
			
		
		
		
	Devices: Removed Devices
They are now superseded by DeviceManagers
This commit is contained in:
		| @@ -1,765 +0,0 @@ | ||||
| #    Copyright 2013-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. | ||||
| # | ||||
|  | ||||
| # pylint: disable=E1101 | ||||
| import os | ||||
| import sys | ||||
| import re | ||||
| import time | ||||
| import tempfile | ||||
| import shutil | ||||
| import threading | ||||
| from subprocess import CalledProcessError | ||||
|  | ||||
| from wlauto.core.extension import Parameter | ||||
| from wlauto.common.linux.device import BaseLinuxDevice, PsEntry | ||||
| from wlauto.exceptions import DeviceError, WorkerThreadError, TimeoutError, DeviceNotRespondingError | ||||
| from wlauto.utils.misc import convert_new_lines | ||||
| from wlauto.utils.types import boolean, regex | ||||
| from wlauto.utils.android import (adb_shell, adb_background_shell, adb_list_devices, | ||||
|                                   adb_command, AndroidProperties, ANDROID_VERSION_MAP) | ||||
|  | ||||
|  | ||||
| SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)', re.I) | ||||
| SCREEN_SIZE_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)\s+(?P<width>\d+)x(?P<height>\d+)') | ||||
|  | ||||
|  | ||||
| class AndroidDevice(BaseLinuxDevice):  # pylint: disable=W0223 | ||||
|     """ | ||||
|     Device running Android OS. | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     platform = 'android' | ||||
|  | ||||
|     parameters = [ | ||||
|         Parameter('adb_name', | ||||
|                   description='The unique ID of the device as output by "adb devices".'), | ||||
|         Parameter('android_prompt', kind=regex, default=re.compile('^.*(shell|root)@.*:/\S* [#$] ', re.MULTILINE), | ||||
|                   description='The format  of matching the shell prompt in Android.'), | ||||
|         Parameter('working_directory', default='/sdcard/wa-working', | ||||
|                   description='Directory that will be used WA on the device for output files etc.'), | ||||
|         Parameter('binaries_directory', default='/data/local/tmp', override=True, | ||||
|                   description='Location of binaries on the device.'), | ||||
|         Parameter('package_data_directory', default='/data/data', | ||||
|                   description='Location of of data for an installed package (APK).'), | ||||
|         Parameter('external_storage_directory', default='/sdcard', | ||||
|                   description='Mount point for external storage.'), | ||||
|         Parameter('connection', default='usb', allowed_values=['usb', 'ethernet'], | ||||
|                   description='Specified the nature of adb connection.'), | ||||
|         Parameter('logcat_poll_period', kind=int, | ||||
|                   description=""" | ||||
|                   If specified and is not ``0``, logcat will be polled every | ||||
|                   ``logcat_poll_period`` seconds, and buffered on the host. This | ||||
|                   can be used if a lot of output is expected in logcat and the fixed | ||||
|                   logcat buffer on the device is not big enough. The trade off is that | ||||
|                   this introduces some minor runtime overhead. Not set by default. | ||||
|                   """), | ||||
|         Parameter('enable_screen_check', kind=boolean, default=False, | ||||
|                   description=""" | ||||
|                   Specified whether the device should make sure that the screen is on | ||||
|                   during initialization. | ||||
|                   """), | ||||
|         Parameter('swipe_to_unlock', kind=str, default=None, | ||||
|                   allowed_values=[None, "horizontal", "vertical"], | ||||
|                   description=""" | ||||
|                   If set a swipe of the specified direction will be performed. | ||||
|                   This should unlock the screen. | ||||
|                   """), | ||||
|     ] | ||||
|  | ||||
|     default_timeout = 30 | ||||
|     delay = 2 | ||||
|     long_delay = 3 * delay | ||||
|     ready_timeout = 60 | ||||
|  | ||||
|     # Overwritten from Device. For documentation, see corresponding method in | ||||
|     # Device. | ||||
|  | ||||
|     @property | ||||
|     def is_rooted(self): | ||||
|         if self._is_rooted is None: | ||||
|             try: | ||||
|                 result = adb_shell(self.adb_name, 'su', timeout=1) | ||||
|                 if 'not found' in result: | ||||
|                     self._is_rooted = False | ||||
|                 else: | ||||
|                     self._is_rooted = True | ||||
|             except TimeoutError: | ||||
|                 self._is_rooted = True | ||||
|             except DeviceError: | ||||
|                 self._is_rooted = False | ||||
|         return self._is_rooted | ||||
|  | ||||
|     @property | ||||
|     def abi(self): | ||||
|         return self.getprop()['ro.product.cpu.abi'].split('-')[0] | ||||
|  | ||||
|     @property | ||||
|     def supported_eabi(self): | ||||
|         props = self.getprop() | ||||
|         result = [props['ro.product.cpu.abi']] | ||||
|         if 'ro.product.cpu.abi2' in props: | ||||
|             result.append(props['ro.product.cpu.abi2']) | ||||
|         if 'ro.product.cpu.abilist' in props: | ||||
|             for eabi in props['ro.product.cpu.abilist'].split(','): | ||||
|                 if eabi not in result: | ||||
|                     result.append(eabi) | ||||
|         return result | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         super(AndroidDevice, self).__init__(**kwargs) | ||||
|         self._logcat_poller = None | ||||
|  | ||||
|     def reset(self): | ||||
|         self._is_ready = False | ||||
|         self._just_rebooted = True | ||||
|         adb_command(self.adb_name, 'reboot', timeout=self.default_timeout) | ||||
|  | ||||
|     def hard_reset(self): | ||||
|         super(AndroidDevice, self).hard_reset() | ||||
|         self._is_ready = False | ||||
|         self._just_rebooted = True | ||||
|  | ||||
|     def boot(self, hard=False, **kwargs): | ||||
|         if hard: | ||||
|             self.hard_reset() | ||||
|         else: | ||||
|             self.reset() | ||||
|  | ||||
|     def connect(self):  # NOQA pylint: disable=R0912 | ||||
|         iteration_number = 0 | ||||
|         max_iterations = self.ready_timeout / self.delay | ||||
|         available = False | ||||
|         self.logger.debug('Polling for device {}...'.format(self.adb_name)) | ||||
|         while iteration_number < max_iterations: | ||||
|             devices = adb_list_devices() | ||||
|             if self.adb_name: | ||||
|                 for device in devices: | ||||
|                     if device.name == self.adb_name and device.status != 'offline': | ||||
|                         available = True | ||||
|             else:  # adb_name not set | ||||
|                 if len(devices) == 1: | ||||
|                     available = True | ||||
|                 elif len(devices) > 1: | ||||
|                     raise DeviceError('More than one device is connected and adb_name is not set.') | ||||
|  | ||||
|             if available: | ||||
|                 break | ||||
|             else: | ||||
|                 time.sleep(self.delay) | ||||
|                 iteration_number += 1 | ||||
|         else: | ||||
|             raise DeviceError('Could not boot {} ({}).'.format(self.name, self.adb_name)) | ||||
|  | ||||
|         while iteration_number < max_iterations: | ||||
|             available = (int('0' + (adb_shell(self.adb_name, 'getprop sys.boot_completed', timeout=self.default_timeout))) == 1) | ||||
|             if available: | ||||
|                 break | ||||
|             else: | ||||
|                 time.sleep(self.delay) | ||||
|                 iteration_number += 1 | ||||
|         else: | ||||
|             raise DeviceError('Could not boot {} ({}).'.format(self.name, self.adb_name)) | ||||
|  | ||||
|         if self._just_rebooted: | ||||
|             self.logger.debug('Waiting for boot to complete...') | ||||
|             # On some devices, adb connection gets reset some time after booting. | ||||
|             # This  causes errors during execution. To prevent this, open a shell | ||||
|             # session and wait for it to be killed. Once its killed, give adb | ||||
|             # enough time to restart, and then the device should be ready. | ||||
|             # TODO: This is more of a work-around rather than an actual solution. | ||||
|             #       Need to figure out what is going on the "proper" way of handling it. | ||||
|             try: | ||||
|                 adb_shell(self.adb_name, '', timeout=20) | ||||
|                 time.sleep(5)  # give adb time to re-initialize | ||||
|             except TimeoutError: | ||||
|                 pass  # timed out waiting for the session to be killed -- assume not going to be. | ||||
|  | ||||
|             self.logger.debug('Boot completed.') | ||||
|             self._just_rebooted = False | ||||
|         self._is_ready = True | ||||
|  | ||||
|     def initialize(self, context): | ||||
|         if self.is_rooted: | ||||
|             self.disable_screen_lock() | ||||
|             self.disable_selinux() | ||||
|         if self.enable_screen_check: | ||||
|             self.ensure_screen_is_on() | ||||
|  | ||||
|     def disconnect(self): | ||||
|         if self._logcat_poller: | ||||
|             self._logcat_poller.close() | ||||
|  | ||||
|     def ping(self): | ||||
|         try: | ||||
|             # May be triggered inside initialize() | ||||
|             adb_shell(self.adb_name, 'ls /', timeout=10) | ||||
|         except (TimeoutError, CalledProcessError): | ||||
|             raise DeviceNotRespondingError(self.adb_name or self.name) | ||||
|  | ||||
|     def start(self): | ||||
|         if self.logcat_poll_period: | ||||
|             if self._logcat_poller: | ||||
|                 self._logcat_poller.close() | ||||
|             self._logcat_poller = _LogcatPoller(self, self.logcat_poll_period, timeout=self.default_timeout) | ||||
|             self._logcat_poller.start() | ||||
|  | ||||
|     def stop(self): | ||||
|         if self._logcat_poller: | ||||
|             self._logcat_poller.stop() | ||||
|  | ||||
|     def get_android_version(self): | ||||
|         return ANDROID_VERSION_MAP.get(self.get_sdk_version(), None) | ||||
|  | ||||
|     def get_android_id(self): | ||||
|         """ | ||||
|         Get the device's ANDROID_ID. Which is | ||||
|  | ||||
|             "A 64-bit number (as a hex string) that is randomly generated when the user | ||||
|             first sets up the device and should remain constant for the lifetime of the | ||||
|             user's device." | ||||
|  | ||||
|         .. note:: This will get reset on userdata erasure. | ||||
|  | ||||
|         """ | ||||
|         output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip() | ||||
|         return output.split('value=')[-1] | ||||
|  | ||||
|     def get_sdk_version(self): | ||||
|         try: | ||||
|             return int(self.getprop('ro.build.version.sdk')) | ||||
|         except (ValueError, TypeError): | ||||
|             return None | ||||
|  | ||||
|     def get_installed_package_version(self, package): | ||||
|         """ | ||||
|         Returns the version (versionName) of the specified package if it is installed | ||||
|         on the device, or ``None`` otherwise. | ||||
|  | ||||
|         Added in version 2.1.4 | ||||
|  | ||||
|         """ | ||||
|         output = self.execute('dumpsys package {}'.format(package)) | ||||
|         for line in convert_new_lines(output).split('\n'): | ||||
|             if 'versionName' in line: | ||||
|                 return line.split('=', 1)[1] | ||||
|         return None | ||||
|  | ||||
|     def list_packages(self): | ||||
|         """ | ||||
|         List packages installed on the device. | ||||
|  | ||||
|         Added in version 2.1.4 | ||||
|  | ||||
|         """ | ||||
|         output = self.execute('pm list packages') | ||||
|         output = output.replace('package:', '') | ||||
|         return output.split() | ||||
|  | ||||
|     def package_is_installed(self, package_name): | ||||
|         """ | ||||
|         Returns ``True`` the if a package with the specified name is installed on | ||||
|         the device, and ``False`` otherwise. | ||||
|  | ||||
|         Added in version 2.1.4 | ||||
|  | ||||
|         """ | ||||
|         return package_name in self.list_packages() | ||||
|  | ||||
|     def executable_is_installed(self, executable_name):  # pylint: disable=unused-argument,no-self-use | ||||
|         raise AttributeError("""Instead of using is_installed, please use | ||||
|             ``get_binary_path`` or ``install_if_needed`` instead. You should | ||||
|             use the path returned by these functions to then invoke the binary | ||||
|  | ||||
|             please see: https://pythonhosted.org/wlauto/writing_extensions.html""") | ||||
|  | ||||
|     def is_installed(self, name): | ||||
|         if self.package_is_installed(name): | ||||
|             return True | ||||
|         elif "." in name:  # assumes android packages have a . in their name and binaries documentation | ||||
|             return False | ||||
|         else: | ||||
|             raise AttributeError("""Instead of using is_installed, please use | ||||
|                 ``get_binary_path`` or ``install_if_needed`` instead. You should | ||||
|                 use the path returned by these functions to then invoke the binary | ||||
|  | ||||
|                 please see: https://pythonhosted.org/wlauto/writing_extensions.html""") | ||||
|  | ||||
|     def listdir(self, path, as_root=False, **kwargs): | ||||
|         contents = self.execute('ls {}'.format(path), as_root=as_root) | ||||
|         return [x.strip() for x in contents.split()] | ||||
|  | ||||
|     def push_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221 | ||||
|         """ | ||||
|         Modified in version 2.1.4: added  ``as_root`` parameter. | ||||
|  | ||||
|         """ | ||||
|         self._check_ready() | ||||
|         try: | ||||
|             if not as_root: | ||||
|                 adb_command(self.adb_name, "push '{}' '{}'".format(source, dest), timeout=timeout) | ||||
|             else: | ||||
|                 device_tempfile = self.path.join(self.file_transfer_cache, source.lstrip(self.path.sep)) | ||||
|                 self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile))) | ||||
|                 adb_command(self.adb_name, "push '{}' '{}'".format(source, device_tempfile), timeout=timeout) | ||||
|                 self.execute('cp {} {}'.format(device_tempfile, dest), as_root=True) | ||||
|         except CalledProcessError as e: | ||||
|             raise DeviceError(e) | ||||
|  | ||||
|     def pull_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221 | ||||
|         """ | ||||
|         Modified in version 2.1.4: added  ``as_root`` parameter. | ||||
|  | ||||
|         """ | ||||
|         self._check_ready() | ||||
|         try: | ||||
|             if not as_root: | ||||
|                 adb_command(self.adb_name, "pull '{}' '{}'".format(source, dest), timeout=timeout) | ||||
|             else: | ||||
|                 device_tempfile = self.path.join(self.file_transfer_cache, source.lstrip(self.path.sep)) | ||||
|                 self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile))) | ||||
|                 self.execute('cp {} {}'.format(source, device_tempfile), as_root=True) | ||||
|                 adb_command(self.adb_name, "pull '{}' '{}'".format(device_tempfile, dest), timeout=timeout) | ||||
|         except CalledProcessError as e: | ||||
|             raise DeviceError(e) | ||||
|  | ||||
|     def delete_file(self, filepath, as_root=False):  # pylint: disable=W0221 | ||||
|         self._check_ready() | ||||
|         adb_shell(self.adb_name, "rm '{}'".format(filepath), as_root=as_root, timeout=self.default_timeout) | ||||
|  | ||||
|     def file_exists(self, filepath): | ||||
|         self._check_ready() | ||||
|         output = adb_shell(self.adb_name, 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath), | ||||
|                            timeout=self.default_timeout) | ||||
|         return bool(int(output)) | ||||
|  | ||||
|     def install(self, filepath, timeout=default_timeout, with_name=None):  # pylint: disable=W0221 | ||||
|         ext = os.path.splitext(filepath)[1].lower() | ||||
|         if ext == '.apk': | ||||
|             return self.install_apk(filepath, timeout) | ||||
|         else: | ||||
|             return self.install_executable(filepath, with_name) | ||||
|  | ||||
|     def install_apk(self, filepath, timeout=default_timeout):  # pylint: disable=W0221 | ||||
|         self._check_ready() | ||||
|         ext = os.path.splitext(filepath)[1].lower() | ||||
|         if ext == '.apk': | ||||
|             return adb_command(self.adb_name, "install {}".format(filepath), timeout=timeout) | ||||
|         else: | ||||
|             raise DeviceError('Can\'t install {}: unsupported format.'.format(filepath)) | ||||
|  | ||||
|     def install_executable(self, filepath, with_name=None): | ||||
|         """ | ||||
|         Installs a binary executable on device. Returns | ||||
|         the path to the installed binary, or ``None`` if the installation has failed. | ||||
|         Optionally, ``with_name`` parameter may be used to specify a different name under | ||||
|         which the executable will be installed. | ||||
|  | ||||
|         Added in version 2.1.3. | ||||
|         Updated in version 2.1.5 with ``with_name`` parameter. | ||||
|  | ||||
|         """ | ||||
|         self._ensure_binaries_directory_is_writable() | ||||
|         executable_name = with_name or 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('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted) | ||||
|         self.execute('chmod 0777 {}'.format(on_device_executable), as_root=self.is_rooted) | ||||
|         return on_device_executable | ||||
|  | ||||
|     def uninstall(self, package): | ||||
|         self._check_ready() | ||||
|         adb_command(self.adb_name, "uninstall {}".format(package), timeout=self.default_timeout) | ||||
|  | ||||
|     def uninstall_executable(self, executable_name): | ||||
|         """ | ||||
|  | ||||
|         Added in version 2.1.3. | ||||
|  | ||||
|         """ | ||||
|         on_device_executable = self.get_binary_path(executable_name, search_system_binaries=False) | ||||
|         if not on_device_executable: | ||||
|             raise DeviceError("Could not uninstall {}, binary not found".format(on_device_executable)) | ||||
|         self._ensure_binaries_directory_is_writable() | ||||
|         self.delete_file(on_device_executable, as_root=self.is_rooted) | ||||
|  | ||||
|     def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False, | ||||
|                 as_root=False, busybox=False, **kwargs): | ||||
|         """ | ||||
|         Execute the specified command on the device using adb. | ||||
|  | ||||
|         Parameters: | ||||
|  | ||||
|             :param command: The command to be executed. It should appear exactly | ||||
|                             as if you were typing it into a shell. | ||||
|             :param timeout: Time, in seconds, to wait for adb to return before aborting | ||||
|                             and raising an error. Defaults to ``AndroidDevice.default_timeout``. | ||||
|             :param check_exit_code: If ``True``, the return code of the command on the Device will | ||||
|                                     be check and exception will be raised if it is not 0. | ||||
|                                     Defaults to ``True``. | ||||
|             :param background: If ``True``, will execute adb in a subprocess, and will return | ||||
|                                immediately, not waiting for adb to return. Defaults to ``False`` | ||||
|             :param busybox: If ``True``, will use busybox to execute the command. Defaults to ``False``. | ||||
|  | ||||
|                             Added in version 2.1.3 | ||||
|  | ||||
|                             .. note:: The device must be rooted to be able to use some busybox features. | ||||
|  | ||||
|             :param as_root: If ``True``, will attempt to execute command in privileged mode. The device | ||||
|                             must be rooted, otherwise an error will be raised. Defaults to ``False``. | ||||
|  | ||||
|                             Added in version 2.1.3 | ||||
|  | ||||
|         :returns: If ``background`` parameter is set to ``True``, the subprocess object will | ||||
|                   be returned; otherwise, the contents of STDOUT from the device will be returned. | ||||
|  | ||||
|         :raises: DeviceError if adb timed out  or if the command returned non-zero exit | ||||
|                  code on the device, or if attempting to execute a command in privileged mode on an | ||||
|                  unrooted device. | ||||
|  | ||||
|         """ | ||||
|         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: | ||||
|             command = ' '.join([self.busybox, command]) | ||||
|         if background: | ||||
|             return adb_background_shell(self.adb_name, command, as_root=as_root) | ||||
|         else: | ||||
|             return adb_shell(self.adb_name, command, timeout, check_exit_code, as_root) | ||||
|  | ||||
|     def kick_off(self, command): | ||||
|         """ | ||||
|         Like execute but closes adb session and returns immediately, leaving the command running on the | ||||
|         device (this is different from execute(background=True) which keeps adb connection open and returns | ||||
|         a subprocess object). | ||||
|  | ||||
|         .. note:: This relies on busybox's nohup applet and so won't work on unrooted devices. | ||||
|  | ||||
|         Added in version 2.1.4 | ||||
|  | ||||
|         """ | ||||
|         if not self.is_rooted: | ||||
|             raise DeviceError('kick_off uses busybox\'s nohup applet and so can only be run a rooted device.') | ||||
|         try: | ||||
|             command = 'cd {} && busybox nohup {}'.format(self.working_directory, command) | ||||
|             output = self.execute(command, timeout=1, as_root=True) | ||||
|         except TimeoutError: | ||||
|             pass | ||||
|         else: | ||||
|             raise ValueError('Background command exited before timeout; got "{}"'.format(output)) | ||||
|  | ||||
|     def get_pids_of(self, process_name): | ||||
|         """Returns a list of PIDs of all processes with the specified name.""" | ||||
|         result = self.execute('ps | {} grep {}'.format(self.busybox, process_name), | ||||
|                               check_exit_code=False).strip() | ||||
|         if result and 'not found' not in result: | ||||
|             return [int(x.split()[1]) for x in result.split('\n')] | ||||
|         else: | ||||
|             return [] | ||||
|  | ||||
|     def ps(self, **kwargs): | ||||
|         """ | ||||
|         Returns the list of running processes on the device. Keyword arguments may | ||||
|         be used to specify simple filters for columns. | ||||
|  | ||||
|         Added in version 2.1.4 | ||||
|  | ||||
|         """ | ||||
|         lines = iter(convert_new_lines(self.execute('ps')).split('\n')) | ||||
|         lines.next()  # header | ||||
|         result = [] | ||||
|         for line in lines: | ||||
|             parts = line.split() | ||||
|             if parts: | ||||
|                 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:]))) | ||||
|         if not kwargs: | ||||
|             return result | ||||
|         else: | ||||
|             filtered_result = [] | ||||
|             for entry in result: | ||||
|                 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()): | ||||
|                     filtered_result.append(entry) | ||||
|             return filtered_result | ||||
|  | ||||
|     def get_properties(self, context): | ||||
|         """Captures and saves the information from /system/build.prop and /proc/version""" | ||||
|         props = super(AndroidDevice, self).get_properties(context) | ||||
|         props.update(self._get_android_properties(context)) | ||||
|         return props | ||||
|  | ||||
|     def _get_android_properties(self, context): | ||||
|         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_target_file = self.path.join(self.working_directory, 'window.dumpsys') | ||||
|         dumpsys_host_file = os.path.join(context.host_working_directory, 'window.dumpsys') | ||||
|         self.execute('{} > {}'.format('dumpsys window', dumpsys_target_file)) | ||||
|         self.pull_file(dumpsys_target_file, dumpsys_host_file) | ||||
|         context.add_run_artifact('dumpsys_window', dumpsys_host_file, 'meta') | ||||
|         return props | ||||
|  | ||||
|     def getprop(self, prop=None): | ||||
|         """Returns parsed output of Android getprop command. If a property is | ||||
|         specified, only the value for that property will be returned (with | ||||
|         ``None`` returned if the property doesn't exist. Otherwise, | ||||
|         ``wlauto.utils.android.AndroidProperties`` will be returned, which is | ||||
|         a dict-like object.""" | ||||
|         props = AndroidProperties(self.execute('getprop')) | ||||
|         if prop: | ||||
|             return props[prop] | ||||
|         return props | ||||
|  | ||||
|     # Android-specific methods. These either rely on specifics of adb or other | ||||
|     # Android-only concepts in their interface and/or implementation. | ||||
|  | ||||
|     def forward_port(self, from_port, to_port): | ||||
|         """ | ||||
|         Forward a port on the device to a port on localhost. | ||||
|  | ||||
|         :param from_port: Port on the device which to forward. | ||||
|         :param to_port: Port on the localhost to which the device port will be forwarded. | ||||
|  | ||||
|         Ports should be specified using adb spec. See the "adb forward" section in "adb help". | ||||
|  | ||||
|         """ | ||||
|         adb_command(self.adb_name, 'forward {} {}'.format(from_port, to_port), timeout=self.default_timeout) | ||||
|  | ||||
|     def dump_logcat(self, outfile, filter_spec=None): | ||||
|         """ | ||||
|         Dump the contents of logcat, for the specified filter spec to the | ||||
|         specified output file. | ||||
|         See http://developer.android.com/tools/help/logcat.html | ||||
|  | ||||
|         :param outfile: Output file on the host into which the contents of the | ||||
|                         log will be written. | ||||
|         :param filter_spec: Logcat filter specification. | ||||
|                             see http://developer.android.com/tools/debugging/debugging-log.html#filteringOutput | ||||
|  | ||||
|         """ | ||||
|         if self._logcat_poller: | ||||
|             return self._logcat_poller.write_log(outfile) | ||||
|         else: | ||||
|             if filter_spec: | ||||
|                 command = 'logcat -d -s {} > {}'.format(filter_spec, outfile) | ||||
|             else: | ||||
|                 command = 'logcat -d > {}'.format(outfile) | ||||
|             return adb_command(self.adb_name, command, timeout=self.default_timeout) | ||||
|  | ||||
|     def clear_logcat(self): | ||||
|         """Clear (flush) logcat log.""" | ||||
|         if self._logcat_poller: | ||||
|             return self._logcat_poller.clear_buffer() | ||||
|         else: | ||||
|             return adb_shell(self.adb_name, 'logcat -c', timeout=self.default_timeout) | ||||
|  | ||||
|     def get_screen_size(self): | ||||
|         output = self.execute('dumpsys window') | ||||
|         match = SCREEN_SIZE_REGEX.search(output) | ||||
|         if match: | ||||
|             return (int(match.group('width')), | ||||
|                     int(match.group('height'))) | ||||
|         else: | ||||
|             return (0, 0) | ||||
|  | ||||
|     def perform_unlock_swipe(self): | ||||
|         width, height = self.get_screen_size() | ||||
|         command = 'input swipe {} {} {} {}' | ||||
|         if self.swipe_to_unlock == "horizontal": | ||||
|             swipe_heigh = height * 2 // 3 | ||||
|             start = 100 | ||||
|             stop = width - start | ||||
|             self.execute(command.format(start, swipe_heigh, stop, swipe_heigh)) | ||||
|         if self.swipe_to_unlock == "vertical": | ||||
|             swipe_middle = height / 2 | ||||
|             swipe_heigh = height * 2 // 3 | ||||
|             self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0)) | ||||
|         else:  # Should never reach here | ||||
|             raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock)) | ||||
|  | ||||
|     def capture_screen(self, filepath): | ||||
|         """Caputers the current device screen into the specified file in a PNG format.""" | ||||
|         on_device_file = self.path.join(self.working_directory, 'screen_capture.png') | ||||
|         self.execute('screencap -p  {}'.format(on_device_file)) | ||||
|         self.pull_file(on_device_file, filepath) | ||||
|         self.delete_file(on_device_file) | ||||
|  | ||||
|     def is_screen_on(self): | ||||
|         """Returns ``True`` if the device screen is currently on, ``False`` otherwise.""" | ||||
|         output = self.execute('dumpsys power') | ||||
|         match = SCREEN_STATE_REGEX.search(output) | ||||
|         if match: | ||||
|             return boolean(match.group(1)) | ||||
|         else: | ||||
|             raise DeviceError('Could not establish screen state.') | ||||
|  | ||||
|     def ensure_screen_is_on(self): | ||||
|         if not self.is_screen_on(): | ||||
|             self.execute('input keyevent 26') | ||||
|             if self.swipe_to_unlock: | ||||
|                 self.perform_unlock_swipe() | ||||
|  | ||||
|     def disable_screen_lock(self): | ||||
|         """ | ||||
|         Attempts to disable he screen lock on the device. | ||||
|  | ||||
|         .. note:: This does not always work... | ||||
|  | ||||
|         Added inversion 2.1.4 | ||||
|  | ||||
|         """ | ||||
|         lockdb = '/data/system/locksettings.db' | ||||
|         sqlcommand = "update locksettings set value='0' where name='screenlock.disabled';" | ||||
|         self.execute('sqlite3 {} "{}"'.format(lockdb, sqlcommand), as_root=True) | ||||
|  | ||||
|     def disable_selinux(self): | ||||
|         # This may be invoked from intialize() so we can't use execute() or the | ||||
|         # standard API for doing this. | ||||
|         api_level = int(adb_shell(self.adb_name, 'getprop ro.build.version.sdk', | ||||
|                                   timeout=self.default_timeout).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_device_model(self): | ||||
|         try: | ||||
|             return self.getprop(prop='ro.product.device') | ||||
|         except KeyError: | ||||
|             return None | ||||
|  | ||||
|     # Internal methods: do not use outside of the class. | ||||
|  | ||||
|     def _update_build_properties(self, filepath, props): | ||||
|         try: | ||||
|             with open(filepath) as fh: | ||||
|                 for line in fh: | ||||
|                     line = re.sub(r'#.*', '', line).strip() | ||||
|                     if not line: | ||||
|                         continue | ||||
|                     key, value = line.split('=', 1) | ||||
|                     props[key] = value | ||||
|         except ValueError: | ||||
|             self.logger.warning('Could not parse build.prop.') | ||||
|  | ||||
|     def _update_versions(self, filepath, props): | ||||
|         with open(filepath) as fh: | ||||
|             text = fh.read() | ||||
|             props['version'] = text | ||||
|             text = re.sub(r'#.*', '', text).strip() | ||||
|             match = re.search(r'^(Linux version .*?)\s*\((gcc version .*)\)$', text) | ||||
|             if match: | ||||
|                 props['linux_version'] = match.group(1).strip() | ||||
|                 props['gcc_version'] = match.group(2).strip() | ||||
|             else: | ||||
|                 self.logger.warning('Could not parse version string.') | ||||
|  | ||||
|     def _ensure_binaries_directory_is_writable(self): | ||||
|         matched = [] | ||||
|         for entry in self.list_file_systems(): | ||||
|             if self.binaries_directory.rstrip('/').startswith(entry.mount_point): | ||||
|                 matched.append(entry) | ||||
|         if matched: | ||||
|             entry = sorted(matched, key=lambda x: len(x.mount_point))[-1] | ||||
|             if 'rw' not in entry.options: | ||||
|                 self.execute('mount -o rw,remount {} {}'.format(entry.device, entry.mount_point), as_root=True) | ||||
|         else: | ||||
|             raise DeviceError('Could not find mount point for binaries directory {}'.format(self.binaries_directory)) | ||||
|  | ||||
|  | ||||
| class _LogcatPoller(threading.Thread): | ||||
|  | ||||
|     join_timeout = 5 | ||||
|  | ||||
|     def __init__(self, device, period, timeout=None): | ||||
|         super(_LogcatPoller, self).__init__() | ||||
|         self.adb_device = device.adb_name | ||||
|         self.logger = device.logger | ||||
|         self.period = period | ||||
|         self.timeout = timeout | ||||
|         self.stop_signal = threading.Event() | ||||
|         self.lock = threading.RLock() | ||||
|         self.buffer_file = tempfile.mktemp() | ||||
|         self.last_poll = 0 | ||||
|         self.daemon = True | ||||
|         self.exc = None | ||||
|  | ||||
|     def run(self): | ||||
|         self.logger.debug('Starting logcat polling.') | ||||
|         try: | ||||
|             while True: | ||||
|                 if self.stop_signal.is_set(): | ||||
|                     break | ||||
|                 with self.lock: | ||||
|                     current_time = time.time() | ||||
|                     if (current_time - self.last_poll) >= self.period: | ||||
|                         self._poll() | ||||
|                 time.sleep(0.5) | ||||
|         except Exception:  # pylint: disable=W0703 | ||||
|             self.exc = WorkerThreadError(self.name, sys.exc_info()) | ||||
|         self.logger.debug('Logcat polling stopped.') | ||||
|  | ||||
|     def stop(self): | ||||
|         self.logger.debug('Stopping logcat polling.') | ||||
|         self.stop_signal.set() | ||||
|         self.join(self.join_timeout) | ||||
|         if self.is_alive(): | ||||
|             self.logger.error('Could not join logcat poller thread.') | ||||
|         if self.exc: | ||||
|             raise self.exc  # pylint: disable=E0702 | ||||
|  | ||||
|     def clear_buffer(self): | ||||
|         self.logger.debug('Clearing logcat buffer.') | ||||
|         with self.lock: | ||||
|             adb_shell(self.adb_device, 'logcat -c', timeout=self.timeout) | ||||
|             with open(self.buffer_file, 'w') as _:  # NOQA | ||||
|                 pass | ||||
|  | ||||
|     def write_log(self, outfile): | ||||
|         self.logger.debug('Writing logbuffer to {}.'.format(outfile)) | ||||
|         with self.lock: | ||||
|             self._poll() | ||||
|             if os.path.isfile(self.buffer_file): | ||||
|                 shutil.copy(self.buffer_file, outfile) | ||||
|             else:  # there was no logcat trace at this time | ||||
|                 with open(outfile, 'w') as _:  # NOQA | ||||
|                     pass | ||||
|  | ||||
|     def close(self): | ||||
|         self.logger.debug('Closing logcat poller.') | ||||
|         if os.path.isfile(self.buffer_file): | ||||
|             os.remove(self.buffer_file) | ||||
|  | ||||
|     def _poll(self): | ||||
|         with self.lock: | ||||
|             self.last_poll = time.time() | ||||
|             adb_command(self.adb_device, 'logcat -d >> {}'.format(self.buffer_file), timeout=self.timeout) | ||||
|             adb_command(self.adb_device, 'logcat -c', timeout=self.timeout) | ||||
|  | ||||
|  | ||||
| class BigLittleDevice(AndroidDevice):  # pylint: disable=W0223 | ||||
|  | ||||
|     parameters = [ | ||||
|         Parameter('scheduler', default='hmp', override=True), | ||||
|     ] | ||||
| @@ -1,684 +0,0 @@ | ||||
| #    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, pxssh | ||||
|  | ||||
| from wlauto import settings, Parameter | ||||
| from wlauto.core.resource import NO_ONE | ||||
| from wlauto.common.resources import Executable | ||||
| 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.m5_path = 'm5' | ||||
|         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<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,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. | ||||
|         attempts = 0 | ||||
|         while attempts < 10: | ||||
|             attempts += 1 | ||||
|             try: | ||||
|                 self.sckt = ssh.TelnetConnection() | ||||
|                 self.sckt.login(host, 'None', port=port, auto_prompt_reset=False, | ||||
|                                 login_timeout=10) | ||||
|                 break | ||||
|             except pxssh.ExceptionPxssh: | ||||
|                 pass | ||||
|         else: | ||||
|             self.gem5.kill() | ||||
|             raise DeviceError("Failed to connect to the gem5 telnet session.") | ||||
|  | ||||
|         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() | ||||
|         self.resize_shell() | ||||
|  | ||||
|     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 | {} grep {}'.format(self.busybox, 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 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!") | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def push_file(self, source, dest, **kwargs): | ||||
|         """ | ||||
|         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)) | ||||
|         if self.busybox: | ||||
|             self.gem5_shell("{} cp /mnt/obb/{} {}".format(self.busybox, filename, dest)) | ||||
|         else: | ||||
|             self.gem5_shell("cat /mnt/obb/{} > {}".format(filename, dest)) | ||||
|         self.gem5_shell("sync") | ||||
|         self.gem5_shell("ls -al {}".format(dest)) | ||||
|         self.gem5_shell("ls -al /mnt/obb/") | ||||
|         self.logger.debug("Push complete.") | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def pull_file(self, source, dest, **kwargs): | ||||
|         """ | ||||
|         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. | ||||
|         """ | ||||
|         filename = os.path.basename(source) | ||||
|  | ||||
|         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("{} cp {} {}".format(self.busybox, source, filename), | ||||
|                         check_exit_code=False) | ||||
|         self.gem5_shell("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.") | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def delete_file(self, filepath, **kwargs): | ||||
|         """ 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 | ||||
|         except ValueError: | ||||
|             # If we cannot process the output, assume that there is no file | ||||
|             pass | ||||
|         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('{} {}'.format(self.m5_path, 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 resize_shell(self): | ||||
|         """ | ||||
|         Resize the shell to avoid line wrapping issues. | ||||
|  | ||||
|         """ | ||||
|         # 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('{} stty columns 1024'.format(self.busybox), check_exit_code=False) | ||||
|         self.gem5_shell('stty cols 1024', check_exit_code=False) | ||||
|         self.gem5_shell('{} stty cols 1024'.format(self.busybox), check_exit_code=False) | ||||
|         self.gem5_shell('reset', check_exit_code=False) | ||||
|  | ||||
|     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('mkdir -p /mnt/obb') | ||||
|  | ||||
|         mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 /mnt/obb".format(self.temp_dir) | ||||
|         self.gem5_shell(mount_command) | ||||
|  | ||||
|     def deploy_m5(self, context, force=False): | ||||
|         """ | ||||
|         Deploys the m5 binary to the device and returns the path to the binary | ||||
|         on the device. | ||||
|  | ||||
|         :param force: by default, if the binary is already present on the | ||||
|                     device, it will not be deployed again. Setting force to | ||||
|                     ``True`` overrides that behaviour and ensures that the | ||||
|                     binary is always copied. Defaults to ``False``. | ||||
|  | ||||
|         :returns: The on-device path to the m5 binary. | ||||
|  | ||||
|         """ | ||||
|         on_device_executable = self.path.join(self.binaries_directory, 'm5') | ||||
|         if not force and self.file_exists(on_device_executable): | ||||
|             # We want to check the version of the binary. We cannot directly | ||||
|             # check this because the m5 binary itself is unversioned. We also | ||||
|             # need to make sure not to check the error code as "m5 --help" | ||||
|             # returns a non-zero error code. | ||||
|             output = self.gem5_shell('m5 --help', check_exit_code=False) | ||||
|             if "writefile" in output: | ||||
|                 self.logger.debug("Using the m5 binary on the device...") | ||||
|                 self.m5_path = on_device_executable | ||||
|                 return on_device_executable | ||||
|             else: | ||||
|                 self.logger.debug("m5 on device does not support writefile!") | ||||
|         host_file = context.resolver.get(Executable(NO_ONE, self.abi, 'm5')) | ||||
|         self.logger.info("Installing the m5 binary to the device...") | ||||
|         self.m5_path = self.install(host_file) | ||||
|         return self.m5_path | ||||
| @@ -1,16 +0,0 @@ | ||||
| #    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. | ||||
| # | ||||
|  | ||||
|  | ||||
| @@ -1,875 +0,0 @@ | ||||
| #    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. | ||||
| # | ||||
|  | ||||
| # pylint: disable=E1101 | ||||
| import os | ||||
| import re | ||||
| import time | ||||
| import socket | ||||
| from collections import namedtuple | ||||
| from subprocess import CalledProcessError | ||||
|  | ||||
| from wlauto.core.extension import Parameter | ||||
| from wlauto.core.device import Device, RuntimeParameter, CoreParameter | ||||
| from wlauto.core.resource import NO_ONE | ||||
| from wlauto.exceptions import ConfigError, DeviceError, TimeoutError, DeviceNotRespondingError | ||||
| from wlauto.common.resources import Executable | ||||
| from wlauto.utils.cpuinfo import Cpuinfo | ||||
| from wlauto.utils.misc import convert_new_lines, escape_double_quotes, ranges_to_list, ABI_MAP | ||||
| from wlauto.utils.misc import isiterable, list_to_mask | ||||
| from wlauto.utils.ssh import SshShell | ||||
| from wlauto.utils.types import boolean, list_of_strings | ||||
|  | ||||
|  | ||||
| FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (\S+) type (\S+) \((\S+)\)') | ||||
|  | ||||
| FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num']) | ||||
| PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name') | ||||
| LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by']) | ||||
|  | ||||
|  | ||||
| class BaseLinuxDevice(Device):  # pylint: disable=abstract-method | ||||
|  | ||||
|     path_module = 'posixpath' | ||||
|     has_gpu = True | ||||
|  | ||||
|     parameters = [ | ||||
|         Parameter('scheduler', kind=str, default='unknown', | ||||
|                   allowed_values=['unknown', 'smp', 'hmp', 'iks', 'ea', 'other'], | ||||
|                   description=""" | ||||
|                   Specifies the type of multi-core scheduling model utilized in the device. The value | ||||
|                   must be one of the following: | ||||
|  | ||||
|                   :unknown: A generic Device interface is used to interact with the underlying device | ||||
|                             and the underlying scheduling model is unkown. | ||||
|                   :smp: A standard single-core or Symmetric Multi-Processing system. | ||||
|                   :hmp: ARM Heterogeneous Multi-Processing system. | ||||
|                   :iks: Linaro In-Kernel Switcher. | ||||
|                   :ea: ARM Energy-Aware scheduler. | ||||
|                   :other: Any other system not covered by the above. | ||||
|  | ||||
|                           .. note:: most currently-available systems would fall under ``smp`` rather than | ||||
|                                     this value. ``other`` is there to future-proof against new schemes | ||||
|                                     not yet covered by WA. | ||||
|  | ||||
|                   """), | ||||
|         Parameter('iks_switch_frequency', kind=int, default=None, | ||||
|                   description=""" | ||||
|                  This is the switching frequency, in kilohertz, of IKS devices. This parameter *MUST NOT* | ||||
|                  be set for non-IKS device (i.e. ``scheduler != 'iks'``). If left unset for IKS devices, | ||||
|                  it will default to ``800000``, i.e. 800MHz. | ||||
|                  """), | ||||
|         Parameter('property_files', kind=list_of_strings, | ||||
|                   default=[ | ||||
|                       '/etc/arch-release', | ||||
|                       '/etc/debian_version', | ||||
|                       '/etc/lsb-release', | ||||
|                       '/proc/config.gz', | ||||
|                       '/proc/cmdline', | ||||
|                       '/proc/cpuinfo', | ||||
|                       '/proc/version', | ||||
|                       '/proc/zconfig', | ||||
|                       '/sys/kernel/debug/sched_features', | ||||
|                       '/sys/kernel/hmp', | ||||
|                   ], | ||||
|                   description=''' | ||||
|                   A list of paths to files containing static OS properties. These will be pulled into the | ||||
|                   __meta directory in output for each run in order to provide information about the platfrom. | ||||
|                   These paths do not have to exist and will be ignored if the path is not present on a | ||||
|                   particular device. | ||||
|                   '''), | ||||
|         Parameter('binaries_directory', | ||||
|                   description='Location of executable binaries on this device (must be in PATH).'), | ||||
|  | ||||
|     ] | ||||
|  | ||||
|     runtime_parameters = [ | ||||
|         RuntimeParameter('sysfile_values', 'get_sysfile_values', 'set_sysfile_values', value_name='params'), | ||||
|         CoreParameter('${core}_cores', 'get_number_of_online_cpus', 'set_number_of_online_cpus', | ||||
|                       value_name='number'), | ||||
|         CoreParameter('${core}_min_frequency', 'get_core_min_frequency', 'set_core_min_frequency', | ||||
|                       value_name='freq'), | ||||
|         CoreParameter('${core}_max_frequency', 'get_core_max_frequency', 'set_core_max_frequency', | ||||
|                       value_name='freq'), | ||||
|         CoreParameter('${core}_frequency', 'get_core_cur_frequency', 'set_core_cur_frequency', | ||||
|                       value_name='freq'), | ||||
|         CoreParameter('${core}_governor', 'get_core_governor', 'set_core_governor', | ||||
|                       value_name='governor'), | ||||
|         CoreParameter('${core}_governor_tunables', 'get_core_governor_tunables', 'set_core_governor_tunables', | ||||
|                       value_name='tunables'), | ||||
|     ] | ||||
|  | ||||
|     dynamic_modules = [ | ||||
|         'devcpufreq', | ||||
|         'cpuidle', | ||||
|     ] | ||||
|  | ||||
|     @property | ||||
|     def abi(self): | ||||
|         if not self._abi: | ||||
|             val = self.execute('uname -m').strip() | ||||
|             for abi, architectures in ABI_MAP.iteritems(): | ||||
|                 if val in architectures: | ||||
|                     self._abi = abi | ||||
|                     break | ||||
|             else: | ||||
|                 self._abi = val | ||||
|         return self._abi | ||||
|  | ||||
|     @property | ||||
|     def online_cpus(self): | ||||
|         val = self.get_sysfile_value('/sys/devices/system/cpu/online') | ||||
|         return ranges_to_list(val) | ||||
|  | ||||
|     @property | ||||
|     def number_of_cores(self): | ||||
|         """ | ||||
|         Added in version 2.1.4. | ||||
|  | ||||
|         """ | ||||
|         if self._number_of_cores is None: | ||||
|             corere = re.compile(r'^\s*cpu\d+\s*$') | ||||
|             output = self.execute('ls /sys/devices/system/cpu') | ||||
|             self._number_of_cores = 0 | ||||
|             for entry in output.split(): | ||||
|                 if corere.match(entry): | ||||
|                     self._number_of_cores += 1 | ||||
|         return self._number_of_cores | ||||
|  | ||||
|     @property | ||||
|     def resource_cache(self): | ||||
|         return self.path.join(self.working_directory, '.cache') | ||||
|  | ||||
|     @property | ||||
|     def file_transfer_cache(self): | ||||
|         return self.path.join(self.working_directory, '.transfer') | ||||
|  | ||||
|     @property | ||||
|     def cpuinfo(self): | ||||
|         if not self._cpuinfo: | ||||
|             self._cpuinfo = Cpuinfo(self.execute('cat /proc/cpuinfo')) | ||||
|         return self._cpuinfo | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         super(BaseLinuxDevice, self).__init__(**kwargs) | ||||
|         self.busybox = None | ||||
|         self._is_initialized = False | ||||
|         self._is_ready = False | ||||
|         self._just_rebooted = False | ||||
|         self._is_rooted = None | ||||
|         self._is_root_user = False | ||||
|         self._available_frequencies = {} | ||||
|         self._available_governors = {} | ||||
|         self._available_governor_tunables = {} | ||||
|         self._number_of_cores = None | ||||
|         self._written_sysfiles = [] | ||||
|         self._cpuinfo = None | ||||
|         self._abi = None | ||||
|  | ||||
|     def validate(self): | ||||
|         if self.iks_switch_frequency is not None and self.scheduler != 'iks':  # pylint: disable=E0203 | ||||
|             raise ConfigError('iks_switch_frequency must NOT be set for non-IKS devices.') | ||||
|         if self.iks_switch_frequency is None and self.scheduler == 'iks':  # pylint: disable=E0203 | ||||
|             self.iks_switch_frequency = 800000  # pylint: disable=W0201 | ||||
|  | ||||
|     def initialize(self, context): | ||||
|         self.execute('mkdir -p {}'.format(self.working_directory)) | ||||
|         if not self.binaries_directory: | ||||
|             self._set_binaries_dir() | ||||
|         self.execute('mkdir -p {}'.format(self.binaries_directory)) | ||||
|         self.busybox = self.deploy_busybox(context) | ||||
|  | ||||
|     def _set_binaries_dir(self): | ||||
|         # pylint: disable=attribute-defined-outside-init | ||||
|         self.binaries_directory = self.path.join(self.working_directory, "bin") | ||||
|  | ||||
|     def is_file(self, filepath): | ||||
|         output = self.execute('if [ -f \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath)) | ||||
|         # output from ssh my contain part of the expression in the buffer, | ||||
|         # split out everything except the last word. | ||||
|         return boolean(output.split()[-1])  # pylint: disable=maybe-no-member | ||||
|  | ||||
|     def is_directory(self, filepath): | ||||
|         output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath)) | ||||
|         # output from ssh my contain part of the expression in the buffer, | ||||
|         # split out everything except the last word. | ||||
|         return boolean(output.split()[-1])  # pylint: disable=maybe-no-member | ||||
|  | ||||
|     def get_properties(self, context): | ||||
|         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): | ||||
|                     with open(outfile, 'w') as wfh: | ||||
|                         wfh.write(self.execute('cat {}'.format(propfile))) | ||||
|                 elif self.is_directory(propfile): | ||||
|                     self.pull_file(propfile, outfile) | ||||
|                 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_sysfile_value(self, sysfile, kind=None): | ||||
|         """ | ||||
|         Get the contents of the specified sysfile. | ||||
|  | ||||
|         :param sysfile: The file who's contents will be returned. | ||||
|  | ||||
|         :param kind: The type of value to be expected in the sysfile. This can | ||||
|                      be any Python callable that takes a single str argument. | ||||
|                      If not specified or is None, the contents will be returned | ||||
|                      as a string. | ||||
|  | ||||
|         """ | ||||
|         output = self.execute('cat \'{}\''.format(sysfile), as_root=self.is_rooted).strip()  # pylint: disable=E1103 | ||||
|         if kind: | ||||
|             return kind(output) | ||||
|         else: | ||||
|             return output | ||||
|  | ||||
|     def set_sysfile_value(self, sysfile, value, verify=True): | ||||
|         """ | ||||
|         Set the value of the specified sysfile. By default, the value will be checked afterwards. | ||||
|         Can be overridden by setting ``verify`` parameter to ``False``. | ||||
|  | ||||
|         """ | ||||
|         value = str(value) | ||||
|         self.execute('echo {} > \'{}\''.format(value, sysfile), check_exit_code=False, as_root=True) | ||||
|         if verify: | ||||
|             output = self.get_sysfile_value(sysfile) | ||||
|             if output.strip() != value:  # pylint: disable=E1103 | ||||
|                 message = 'Could not set the value of {} to {}'.format(sysfile, value) | ||||
|                 raise DeviceError(message) | ||||
|         self._written_sysfiles.append(sysfile) | ||||
|  | ||||
|     def get_sysfile_values(self): | ||||
|         """ | ||||
|         Returns a dict mapping paths of sysfiles that were previously set to their | ||||
|         current values. | ||||
|  | ||||
|         """ | ||||
|         values = {} | ||||
|         for sysfile in self._written_sysfiles: | ||||
|             values[sysfile] = self.get_sysfile_value(sysfile) | ||||
|         return values | ||||
|  | ||||
|     def set_sysfile_values(self, params): | ||||
|         """ | ||||
|         The plural version of ``set_sysfile_value``. Takes a single parameter which is a mapping of | ||||
|         file paths to values to be set. By default, every value written will be verified. The can | ||||
|         be disabled for individual paths by appending ``'!'`` to them. | ||||
|  | ||||
|         """ | ||||
|         for sysfile, value in params.iteritems(): | ||||
|             verify = not sysfile.endswith('!') | ||||
|             sysfile = sysfile.rstrip('!') | ||||
|             self.set_sysfile_value(sysfile, value, verify=verify) | ||||
|  | ||||
|     def deploy_busybox(self, context, force=False): | ||||
|         """ | ||||
|         Deploys the busybox binary to the specified device and returns | ||||
|         the path to the binary on the device. | ||||
|  | ||||
|         :param context: an instance of ExecutionContext | ||||
|         :param force: by default, if the binary is already present on the | ||||
|                     device, it will not be deployed again. Setting force | ||||
|                     to ``True`` overrides that behavior and ensures that the | ||||
|                     binary is always copied. Defaults to ``False``. | ||||
|  | ||||
|         :returns: The on-device path to the busybox binary. | ||||
|  | ||||
|         """ | ||||
|         on_device_executable = self.get_binary_path("busybox", search_system_binaries=False) | ||||
|         if force or not on_device_executable: | ||||
|             host_file = context.resolver.get(Executable(NO_ONE, self.abi, 'busybox')) | ||||
|             return self.install(host_file) | ||||
|         return on_device_executable | ||||
|  | ||||
|     def is_installed(self, name):  # pylint: disable=unused-argument,no-self-use | ||||
|         raise AttributeError("""Instead of using is_installed, please use | ||||
|             ``get_binary_path`` or ``install_if_needed`` instead. You should | ||||
|             use the path returned by these functions to then invoke the binary | ||||
|  | ||||
|             please see: https://pythonhosted.org/wlauto/writing_extensions.html""") | ||||
|  | ||||
|     def get_binary_path(self, name, search_system_binaries=True): | ||||
|         """ | ||||
|         Searches the devices ``binary_directory`` for the given binary, | ||||
|         if it cant find it there it tries using which to find it. | ||||
|  | ||||
|         :param name: The name of the binary | ||||
|         :param search_system_binaries: By default this function will try using | ||||
|                                        which to find the binary if it isn't in | ||||
|                                        ``binary_directory``. When this is set | ||||
|                                        to ``False`` it will not try this. | ||||
|  | ||||
|         :returns: The on-device path to the binary. | ||||
|  | ||||
|         """ | ||||
|         wa_binary = self.path.join(self.binaries_directory, name) | ||||
|         if self.file_exists(wa_binary): | ||||
|             return wa_binary | ||||
|         if search_system_binaries: | ||||
|             try: | ||||
|                 return self.execute('{} which {}'.format(self.busybox, name)).strip() | ||||
|             except DeviceError: | ||||
|                 pass | ||||
|         return None | ||||
|  | ||||
|     def install_if_needed(self, host_path, search_system_binaries=True): | ||||
|         """ | ||||
|         Similar to get_binary_path but will install the binary if not found. | ||||
|  | ||||
|         :param host_path: The path to the binary on the host | ||||
|         :param search_system_binaries: By default this function will try using | ||||
|                                        which to find the binary if it isn't in | ||||
|                                        ``binary_directory``. When this is set | ||||
|                                        to ``False`` it will not try this. | ||||
|  | ||||
|         :returns: The on-device path to the binary. | ||||
|  | ||||
|         """ | ||||
|         binary_path = self.get_binary_path(os.path.split(host_path)[1], | ||||
|                                            search_system_binaries=search_system_binaries) | ||||
|         if not binary_path: | ||||
|             binary_path = self.install(host_path) | ||||
|         return binary_path | ||||
|  | ||||
|     def list_file_systems(self): | ||||
|         output = self.execute('mount') | ||||
|         fstab = [] | ||||
|         for line in output.split('\n'): | ||||
|             line = line.strip() | ||||
|             if not line: | ||||
|                 continue | ||||
|             match = FSTAB_ENTRY_REGEX.search(line) | ||||
|             if match: | ||||
|                 fstab.append(FstabEntry(match.group(1), match.group(2), | ||||
|                                         match.group(3), match.group(4), | ||||
|                                         None, None)) | ||||
|             else:  # assume pre-M Android | ||||
|                 fstab.append(FstabEntry(*line.split())) | ||||
|         return fstab | ||||
|  | ||||
|     # Process query and control | ||||
|  | ||||
|     def get_pids_of(self, process_name): | ||||
|         raise NotImplementedError() | ||||
|  | ||||
|     def ps(self, **kwargs): | ||||
|         raise NotImplementedError() | ||||
|  | ||||
|     def kill(self, pid, signal=None, as_root=False):  # pylint: disable=W0221 | ||||
|         """ | ||||
|         Kill the specified process. | ||||
|  | ||||
|             :param pid: PID of the process to kill. | ||||
|             :param signal: Specify which singal to send to the process. This must | ||||
|                            be a valid value for -s option of kill. Defaults to ``None``. | ||||
|  | ||||
|         Modified in version 2.1.4: added ``signal`` parameter. | ||||
|  | ||||
|         """ | ||||
|         signal_string = '-s {}'.format(signal) if signal else '' | ||||
|         self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root) | ||||
|  | ||||
|     def killall(self, process_name, signal=None, as_root=False):  # pylint: disable=W0221 | ||||
|         """ | ||||
|         Kill all processes with the specified name. | ||||
|  | ||||
|             :param process_name: The name of the process(es) to kill. | ||||
|             :param signal: Specify which singal to send to the process. This must | ||||
|                            be a valid value for -s option of kill. Defaults to ``None``. | ||||
|  | ||||
|         Modified in version 2.1.5: added ``as_root`` parameter. | ||||
|  | ||||
|         """ | ||||
|         for pid in self.get_pids_of(process_name): | ||||
|             self.kill(pid, signal=signal, as_root=as_root) | ||||
|  | ||||
|     def get_online_cpus(self, c): | ||||
|         if isinstance(c, int):  # assume c == cluster | ||||
|             return [i for i in self.online_cpus if self.core_clusters[i] == c] | ||||
|         elif isinstance(c, basestring):  # assume c == core | ||||
|             return [i for i in self.online_cpus if self.core_names[i] == c] | ||||
|         else: | ||||
|             raise ValueError(c) | ||||
|  | ||||
|     def get_number_of_online_cpus(self, c): | ||||
|         return len(self.get_online_cpus(c)) | ||||
|  | ||||
|     def set_number_of_online_cpus(self, core, number): | ||||
|         core_ids = [i for i, c in enumerate(self.core_names) if c == core] | ||||
|         max_cores = len(core_ids) | ||||
|         if number > max_cores: | ||||
|             message = 'Attempting to set the number of active {} to {}; maximum is {}' | ||||
|             raise ValueError(message.format(core, number, max_cores)) | ||||
|         for i in xrange(0, number): | ||||
|             self.enable_cpu(core_ids[i]) | ||||
|         for i in xrange(number, max_cores): | ||||
|             self.disable_cpu(core_ids[i]) | ||||
|  | ||||
|     # hotplug | ||||
|  | ||||
|     def enable_cpu(self, cpu): | ||||
|         """ | ||||
|         Enable the specified core. | ||||
|  | ||||
|         :param cpu: CPU core to enable. This must be the full name as it | ||||
|                     appears in sysfs, e.g. "cpu0". | ||||
|  | ||||
|         """ | ||||
|         self.hotplug_cpu(cpu, online=True) | ||||
|  | ||||
|     def disable_cpu(self, cpu): | ||||
|         """ | ||||
|         Disable the specified core. | ||||
|  | ||||
|         :param cpu: CPU core to disable. This must be the full name as it | ||||
|                     appears in sysfs, e.g. "cpu0". | ||||
|         """ | ||||
|         self.hotplug_cpu(cpu, online=False) | ||||
|  | ||||
|     def hotplug_cpu(self, cpu, online): | ||||
|         """ | ||||
|         Hotplug the specified CPU either on or off. | ||||
|         See https://www.kernel.org/doc/Documentation/cpu-hotplug.txt | ||||
|  | ||||
|         :param cpu: The CPU for which the governor is to be set. This must be | ||||
|                     the full name as it appears in sysfs, e.g. "cpu0". | ||||
|         :param online: CPU will be enabled if this value bool()'s to True, and | ||||
|                        will be disabled otherwise. | ||||
|  | ||||
|         """ | ||||
|         if isinstance(cpu, int): | ||||
|             cpu = 'cpu{}'.format(cpu) | ||||
|         status = 1 if online else 0 | ||||
|         sysfile = '/sys/devices/system/cpu/{}/online'.format(cpu) | ||||
|         self.set_sysfile_value(sysfile, status) | ||||
|  | ||||
|     def get_number_of_active_cores(self, core): | ||||
|         if core not in self.core_names: | ||||
|             raise ValueError('Unexpected core: {}; must be in {}'.format(core, list(set(self.core_names)))) | ||||
|         active_cpus = self.active_cpus | ||||
|         num_active_cores = 0 | ||||
|         for i, c in enumerate(self.core_names): | ||||
|             if c == core and i in active_cpus: | ||||
|                 num_active_cores += 1 | ||||
|         return num_active_cores | ||||
|  | ||||
|     def set_number_of_active_cores(self, core, number):  # NOQA | ||||
|         if core not in self.core_names: | ||||
|             raise ValueError('Unexpected core: {}; must be in {}'.format(core, list(set(self.core_names)))) | ||||
|         core_ids = [i for i, c in enumerate(self.core_names) if c == core] | ||||
|         max_cores = len(core_ids) | ||||
|         if number > max_cores: | ||||
|             message = 'Attempting to set the number of active {} to {}; maximum is {}' | ||||
|             raise ValueError(message.format(core, number, max_cores)) | ||||
|  | ||||
|         if not number: | ||||
|             # make sure at least one other core is enabled to avoid trying to | ||||
|             # hotplug everything. | ||||
|             for i, c in enumerate(self.core_names): | ||||
|                 if c != core: | ||||
|                     self.enable_cpu(i) | ||||
|                     break | ||||
|             else:  # did not find one | ||||
|                 raise ValueError('Cannot hotplug all cpus on the device!') | ||||
|  | ||||
|         for i in xrange(0, number): | ||||
|             self.enable_cpu(core_ids[i]) | ||||
|         for i in xrange(number, max_cores): | ||||
|             self.disable_cpu(core_ids[i]) | ||||
|  | ||||
|     def invoke(self, binary, args=None, in_directory=None, on_cpus=None, | ||||
|                background=False, as_root=False, timeout=30): | ||||
|         """ | ||||
|         Executes the specified binary under the specified conditions. | ||||
|  | ||||
|         :binary: binary to execute. Must be present and executable on the device. | ||||
|         :args: arguments to be passed to the binary. The can be either a list or | ||||
|                a string. | ||||
|         :in_directory:  execute the binary in the  specified directory. This must | ||||
|                         be an absolute path. | ||||
|         :on_cpus:  taskset the binary to these CPUs. This may be a single ``int`` (in which | ||||
|                    case, it will be interpreted as the mask), a list of ``ints``, in which | ||||
|                    case this will be interpreted as the list of cpus, or string, which | ||||
|                    will be interpreted as a comma-separated list of cpu ranges, e.g. | ||||
|                    ``"0,4-7"``. | ||||
|         :background: If ``True``, a ``subprocess.Popen`` object will be returned straight | ||||
|                      away. If ``False`` (the default), this will wait for the command to | ||||
|                      terminate and return the STDOUT output | ||||
|         :as_root: Specify whether the command should be run as root | ||||
|         :timeout: If the invocation does not terminate within this number of seconds, | ||||
|                   a ``TimeoutError`` exception will be raised. Set to ``None`` if the | ||||
|                   invocation should not timeout. | ||||
|  | ||||
|         """ | ||||
|         command = binary | ||||
|         if args: | ||||
|             if isiterable(args): | ||||
|                 args = ' '.join(args) | ||||
|             command = '{} {}'.format(command, args) | ||||
|         if on_cpus: | ||||
|             if isinstance(on_cpus, basestring): | ||||
|                 on_cpus = ranges_to_list(on_cpus) | ||||
|             if isiterable(on_cpus): | ||||
|                 on_cpus = list_to_mask(on_cpus)  # pylint: disable=redefined-variable-type | ||||
|             command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command) | ||||
|         if in_directory: | ||||
|             command = 'cd {} && {}'.format(in_directory, command) | ||||
|         return self.execute(command, background=background, as_root=as_root, timeout=timeout) | ||||
|  | ||||
|     def get_device_model(self): | ||||
|         if self.file_exists("/proc/device-tree/model"): | ||||
|             raw_model = self.execute("cat /proc/device-tree/model") | ||||
|             return '_'.join(raw_model.split()[:2]) | ||||
|         # Right now we don't know any other way to get device model | ||||
|         # info in linux on arm platforms | ||||
|         return None | ||||
|  | ||||
|     # internal methods | ||||
|  | ||||
|     def _check_ready(self): | ||||
|         if not self._is_ready: | ||||
|             raise AttributeError('Device not ready.') | ||||
|  | ||||
|     def _get_core_cluster(self, core): | ||||
|         """Returns the first cluster that has cores of the specified type. Raises | ||||
|         value error if no cluster for the specified type has been found""" | ||||
|         core_indexes = [i for i, c in enumerate(self.core_names) if c == core] | ||||
|         core_clusters = set(self.core_clusters[i] for i in core_indexes) | ||||
|         if not core_clusters: | ||||
|             raise ValueError('No cluster found for core {}'.format(core)) | ||||
|         return sorted(list(core_clusters))[0] | ||||
|  | ||||
|  | ||||
| class LinuxDevice(BaseLinuxDevice): | ||||
|  | ||||
|     platform = 'linux' | ||||
|  | ||||
|     default_timeout = 30 | ||||
|     delay = 2 | ||||
|     long_delay = 3 * delay | ||||
|     ready_timeout = 60 | ||||
|  | ||||
|     parameters = [ | ||||
|         Parameter('host', mandatory=True, description='Host name or IP address for the device.'), | ||||
|         Parameter('username', mandatory=True, description='User name for the account on the device.'), | ||||
|         Parameter('password', description='Password for the account on the device (for password-based auth).'), | ||||
|         Parameter('keyfile', description='Keyfile to be used for key-based authentication.'), | ||||
|         Parameter('port', kind=int, default=22, description='SSH port number on the device.'), | ||||
|         Parameter('password_prompt', default='[sudo] password', | ||||
|                   description='Prompt presented by sudo when requesting the password.'), | ||||
|  | ||||
|         Parameter('use_telnet', kind=boolean, default=False, | ||||
|                   description='Optionally, telnet may be used instead of ssh, though this is discouraged.'), | ||||
|         Parameter('boot_timeout', kind=int, default=120, | ||||
|                   description='How long to try to connect to the device after a reboot.'), | ||||
|  | ||||
|         Parameter('working_directory', default=None, | ||||
|                   description=''' | ||||
|                   Working directory to be used by WA. This must be in a location where the specified user | ||||
|                   has write permissions. This will default to /home/<username>/wa (or to /root/wa, if | ||||
|                   username is 'root'). | ||||
|                   '''), | ||||
|     ] | ||||
|  | ||||
|     @property | ||||
|     def is_rooted(self): | ||||
|         if self._is_rooted is None: | ||||
|             # First check if the user is root | ||||
|             try: | ||||
|                 self.execute('test $(id -u) = 0') | ||||
|                 self._is_root_user = True | ||||
|                 self._is_rooted = True | ||||
|                 return self._is_rooted | ||||
|             except DeviceError: | ||||
|                 self._is_root_user = False | ||||
|  | ||||
|             # Otherwise, check if the user has sudo rights | ||||
|             try: | ||||
|                 self.execute('ls /', as_root=True) | ||||
|                 self._is_rooted = True | ||||
|             except DeviceError: | ||||
|                 self._is_rooted = False | ||||
|         return self._is_rooted | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super(LinuxDevice, self).__init__(*args, **kwargs) | ||||
|         self.shell = None | ||||
|         self._is_rooted = None | ||||
|  | ||||
|     def validate(self): | ||||
|         if self.working_directory is None:  # pylint: disable=access-member-before-definition | ||||
|             if self.username == 'root': | ||||
|                 self.working_directory = '/root/wa'  # pylint: disable=attribute-defined-outside-init | ||||
|             else: | ||||
|                 self.working_directory = '/home/{}/wa'.format(self.username)  # pylint: disable=attribute-defined-outside-init | ||||
|  | ||||
|     def initialize(self, context, *args, **kwargs): | ||||
|         self.execute('mkdir -p {}'.format(self.binaries_directory)) | ||||
|         self.execute('export PATH={}:$PATH'.format(self.binaries_directory)) | ||||
|         super(LinuxDevice, self).initialize(context, *args, **kwargs) | ||||
|  | ||||
|     # Power control | ||||
|  | ||||
|     def reset(self): | ||||
|         self.execute('reboot', as_root=True) | ||||
|         self._is_ready = False | ||||
|  | ||||
|     def hard_reset(self): | ||||
|         self._is_ready = False | ||||
|  | ||||
|     def boot(self, hard=False, **kwargs): | ||||
|         if hard: | ||||
|             self.hard_reset() | ||||
|         else: | ||||
|             self.reset() | ||||
|         self.logger.debug('Waiting for device...') | ||||
|         start_time = time.time() | ||||
|         while (time.time() - start_time) < self.boot_timeout: | ||||
|             try: | ||||
|                 s = socket.create_connection((self.host, self.port), timeout=5) | ||||
|                 s.close() | ||||
|                 break | ||||
|             except socket.timeout: | ||||
|                 pass | ||||
|             except socket.error: | ||||
|                 time.sleep(5) | ||||
|         else: | ||||
|             raise DeviceError('Could not connect to {} after reboot'.format(self.host)) | ||||
|  | ||||
|     def connect(self):  # NOQA pylint: disable=R0912 | ||||
|         self.shell = SshShell(password_prompt=self.password_prompt, | ||||
|                               timeout=self.default_timeout, telnet=self.use_telnet) | ||||
|         self.shell.login(self.host, self.username, self.password, self.keyfile, self.port) | ||||
|         self._is_ready = True | ||||
|  | ||||
|     def disconnect(self):  # NOQA pylint: disable=R0912 | ||||
|         self.shell.logout() | ||||
|         self._is_ready = False | ||||
|  | ||||
|     # Execution | ||||
|  | ||||
|     def has_root(self): | ||||
|         try: | ||||
|             self.execute('ls /', as_root=True) | ||||
|             return True | ||||
|         except DeviceError as e: | ||||
|             if 'not in the sudoers file' not in e.message: | ||||
|                 raise e | ||||
|             return False | ||||
|  | ||||
|     def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False, | ||||
|                 as_root=False, strip_colors=True, **kwargs): | ||||
|         """ | ||||
|         Execute the specified command on the device using adb. | ||||
|  | ||||
|         Parameters: | ||||
|  | ||||
|             :param command: The command to be executed. It should appear exactly | ||||
|                             as if you were typing it into a shell. | ||||
|             :param timeout: Time, in seconds, to wait for adb to return before aborting | ||||
|                             and raising an error. Defaults to ``AndroidDevice.default_timeout``. | ||||
|             :param check_exit_code: If ``True``, the return code of the command on the Device will | ||||
|                                     be check and exception will be raised if it is not 0. | ||||
|                                     Defaults to ``True``. | ||||
|             :param background: If ``True``, will execute create a new ssh shell rather than using | ||||
|                                the default session and will return it immediately. If this is ``True``, | ||||
|                                ``timeout``, ``strip_colors`` and (obvisously) ``check_exit_code`` will | ||||
|                                be ignored; also, with this, ``as_root=True``  is only valid if ``username`` | ||||
|                                for the device was set to ``root``. | ||||
|             :param as_root: If ``True``, will attempt to execute command in privileged mode. The device | ||||
|                             must be rooted, otherwise an error will be raised. Defaults to ``False``. | ||||
|  | ||||
|                             Added in version 2.1.3 | ||||
|  | ||||
|         :returns: If ``background`` parameter is set to ``True``, the subprocess object will | ||||
|                   be returned; otherwise, the contents of STDOUT from the device will be returned. | ||||
|  | ||||
|         """ | ||||
|         self._check_ready() | ||||
|         try: | ||||
|             if background: | ||||
|                 if as_root and self.username != 'root': | ||||
|                     raise DeviceError('Cannot execute in background with as_root=True unless user is root.') | ||||
|                 return self.shell.background(command) | ||||
|             else: | ||||
|                 # If we're already the root user, don't bother with sudo | ||||
|                 if self._is_root_user: | ||||
|                     as_root = False | ||||
|                 return self.shell.execute(command, timeout, check_exit_code, as_root, strip_colors) | ||||
|         except CalledProcessError as e: | ||||
|             raise DeviceError(e) | ||||
|  | ||||
|     def kick_off(self, command, as_root=False): | ||||
|         """ | ||||
|         Like execute but closes adb session and returns immediately, leaving the command running on the | ||||
|         device (this is different from execute(background=True) which keeps adb connection open and returns | ||||
|         a subprocess object). | ||||
|  | ||||
|         """ | ||||
|         self._check_ready() | ||||
|         command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command)) | ||||
|         return self.shell.execute(command, as_root=as_root) | ||||
|  | ||||
|     def get_pids_of(self, process_name): | ||||
|         """Returns a list of PIDs of all processes with the specified name.""" | ||||
|         # result should be a column of PIDs with the first row as "PID" header | ||||
|         result = self.execute('ps -C {} -o pid'.format(process_name),  # NOQA | ||||
|                               check_exit_code=False).strip().split() | ||||
|         if len(result) >= 2:  # at least one row besides the header | ||||
|             return map(int, result[1:]) | ||||
|         else: | ||||
|             return [] | ||||
|  | ||||
|     def ps(self, **kwargs): | ||||
|         command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname' | ||||
|         lines = iter(convert_new_lines(self.execute(command)).split('\n')) | ||||
|         lines.next()  # header | ||||
|  | ||||
|         result = [] | ||||
|         for line in lines: | ||||
|             parts = re.split(r'\s+', line, maxsplit=8) | ||||
|             if parts: | ||||
|                 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:]))) | ||||
|  | ||||
|         if not kwargs: | ||||
|             return result | ||||
|         else: | ||||
|             filtered_result = [] | ||||
|             for entry in result: | ||||
|                 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()): | ||||
|                     filtered_result.append(entry) | ||||
|             return filtered_result | ||||
|  | ||||
|     # File management | ||||
|  | ||||
|     def push_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221 | ||||
|         self._check_ready() | ||||
|         try: | ||||
|             if not as_root or self.username == 'root': | ||||
|                 self.shell.push_file(source, dest, timeout=timeout) | ||||
|             else: | ||||
|                 tempfile = self.path.join(self.working_directory, self.path.basename(dest)) | ||||
|                 self.shell.push_file(source, tempfile, timeout=timeout) | ||||
|                 self.shell.execute('cp -r {} {}'.format(tempfile, dest), timeout=timeout, as_root=True) | ||||
|         except CalledProcessError as e: | ||||
|             raise DeviceError(e) | ||||
|  | ||||
|     def pull_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221 | ||||
|         self._check_ready() | ||||
|         try: | ||||
|             if not as_root or self.username == 'root': | ||||
|                 self.shell.pull_file(source, dest, timeout=timeout) | ||||
|             else: | ||||
|                 tempfile = self.path.join(self.working_directory, self.path.basename(source)) | ||||
|                 self.shell.execute('cp -r {} {}'.format(source, tempfile), timeout=timeout, as_root=True) | ||||
|                 self.shell.execute('chown -R {} {}'.format(self.username, tempfile), timeout=timeout, as_root=True) | ||||
|                 self.shell.pull_file(tempfile, dest, timeout=timeout) | ||||
|         except CalledProcessError as e: | ||||
|             raise DeviceError(e) | ||||
|  | ||||
|     def delete_file(self, filepath, as_root=False):  # pylint: disable=W0221 | ||||
|         self.execute('rm -rf {}'.format(filepath), as_root=as_root) | ||||
|  | ||||
|     def file_exists(self, filepath): | ||||
|         output = self.execute('if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath)) | ||||
|         # output from ssh my contain part of the expression in the buffer, | ||||
|         # split out everything except the last word. | ||||
|         return boolean(output.split()[-1])  # pylint: disable=maybe-no-member | ||||
|  | ||||
|     def listdir(self, path, as_root=False, **kwargs): | ||||
|         contents = self.execute('ls -1 {}'.format(path), as_root=as_root).strip() | ||||
|         if not contents: | ||||
|             return [] | ||||
|         return [x.strip() for x in contents.split('\n')]  # pylint: disable=maybe-no-member | ||||
|  | ||||
|     def install(self, filepath, timeout=default_timeout, with_name=None):  # pylint: disable=W0221 | ||||
|         destpath = self.path.join(self.binaries_directory, | ||||
|                                   with_name or self.path.basename(filepath)) | ||||
|         self.push_file(filepath, destpath, as_root=True) | ||||
|         self.execute('chmod a+x {}'.format(destpath), timeout=timeout, as_root=True) | ||||
|         return destpath | ||||
|  | ||||
|     install_executable = install  # compatibility | ||||
|  | ||||
|     def uninstall(self, executable_name): | ||||
|         on_device_executable = self.get_binary_path(executable_name, search_system_binaries=False) | ||||
|         if not on_device_executable: | ||||
|             raise DeviceError("Could not uninstall {}, binary not found".format(on_device_executable)) | ||||
|         self.delete_file(on_device_executable, as_root=self.is_rooted) | ||||
|  | ||||
|     uninstall_executable = uninstall  # compatibility | ||||
|  | ||||
|     # misc | ||||
|  | ||||
|     def lsmod(self): | ||||
|         """List loaded kernel modules.""" | ||||
|         lines = self.execute('lsmod').splitlines() | ||||
|         entries = [] | ||||
|         for line in lines[1:]:  # first line is the header | ||||
|             if not line.strip(): | ||||
|                 continue | ||||
|             parts = line.split() | ||||
|             name = parts[0] | ||||
|             size = int(parts[1]) | ||||
|             use_count = int(parts[2]) | ||||
|             if len(parts) > 3: | ||||
|                 used_by = ''.join(parts[3:]).split(',') | ||||
|             else: | ||||
|                 used_by = [] | ||||
|             entries.append(LsmodEntry(name, size, use_count, used_by)) | ||||
|         return entries | ||||
|  | ||||
|     def insmod(self, path): | ||||
|         """Install a kernel module located on the host on the target device.""" | ||||
|         target_path = self.path.join(self.working_directory, os.path.basename(path)) | ||||
|         self.push_file(path, target_path) | ||||
|         self.execute('insmod {}'.format(target_path), as_root=True) | ||||
|  | ||||
|     def ping(self): | ||||
|         try: | ||||
|             # May be triggered inside initialize() | ||||
|             self.shell.execute('ls /', timeout=5) | ||||
|         except (TimeoutError, CalledProcessError): | ||||
|             raise DeviceNotRespondingError(self.host) | ||||
|  | ||||
|     def capture_screen(self, filepath): | ||||
|         if not self.get_binary_path('scrot'): | ||||
|             self.logger.debug('Could not take screenshot as scrot is not installed.') | ||||
|             return | ||||
|         try: | ||||
|             tempfile = self.path.join(self.working_directory, os.path.basename(filepath)) | ||||
|             self.execute('DISPLAY=:0.0 scrot {}'.format(tempfile)) | ||||
|             self.pull_file(tempfile, filepath) | ||||
|             self.delete_file(tempfile) | ||||
|         except DeviceError as e: | ||||
|             if "Can't open X dispay." not in e.message: | ||||
|                 raise e | ||||
|             message = e.message.split('OUTPUT:', 1)[1].strip() | ||||
|             self.logger.debug('Could not take screenshot: {}'.format(message)) | ||||
|  | ||||
|     def is_screen_on(self): | ||||
|         pass  # TODO | ||||
|  | ||||
|     def ensure_screen_is_on(self): | ||||
|         pass  # TODO | ||||
		Reference in New Issue
	
	Block a user