| #!/usr/bin/env python3.8 |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import errno |
| import glob |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import time |
| |
| from .process import Process |
| |
| |
| class Host(object): |
| """Represent the platform-specific interface to the host system. |
| |
| Attributes: |
| platform: The name of the current operating system. |
| tracing: Indicates if additional output is enabled. |
| """ |
| |
| # Convenience file descriptor for silencing subprocess output |
| DEVNULL = open(os.devnull, 'w') |
| |
| # Used to pass tracing flag to tests in subprocesses. |
| TRACE_ENVVAR = 'FX_FUZZ_TRACE' |
| |
| def __init__(self): |
| self._platform = 'mac-x64' if os.uname()[0] == 'Darwin' else 'linux-x64' |
| self._fd_out = sys.stdout |
| self._fd_err = sys.stderr |
| self._tracing = os.getenv(Host.TRACE_ENVVAR) == '1' |
| |
| @property |
| def platform(self): |
| return self._platform |
| |
| @property |
| def fd_out(self): |
| return self._fd_out |
| |
| @fd_out.setter |
| def fd_out(self, fd_out): |
| self._fd_out = fd_out |
| |
| @property |
| def fd_err(self): |
| return self._fd_err |
| |
| @fd_out.setter |
| def fd_err(self, fd_err): |
| self._fd_err = fd_err |
| |
| @property |
| def tracing(self): |
| """Enables detailed output about action the script is taking.""" |
| return self._tracing |
| |
| @tracing.setter |
| def tracing(self, tracing): |
| self._tracing = tracing |
| |
| # I/O routines |
| |
| def trace(self, message): |
| """Prints execution details to stdout.""" |
| if self._tracing: |
| print('+ {}'.format(message)) |
| |
| def echo(self, *args, **kwargs): |
| """Print an informational message from a list of strings. |
| |
| Arguments: |
| fd File descriptor to print to. |
| end Terminating character to append to message. |
| """ |
| if not args: |
| args = [''] |
| fd = kwargs.pop('fd', self._fd_out) |
| end = kwargs.pop('end', '\n') |
| assert not kwargs, 'Unexpected keyword arguments: {}'.format(kwargs) |
| for line in args: |
| fd.write(line) |
| fd.write(end) |
| fd.flush() |
| |
| def error(self, *lines, **kwargs): |
| """Print an error message and exit.""" |
| assert lines, 'Fatal error without error message.' |
| fd = kwargs.pop('fd', self._fd_err) |
| status = kwargs.pop('status', 1) |
| assert not kwargs, 'Unexpected keyword arguments: {}'.format(kwargs) |
| self.echo('ERROR: {}'.format(lines[0]), fd=fd) |
| for line in lines[1:]: |
| self.echo(' {}'.format(line), fd=fd) |
| sys.exit(status) |
| |
| def choose(self, prompt, choices): |
| """Displays a simple interactive menu.""" |
| self.echo(prompt + ' (or enter 0 to cancel):') |
| for i, choice in enumerate(choices, 1): |
| self.echo(" {}) {}".format(i, choice)) |
| choice = None |
| while not choice: |
| self.echo("Choose 1-{}: ".format(len(choices)), end='') |
| try: |
| raw_choice = int(input()) |
| if raw_choice == 0: |
| self.echo("User canceled.") |
| sys.exit(0) |
| choice = choices[raw_choice - 1] |
| except (ValueError, IndexError): |
| self.echo("Invalid selection.") |
| return choice |
| |
| # Filesystem routines |
| |
| def getcwd(self): |
| """Returns the current working directory.""" |
| return os.getcwd() |
| |
| def isfile(self, pathname): |
| """Returns true for files that exist on the host.""" |
| return os.path.isfile(pathname) |
| |
| def isdir(self, pathname): |
| """Returns true for directories that exist on the host.""" |
| return os.path.isdir(pathname) |
| |
| def glob(self, pattern): |
| """Returns a list of pathnames from shell-expanding the pattern.""" |
| return sorted(glob.glob(pattern)) |
| |
| def open(self, pathname, mode='r', on_error=None, missing_ok=False): |
| """Opens and returns a file-like object. |
| |
| It is the callers responsibility to clean up, preferably by using a |
| "with" statement. |
| |
| Arguments: |
| mode: Same as for built-in open() |
| on_error: Message to display if file fails to open. |
| missing_ok: If true, return None if file not found. |
| """ |
| try: |
| self.trace(' opening: {}'.format(pathname)) |
| return open(pathname, mode) |
| except IOError as e: |
| if e.errno != errno.ENOENT: |
| raise |
| elif missing_ok: |
| return None |
| elif on_error: |
| self.error(*on_error) |
| else: |
| self.error('Failed to open {}.'.format(pathname)) |
| |
| def readfile(self, pathname, **kwargs): |
| """Returns the contents of a file.""" |
| opened = self.open(pathname, **kwargs) |
| if not opened: |
| return None |
| try: |
| return opened.read().strip() |
| finally: |
| opened.close() |
| |
| def touch(self, pathname): |
| with self.open(pathname, 'a') as opened: |
| pass |
| |
| def mkdir(self, pathname): |
| """Creates a directory and its parents if any of them are missing.""" |
| self.trace('creating: {}'.format(pathname)) |
| try: |
| os.makedirs(pathname) |
| except OSError as e: |
| if e.errno != errno.EEXIST: |
| raise |
| |
| def link(self, pathname, linkname): |
| """Creates or replaces a symbolic link from linkname to pathname.""" |
| self.trace(' linking: {}'.format(linkname)) |
| self.trace(' to: {}'.format(pathname)) |
| try: |
| os.unlink(linkname) |
| except OSError as e: |
| if e.errno != errno.ENOENT: |
| raise |
| os.symlink(pathname, linkname) |
| |
| def remove(self, pathname): |
| self.trace('removing: {}'.format(pathname)) |
| if self.isdir(pathname): |
| shutil.rmtree(pathname) |
| else: |
| os.remove(pathname) |
| |
| def _mkdtemp(self): |
| return tempfile.mkdtemp() |
| |
| def temp_dir(self): |
| return _TemporaryDirectory(self) |
| |
| # Other routines. |
| |
| def getenv(self, name): |
| return os.getenv(name) |
| |
| def putenv(self, name, value): |
| os.putenv(name, value) |
| |
| def create_process(self, args, **kwargs): |
| self.trace(' running: {}'.format(' '.join(args))) |
| return Process(args, **kwargs) |
| |
| def sleep(self, duration): |
| if duration > 0: |
| self.trace('sleeping: {}'.format(duration)) |
| time.sleep(duration) |
| |
| |
| class _TemporaryDirectory(object): |
| """A temporary directory that can be used with "with".""" |
| |
| def __init__(self, host): |
| self._host = host |
| self._pathname = None |
| |
| @property |
| def pathname(self): |
| return self._pathname |
| |
| def __enter__(self): |
| self._pathname = self._host._mkdtemp() |
| return self |
| |
| def __exit__(self, exc_type, exc_value, exc_traceback): |
| self._host.remove(self._pathname) |