# 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. # """ Miscellaneous functions that don't fit anywhere else. """ from __future__ import division import os import sys import re import math import imp import uuid import string import threading import signal import subprocess import pkgutil import traceback import logging import random from datetime import datetime, timedelta from operator import mul, itemgetter from StringIO import StringIO from itertools import cycle, groupby from distutils.spawn import find_executable import yaml from dateutil import tz from wa.framework.version import get_wa_version # ABI --> architectures list ABI_MAP = { 'armeabi': ['armeabi', 'armv7', 'armv7l', 'armv7el', 'armv7lh'], 'arm64': ['arm64', 'armv8', 'arm64-v8a'], } def preexec_function(): # Ignore the SIGINT signal by setting the handler to the standard # signal handler SIG_IGN. signal.signal(signal.SIGINT, signal.SIG_IGN) # Change process group in case we have to kill the subprocess and all of # its children later. # TODO: this is Unix-specific; would be good to find an OS-agnostic way # to do this in case we wanna port WA to Windows. os.setpgrp() check_output_logger = logging.getLogger('check_output') # Defined here rather than in wlauto.exceptions due to module load dependencies class TimeoutError(Exception): """Raised when a subprocess command times out. This is basically a ``WAError``-derived version of ``subprocess.CalledProcessError``, the thinking being that while a timeout could be due to programming error (e.g. not setting long enough timers), it is often due to some failure in the environment, and there fore should be classed as a "user error".""" def __init__(self, command, output): super(TimeoutError, self).__init__('Timed out: {}'.format(command)) self.command = command self.output = output def __str__(self): return '\n'.join([self.message, 'OUTPUT:', self.output or '']) def check_output(command, timeout=None, ignore=None, **kwargs): """This is a version of subprocess.check_output that adds a timeout parameter to kill the subprocess if it does not return within the specified time.""" # pylint: disable=too-many-branches if ignore is None: ignore = [] elif isinstance(ignore, int): ignore = [ignore] elif not isinstance(ignore, list) and ignore != 'all': message = 'Invalid value for ignore parameter: "{}"; must be an int or a list' raise ValueError(message.format(ignore)) if 'stdout' in kwargs: raise ValueError('stdout argument not allowed, it will be overridden.') def callback(pid): try: check_output_logger.debug('{} timed out; sending SIGKILL'.format(pid)) os.killpg(pid, signal.SIGKILL) except OSError: pass # process may have already terminated. process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function, **kwargs) if timeout: timer = threading.Timer(timeout, callback, [process.pid, ]) timer.start() try: output, error = process.communicate() finally: if timeout: timer.cancel() retcode = process.poll() if retcode: if retcode == -9: # killed, assume due to timeout callback raise TimeoutError(command, output='\n'.join([output, error])) elif ignore != 'all' and retcode not in ignore: raise subprocess.CalledProcessError(retcode, command, output='\n'.join([output, error])) return output, error def init_argument_parser(parser): parser.add_argument('-c', '--config', help='specify an additional config.py') parser.add_argument('-v', '--verbose', action='count', help='The scripts will produce verbose output.') parser.add_argument('--debug', action='store_true', help='Enable debug mode. Note: this implies --verbose.') parser.add_argument('--version', action='version', version='%(prog)s {}'.format(get_wa_version())) return parser def walk_modules(path): """ Given a path to a Python package, iterate over all the modules and sub-packages in that package. """ try: root_mod = __import__(path, {}, {}, ['']) yield root_mod except ImportError as e: e.path = path raise e if not hasattr(root_mod, '__path__'): # module, not package return for _, name, ispkg in pkgutil.iter_modules(root_mod.__path__): try: submod_path = '.'.join([path, name]) if ispkg: for submod in walk_modules(submod_path): yield submod else: yield __import__(submod_path, {}, {}, ['']) except ImportError as e: e.path = submod_path raise e def ensure_directory_exists(dirpath): """A filter for directory paths to ensure they exist.""" if not os.path.isdir(dirpath): os.makedirs(dirpath) return dirpath def ensure_file_directory_exists(filepath): """ A filter for file paths to ensure the directory of the file exists and the file can be created there. The file itself is *not* going to be created if it doesn't already exist. """ ensure_directory_exists(os.path.dirname(filepath)) return filepath def diff_tokens(before_token, after_token): """ Creates a diff of two tokens. If the two tokens are the same it just returns returns the token (whitespace tokens are considered the same irrespective of type/number of whitespace characters in the token). If the tokens are numeric, the difference between the two values is returned. Otherwise, a string in the form [before -> after] is returned. """ if before_token.isspace() and after_token.isspace(): return after_token elif before_token.isdigit() and after_token.isdigit(): try: diff = int(after_token) - int(before_token) return str(diff) except ValueError: return "[%s -> %s]" % (before_token, after_token) elif before_token == after_token: return after_token else: return "[%s -> %s]" % (before_token, after_token) def prepare_table_rows(rows): """Given a list of lists, make sure they are prepared to be formatted into a table by making sure each row has the same number of columns and stringifying all values.""" rows = [map(str, r) for r in rows] max_cols = max(map(len, rows)) for row in rows: pad = max_cols - len(row) for _ in xrange(pad): row.append('') return rows def write_table(rows, wfh, align='>', headers=None): # pylint: disable=R0914 """Write a column-aligned table to the specified file object.""" if not rows: return rows = prepare_table_rows(rows) num_cols = len(rows[0]) # cycle specified alignments until we have max_cols of them. This is # consitent with how such cases are handled in R, pandas, etc. it = cycle(align) align = [it.next() for _ in xrange(num_cols)] cols = zip(*rows) col_widths = [max(map(len, c)) for c in cols] row_format = ' '.join(['{:%s%s}' % (align[i], w) for i, w in enumerate(col_widths)]) row_format += '\n' if headers: wfh.write(row_format.format(*headers)) underlines = ['-' * len(h) for h in headers] wfh.write(row_format.format(*underlines)) for row in rows: wfh.write(row_format.format(*row)) def get_null(): """Returns the correct null sink based on the OS.""" return 'NUL' if os.name == 'nt' else '/dev/null' def get_traceback(exc=None): """ Returns the string with the traceback for the specifiec exc object, or for the current exception exc is not specified. """ if exc is None: exc = sys.exc_info() if not exc: return None tb = exc[2] sio = StringIO() traceback.print_tb(tb, file=sio) del tb # needs to be done explicitly see: http://docs.python.org/2/library/sys.html#sys.exc_info return sio.getvalue() def normalize(value, dict_type=dict): """Normalize values. Recursively normalizes dict keys to be lower case, no surrounding whitespace, underscore-delimited strings.""" if isinstance(value, dict): normalized = dict_type() for k, v in value.iteritems(): if isinstance(k, basestring): k = k.strip().lower().replace(' ', '_') normalized[k] = normalize(v, dict_type) return normalized elif isinstance(value, list): return [normalize(v, dict_type) for v in value] elif isinstance(value, tuple): return tuple([normalize(v, dict_type) for v in value]) else: return value VALUE_REGEX = re.compile(r'(\d+(?:\.\d+)?)\s*(\w*)') UNITS_MAP = { 's': 'seconds', 'ms': 'milliseconds', 'us': 'microseconds', 'ns': 'nanoseconds', 'V': 'volts', 'A': 'amps', 'mA': 'milliamps', 'J': 'joules', } def parse_value(value_string): """parses a string representing a numerical value and returns a tuple (value, units), where value will be either int or float, and units will be a string representing the units or None.""" match = VALUE_REGEX.search(value_string) if match: vs = match.group(1) value = float(vs) if '.' in vs else int(vs) us = match.group(2) units = UNITS_MAP.get(us, us) return (value, units) else: return (value_string, None) def get_meansd(values): """Returns mean and standard deviation of the specified values.""" if not values: return float('nan'), float('nan') mean = sum(values) / len(values) sd = math.sqrt(sum([(v - mean) ** 2 for v in values]) / len(values)) return mean, sd def geomean(values): """Returns the geometric mean of the values.""" return reduce(mul, values) ** (1.0 / len(values)) def capitalize(text): """Capitalises the specified text: first letter upper case, all subsequent letters lower case.""" if not text: return '' return text[0].upper() + text[1:].lower() def convert_new_lines(text): """ Convert new lines to a common format. """ return text.replace('\r\n', '\n').replace('\r', '\n') def escape_quotes(text): """Escape quotes, and escaped quotes, in the specified text.""" return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\'', '\\\'').replace('\"', '\\\"') def escape_single_quotes(text): """Escape single quotes, and escaped single quotes, in the specified text.""" return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\'', '\'\\\'\'') def escape_double_quotes(text): """Escape double quotes, and escaped double quotes, in the specified text.""" return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\"', '\\\"') def getch(count=1): """Read ``count`` characters from standard input.""" if os.name == 'nt': import msvcrt # pylint: disable=F0401 return ''.join([msvcrt.getch() for _ in xrange(count)]) else: # assume Unix import tty # NOQA import termios # NOQA fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(count) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch def isiterable(obj): """Returns ``True`` if the specified object is iterable and *is not a string type*, ``False`` otherwise.""" return hasattr(obj, '__iter__') and not isinstance(obj, basestring) def utc_to_local(dt): """Convert naive datetime to local time zone, assuming UTC.""" return dt.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()) def local_to_utc(dt): """Convert naive datetime to UTC, assuming local time zone.""" return dt.replace(tzinfo=tz.tzlocal()).astimezone(tz.tzutc()) def as_relative(path): """Convert path to relative by stripping away the leading '/' on UNIX or the equivant on other platforms.""" path = os.path.splitdrive(path)[1] return path.lstrip(os.sep) def get_cpu_mask(cores): """Return a string with the hex for the cpu mask for the specified core numbers.""" mask = 0 for i in cores: mask |= 1 << i return '0x{0:x}'.format(mask) def load_class(classpath): """Loads the specified Python class. ``classpath`` must be a fully-qualified class name (i.e. namspaced under module/package).""" modname, clsname = classpath.rsplit('.', 1) return getattr(__import__(modname), clsname) def get_pager(): """Returns the name of the system pager program.""" pager = os.getenv('PAGER') if pager is None: pager = find_executable('less') if pager is None: pager = find_executable('more') return pager def enum_metaclass(enum_param, return_name=False, start=0): """ Returns a ``type`` subclass that may be used as a metaclass for an enum. Paremeters: :enum_param: the name of class attribute that defines enum values. The metaclass will add a class attribute for each value in ``enum_param``. The value of the attribute depends on the type of ``enum_param`` and on the values of ``return_name``. If ``return_name`` is ``True``, then the value of the new attribute is the name of that attribute; otherwise, if ``enum_param`` is a ``list`` or a ``tuple``, the value will be the index of that param in ``enum_param``, optionally offset by ``start``, otherwise, it will be assumed that ``enum_param`` implementa a dict-like inteface and the value will be ``enum_param[attr_name]``. :return_name: If ``True``, the enum values will the names of enum attributes. If ``False``, the default, the values will depend on the type of ``enum_param`` (see above). :start: If ``enum_param`` is a list or a tuple, and ``return_name`` is ``False``, this specifies an "offset" that will be added to the index of the attribute within ``enum_param`` to form the value. """ class __EnumMeta(type): def __new__(mcs, clsname, bases, attrs): cls = type.__new__(mcs, clsname, bases, attrs) values = getattr(cls, enum_param, []) if return_name: for name in values: setattr(cls, name, name) else: if isinstance(values, list) or isinstance(values, tuple): for i, name in enumerate(values): setattr(cls, name, i + start) else: # assume dict-like for name in values: setattr(cls, name, values[name]) return cls return __EnumMeta def which(name): """Platform-independent version of UNIX which utility.""" if os.name == 'nt': paths = os.getenv('PATH').split(os.pathsep) exts = os.getenv('PATHEXT').split(os.pathsep) for path in paths: testpath = os.path.join(path, name) if os.path.isfile(testpath): return testpath for ext in exts: testpathext = testpath + ext if os.path.isfile(testpathext): return testpathext return None else: # assume UNIX-like try: result = check_output(['which', name])[0] return result.strip() # pylint: disable=E1103 except subprocess.CalledProcessError: return None _bash_color_regex = re.compile('\x1b\\[[0-9;]+m') def strip_bash_colors(text): return _bash_color_regex.sub('', text) def format_duration(seconds, sep=' ', order=['day', 'hour', 'minute', 'second']): # pylint: disable=dangerous-default-value """ Formats the specified number of seconds into human-readable duration. """ if isinstance(seconds, timedelta): td = seconds else: td = timedelta(seconds=seconds) dt = datetime(1, 1, 1) + td result = [] for item in order: value = getattr(dt, item, None) if item is 'day': value -= 1 if not value: continue suffix = '' if value == 1 else 's' result.append('{} {}{}'.format(value, item, suffix)) return sep.join(result) def get_article(word): """ Returns the appropriate indefinite article for the word (ish). .. note:: Indefinite article assignment in English is based on sound rather than spelling, so this will not work correctly in all case; e.g. this will return ``"a hour"``. """ return'an' if word[0] in 'aoeiu' else 'a' def get_random_string(length): """Returns a random ASCII string of the specified length).""" return ''.join(random.choice(string.ascii_letters + string.digits) for _ in xrange(length)) RAND_MOD_NAME_LEN = 30 BAD_CHARS = string.punctuation + string.whitespace TRANS_TABLE = string.maketrans(BAD_CHARS, '_' * len(BAD_CHARS)) def to_identifier(text): """Converts text to a valid Python identifier by replacing all whitespace and punctuation.""" result = re.sub('_+', '_', text.translate(TRANS_TABLE)) if result and result[0] in string.digits: result = '_' + result return result def unique(alist): """ Returns a list containing only unique elements from the input list (but preserves order, unlike sets). """ result = [] for item in alist: if item not in result: result.append(item) return result def open_file(filepath): """ Open the specified file path with the associated launcher in an OS-agnostic way. """ if os.name == 'nt': # Windows return os.startfile(filepath) # pylint: disable=no-member elif sys.platform == 'darwin': # Mac OSX return subprocess.call(['open', filepath]) else: # assume Linux or similar running a freedesktop-compliant GUI return subprocess.call(['xdg-open', filepath]) def ranges_to_list(ranges_string): """Converts a sysfs-style ranges string, e.g. ``"0,2-4"``, into a list ,e.g ``[0,2,3,4]``""" values = [] for rg in ranges_string.split(','): if '-' in rg: first, last = map(int, rg.split('-')) values.extend(xrange(first, last + 1)) else: values.append(int(rg)) return values def list_to_ranges(values): """Converts a list, e.g ``[0,2,3,4]``, into a sysfs-style ranges string, e.g. ``"0,2-4"``""" range_groups = [] for _, g in groupby(enumerate(values), lambda (i, x): i - x): range_groups.append(map(itemgetter(1), g)) range_strings = [] for group in range_groups: if len(group) == 1: range_strings.append(str(group[0])) else: range_strings.append('{}-{}'.format(group[0], group[-1])) return ','.join(range_strings) def list_to_mask(values, base=0x0): """Converts the specified list of integer values into a bit mask for those values. Optinally, the list can be applied to an existing mask.""" for v in values: base |= (1 << v) return base def mask_to_list(mask): """Converts the specfied integer bitmask into a list of indexes of bits that are set in the mask.""" size = len(bin(mask)) - 2 # because of "0b" return [size - i - 1 for i in xrange(size) if mask & (1 << size - i - 1)] class Namespace(dict): """ A dict-like object that allows treating keys and attributes interchangeably (this means that keys are restricted to strings that are valid Python identifiers). """ def __getattr__(self, name): try: return self[name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): self[name] = value def __setitem__(self, name, value): if to_identifier(name) != name: message = 'Key must be a valid identifier; got "{}"' raise ValueError(message.format(name)) dict.__setitem__(self, name, value)