command.py 8.93 KB
Newer Older
1 2 3 4
# Copyright 2017 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

5 6
# for py2/py3 compatibility
from __future__ import print_function
7 8

import os
9
import re
10
import signal
11 12 13
import subprocess
import sys
import threading
14
import time
15

16 17
from ..local.android import (
    android_driver, CommandFailedException, TimeoutException)
18 19 20 21
from ..local import utils
from ..objects import output


22 23 24
BASE_DIR = os.path.normpath(
    os.path.join(os.path.dirname(os.path.abspath(__file__)), '..' , '..', '..'))

25 26
SEM_INVALID_VALUE = -1
SEM_NOGPFAULTERRORBOX = 0x0002  # Microsoft Platform SDK WinBase.h
27 28


29 30 31 32 33 34 35 36 37 38 39 40 41
def setup_testing():
  """For testing only: We use threading under the hood instead of
  multiprocessing to make coverage work. Signal handling is only supported
  in the main thread, so we disable it for testing.
  """
  signal.signal = lambda *_: None


class AbortException(Exception):
  """Indicates early abort on SIGINT, SIGTERM or internal hard timeout."""
  pass


42
class BaseCommand(object):
43
  def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
44 45 46 47 48 49 50 51 52 53 54 55
               verbose=False, resources_func=None):
    """Initialize the command.

    Args:
      shell: The name of the executable (e.g. d8).
      args: List of args to pass to the executable.
      cmd_prefix: Prefix of command (e.g. a wrapper script).
      timeout: Timeout in seconds.
      env: Environment dict for execution.
      verbose: Print additional output.
      resources_func: Callable, returning all test files needed by this command.
    """
56 57 58 59 60 61 62 63 64
    assert(timeout > 0)

    self.shell = shell
    self.args = args or []
    self.cmd_prefix = cmd_prefix or []
    self.timeout = timeout
    self.env = env or {}
    self.verbose = verbose

65
  def execute(self):
66
    if self.verbose:
67
      print('# %s' % self)
68

69
    process = self._start_process()
70

71 72 73 74 75 76
    # Variable to communicate with the signal handler.
    abort_occured = [False]
    def handler(signum, frame):
      self._abort(process, abort_occured)
    signal.signal(signal.SIGTERM, handler)

77 78 79
    # Variable to communicate with the timer.
    timeout_occured = [False]
    timer = threading.Timer(
80
        self.timeout, self._abort, [process, timeout_occured])
81 82
    timer.start()

83
    start_time = time.time()
84
    stdout, stderr = process.communicate()
85
    duration = time.time() - start_time
86 87 88

    timer.cancel()

89 90 91
    if abort_occured[0]:
      raise AbortException()

92 93 94 95 96 97
    return output.Output(
      process.returncode,
      timeout_occured[0],
      stdout.decode('utf-8', 'replace').encode('utf-8'),
      stderr.decode('utf-8', 'replace').encode('utf-8'),
      process.pid,
98
      duration
99 100
    )

101
  def _start_process(self):
102 103 104 105 106 107 108 109 110 111 112
    try:
      return subprocess.Popen(
        args=self._get_popen_args(),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        env=self._get_env(),
      )
    except Exception as e:
      sys.stderr.write('Error executing: %s\n' % self)
      raise e

113
  def _get_popen_args(self):
114
    return self._to_args_list()
115 116 117 118 119 120 121 122 123 124 125

  def _get_env(self):
    env = os.environ.copy()
    env.update(self.env)
    # GTest shard information is read by the V8 tests runner. Make sure it
    # doesn't leak into the execution of gtests we're wrapping. Those might
    # otherwise apply a second level of sharding and as a result skip tests.
    env.pop('GTEST_TOTAL_SHARDS', None)
    env.pop('GTEST_SHARD_INDEX', None)
    return env

126 127 128
  def _kill_process(self, process):
    raise NotImplementedError()

129 130
  def _abort(self, process, abort_called):
    abort_called[0] = True
131
    try:
132 133
      print('Attempting to kill process %s' % process.pid)
      sys.stdout.flush()
134
      self._kill_process(process)
135 136 137
    except OSError as e:
      print(e)
      sys.stdout.flush()
138
      pass
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158

  def __str__(self):
    return self.to_string()

  def to_string(self, relative=False):
    def escape(part):
      # Escape spaces. We may need to escape more characters for this to work
      # properly.
      if ' ' in part:
        return '"%s"' % part
      return part

    parts = map(escape, self._to_args_list())
    cmd = ' '.join(parts)
    if relative:
      cmd = cmd.replace(os.getcwd() + os.sep, '')
    return cmd

  def _to_args_list(self):
    return self.cmd_prefix + [self.shell] + self.args
159 160 161


