Home | History | Annotate | Download | only in local
      1 # Copyright 2017 the V8 project authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 
      6 import os
      7 import re
      8 import signal
      9 import subprocess
     10 import sys
     11 import threading
     12 import time
     13 
     14 from ..local.android import (
     15     android_driver, CommandFailedException, TimeoutException)
     16 from ..local import utils
     17 from ..objects import output
     18 
     19 
     20 BASE_DIR = os.path.normpath(
     21     os.path.join(os.path.dirname(os.path.abspath(__file__)), '..' , '..', '..'))
     22 
     23 SEM_INVALID_VALUE = -1
     24 SEM_NOGPFAULTERRORBOX = 0x0002  # Microsoft Platform SDK WinBase.h
     25 
     26 
     27 def setup_testing():
     28   """For testing only: We use threading under the hood instead of
     29   multiprocessing to make coverage work. Signal handling is only supported
     30   in the main thread, so we disable it for testing.
     31   """
     32   signal.signal = lambda *_: None
     33 
     34 
     35 class AbortException(Exception):
     36   """Indicates early abort on SIGINT, SIGTERM or internal hard timeout."""
     37   pass
     38 
     39 
     40 class BaseCommand(object):
     41   def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
     42                verbose=False, resources_func=None):
     43     """Initialize the command.
     44 
     45     Args:
     46       shell: The name of the executable (e.g. d8).
     47       args: List of args to pass to the executable.
     48       cmd_prefix: Prefix of command (e.g. a wrapper script).
     49       timeout: Timeout in seconds.
     50       env: Environment dict for execution.
     51       verbose: Print additional output.
     52       resources_func: Callable, returning all test files needed by this command.
     53     """
     54     assert(timeout > 0)
     55 
     56     self.shell = shell
     57     self.args = args or []
     58     self.cmd_prefix = cmd_prefix or []
     59     self.timeout = timeout
     60     self.env = env or {}
     61     self.verbose = verbose
     62 
     63   def execute(self):
     64     if self.verbose:
     65       print '# %s' % self
     66 
     67     process = self._start_process()
     68 
     69     # Variable to communicate with the signal handler.
     70     abort_occured = [False]
     71     def handler(signum, frame):
     72       self._abort(process, abort_occured)
     73     signal.signal(signal.SIGTERM, handler)
     74 
     75     # Variable to communicate with the timer.
     76     timeout_occured = [False]
     77     timer = threading.Timer(
     78         self.timeout, self._abort, [process, timeout_occured])
     79     timer.start()
     80 
     81     start_time = time.time()
     82     stdout, stderr = process.communicate()
     83     duration = time.time() - start_time
     84 
     85     timer.cancel()
     86 
     87     if abort_occured[0]:
     88       raise AbortException()
     89 
     90     return output.Output(
     91       process.returncode,
     92       timeout_occured[0],
     93       stdout.decode('utf-8', 'replace').encode('utf-8'),
     94       stderr.decode('utf-8', 'replace').encode('utf-8'),
     95       process.pid,
     96       duration
     97     )
     98 
     99   def _start_process(self):
    100     try:
    101       return subprocess.Popen(
    102         args=self._get_popen_args(),
    103         stdout=subprocess.PIPE,
    104         stderr=subprocess.PIPE,
    105         env=self._get_env(),
    106       )
    107     except Exception as e:
    108       sys.stderr.write('Error executing: %s\n' % self)
    109       raise e
    110 
    111   def _get_popen_args(self):
    112     return self._to_args_list()
    113 
    114   def _get_env(self):
    115     env = os.environ.copy()
    116     env.update(self.env)
    117     # GTest shard information is read by the V8 tests runner. Make sure it
    118     # doesn't leak into the execution of gtests we're wrapping. Those might
    119     # otherwise apply a second level of sharding and as a result skip tests.
    120     env.pop('GTEST_TOTAL_SHARDS', None)
    121     env.pop('GTEST_SHARD_INDEX', None)
    122     return env
    123 
    124   def _kill_process(self, process):
    125     raise NotImplementedError()
    126 
    127   def _abort(self, process, abort_called):
    128     abort_called[0] = True
    129     try:
    130       self._kill_process(process)
    131     except OSError:
    132       pass
    133 
    134   def __str__(self):
    135     return self.to_string()
    136 
    137   def to_string(self, relative=False):
    138     def escape(part):
    139       # Escape spaces. We may need to escape more characters for this to work
    140       # properly.
    141       if ' ' in part:
    142         return '"%s"' % part
    143       return part
    144 
    145     parts = map(escape, self._to_args_list())
    146     cmd = ' '.join(parts)
    147     if relative:
    148       cmd = cmd.replace(os.getcwd() + os.sep, '')
    149     return cmd
    150 
    151   def _to_args_list(self):
    152     return self.cmd_prefix + [self.shell] + self.args
    153 
    154 
    155 class PosixCommand(BaseCommand):
    156   def _kill_process(self, process):
    157     process.kill()
    158 
    159 
    160 class WindowsCommand(BaseCommand):
    161   def _start_process(self, **kwargs):
    162     # Try to change the error mode to avoid dialogs on fatal errors. Don't
    163     # touch any existing error mode flags by merging the existing error mode.
    164     # See http://blogs.msdn.com/oldnewthing/archive/2004/07/27/198410.aspx.
    165     def set_error_mode(mode):
    166       prev_error_mode = SEM_INVALID_VALUE
    167       try:
    168         import ctypes
    169         prev_error_mode = (
    170             ctypes.windll.kernel32.SetErrorMode(mode))  #@UndefinedVariable
    171       except ImportError:
    172         pass
    173       return prev_error_mode
    174 
    175     error_mode = SEM_NOGPFAULTERRORBOX
    176     prev_error_mode = set_error_mode(error_mode)
    177     set_error_mode(error_mode | prev_error_mode)
    178 
    179     try:
    180       return super(WindowsCommand, self)._start_process(**kwargs)
    181     finally:
    182       if prev_error_mode != SEM_INVALID_VALUE:
    183         set_error_mode(prev_error_mode)
    184 
    185   def _get_popen_args(self):
    186     return subprocess.list2cmdline(self._to_args_list())
    187 
    188   def _kill_process(self, process):
    189     if self.verbose:
    190       print 'Attempting to kill process %d' % process.pid
    191       sys.stdout.flush()
    192     tk = subprocess.Popen(
    193         'taskkill /T /F /PID %d' % process.pid,
    194         stdout=subprocess.PIPE,
    195         stderr=subprocess.PIPE,
    196     )
    197     stdout, stderr = tk.communicate()
    198     if self.verbose:
    199       print 'Taskkill results for %d' % process.pid
    200       print stdout
    201       print stderr
    202       print 'Return code: %d' % tk.returncode
    203       sys.stdout.flush()
    204 
    205 
    206 class AndroidCommand(BaseCommand):
    207   def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
    208                verbose=False, resources_func=None):
    209     """Initialize the command and all files that need to be pushed to the
    210     Android device.
    211     """
    212     self.shell_name = os.path.basename(shell)
    213     self.shell_dir = os.path.dirname(shell)
    214     self.files_to_push = resources_func()
    215 
    216     # Make all paths in arguments relative and also prepare files from arguments
    217     # for pushing to the device.
    218     rel_args = []
    219     find_path_re = re.compile(r'.*(%s/[^\'"]+).*' % re.escape(BASE_DIR))
    220     for arg in (args or []):
    221       match = find_path_re.match(arg)
    222       if match:
    223         self.files_to_push.append(match.group(1))
    224       rel_args.append(
    225           re.sub(r'(.*)%s/(.*)' % re.escape(BASE_DIR), r'\1\2', arg))
    226 
    227     super(AndroidCommand, self).__init__(
    228         shell, args=rel_args, cmd_prefix=cmd_prefix, timeout=timeout, env=env,
    229         verbose=verbose)
    230 
    231   def execute(self, **additional_popen_kwargs):
    232     """Execute the command on the device.
    233 
    234     This pushes all required files to the device and then runs the command.
    235     """
    236     if self.verbose:
    237       print '# %s' % self
    238 
    239     android_driver().push_executable(self.shell_dir, 'bin', self.shell_name)
    240 
    241     for abs_file in self.files_to_push:
    242       abs_dir = os.path.dirname(abs_file)
    243       file_name = os.path.basename(abs_file)
    244       rel_dir = os.path.relpath(abs_dir, BASE_DIR)
    245       android_driver().push_file(abs_dir, file_name, rel_dir)
    246 
    247     start_time = time.time()
    248     return_code = 0
    249     timed_out = False
    250     try:
    251       stdout = android_driver().run(
    252           'bin', self.shell_name, self.args, '.', self.timeout, self.env)
    253     except CommandFailedException as e:
    254       return_code = e.status
    255       stdout = e.output
    256     except TimeoutException as e:
    257       return_code = 1
    258       timed_out = True
    259       # Sadly the Android driver doesn't provide output on timeout.
    260       stdout = ''
    261 
    262     duration = time.time() - start_time
    263     return output.Output(
    264         return_code,
    265         timed_out,
    266         stdout,
    267         '',  # No stderr available.
    268         -1,  # No pid available.
    269         duration,
    270     )
    271 
    272 
    273 Command = None
    274 def setup(target_os):
    275   """Set the Command class to the OS-specific version."""
    276   global Command
    277   if target_os == 'android':
    278     Command = AndroidCommand
    279   elif target_os == 'windows':
    280     Command = WindowsCommand
    281   else:
    282     Command = PosixCommand
    283 
    284 def tear_down():
    285   """Clean up after using commands."""
    286   if Command == AndroidCommand:
    287     android_driver().tear_down()
    288