class PosixCommand(BaseCommand):
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
  # TODO(machenbach): Use base process start without shell once
  # https://crbug.com/v8/8889 is resolved.
  def _start_process(self):
    def wrapped(arg):
      if set('() \'"') & set(arg):
        return "'%s'" % arg.replace("'", "'\"'\"'")
      return arg
    try:
      return subprocess.Popen(
        args=' '.join(map(wrapped, self._get_popen_args())),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        env=self._get_env(),
        shell=True,
      )
    except Exception as e:
      sys.stderr.write('Error executing: %s\n' % self)
      raise e

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
  def _kill_process(self, process):
    process.kill()


class WindowsCommand(BaseCommand):
  def _start_process(self, **kwargs):
    # Try to change the error mode to avoid dialogs on fatal errors. Don't
    # touch any existing error mode flags by merging the existing error mode.
    # See http://blogs.msdn.com/oldnewthing/archive/2004/07/27/198410.aspx.
    def set_error_mode(mode):
      prev_error_mode = SEM_INVALID_VALUE
      try:
        import ctypes
        prev_error_mode = (
            ctypes.windll.kernel32.SetErrorMode(mode))  #@UndefinedVariable
      except ImportError:
        pass
      return prev_error_mode

    error_mode = SEM_NOGPFAULTERRORBOX
    prev_error_mode = set_error_mode(error_mode)
    set_error_mode(error_mode | prev_error_mode)

    try:
      return super(WindowsCommand, self)._start_process(**kwargs)
    finally:
      if prev_error_mode != SEM_INVALID_VALUE:
        set_error_mode(prev_error_mode)

  def _get_popen_args(self):
    return subprocess.list2cmdline(self._to_args_list())

  def _kill_process(self, process):
    tk = subprocess.Popen(
        'taskkill /T /F /PID %d' % process.pid,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    stdout, stderr = tk.communicate()
    if self.verbose:
221 222 223 224
      print('Taskkill results for %d' % process.pid)
      print(stdout)
      print(stderr)
      print('Return code: %d' % tk.returncode)
225 226 227
      sys.stdout.flush()


228
class AndroidCommand(BaseCommand):
229 230 231
  # This must be initialized before creating any instances of this class.
  driver = None

232 233 234 235 236 237 238
  def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
               verbose=False, resources_func=None):
    """Initialize the command and all files that need to be pushed to the
    Android device.
    """
    self.shell_name = os.path.basename(shell)
    self.shell_dir = os.path.dirname(shell)
239
    self.files_to_push = (resources_func or (lambda: []))()
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

    # Make all paths in arguments relative and also prepare files from arguments
    # for pushing to the device.
    rel_args = []
    find_path_re = re.compile(r'.*(%s/[^\'"]+).*' % re.escape(BASE_DIR))
    for arg in (args or []):
      match = find_path_re.match(arg)
      if match:
        self.files_to_push.append(match.group(1))
      rel_args.append(
          re.sub(r'(.*)%s/(.*)' % re.escape(BASE_DIR), r'\1\2', arg))

    super(AndroidCommand, self).__init__(
        shell, args=rel_args, cmd_prefix=cmd_prefix, timeout=timeout, env=env,
        verbose=verbose)

  def execute(self, **additional_popen_kwargs):
    """Execute the command on the device.

    This pushes all required files to the device and then runs the command.
    """
    if self.verbose:
262
      print('# %s' % self)
263

264
    self.driver.push_executable(self.shell_dir, 'bin', self.shell_name)
265 266 267 268 269

    for abs_file in self.files_to_push:
      abs_dir = os.path.dirname(abs_file)
      file_name = os.path.basename(abs_file)
      rel_dir = os.path.relpath(abs_dir, BASE_DIR)
270
      self.driver.push_file(abs_dir, file_name, rel_dir)
271 272 273 274 275

    start_time = time.time()
    return_code = 0
    timed_out = False
    try:
276
      stdout = self.driver.run(
277
          'bin', self.shell_name, self.args, '.', self.timeout, self.env)
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    except CommandFailedException as e:
      return_code = e.status
      stdout = e.output
    except TimeoutException as e:
      return_code = 1
      timed_out = True
      # Sadly the Android driver doesn't provide output on timeout.
      stdout = ''

    duration = time.time() - start_time
    return output.Output(
        return_code,
        timed_out,
        stdout,
        '',  # No stderr available.
        -1,  # No pid available.
        duration,
    )


Command = None
299
def setup(target_os, device):
300 301 302
  """Set the Command class to the OS-specific version."""
  global Command
  if target_os == 'android':
303
    AndroidCommand.driver = android_driver(device)
304 305 306 307 308 309 310 311 312
    Command = AndroidCommand
  elif target_os == 'windows':
    Command = WindowsCommand
  else:
    Command = PosixCommand

def tear_down():
  """Clean up after using commands."""
  if Command == AndroidCommand:
313
    AndroidCommand.driver.tear_down()