• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5from contextlib import contextmanager
6import os
7import re
8import signal
9import subprocess
10import sys
11import threading
12import time
13
14from ..local.android import (
15    android_driver, CommandFailedException, TimeoutException)
16from ..local import utils
17from ..objects import output
18
19BASE_DIR = os.path.normpath(
20    os.path.join(os.path.dirname(os.path.abspath(__file__)), '..' , '..', '..'))
21
22SEM_INVALID_VALUE = -1
23SEM_NOGPFAULTERRORBOX = 0x0002  # Microsoft Platform SDK WinBase.h
24
25
26def setup_testing():
27  """For testing only: We use threading under the hood instead of
28  multiprocessing to make coverage work. Signal handling is only supported
29  in the main thread, so we disable it for testing.
30  """
31  signal.signal = lambda *_: None
32
33
34class AbortException(Exception):
35  """Indicates early abort on SIGINT, SIGTERM or internal hard timeout."""
36  pass
37
38
39@contextmanager
40def handle_sigterm(process, abort_fun, enabled):
41  """Call`abort_fun` on sigterm and restore previous handler to prevent
42  erroneous termination of an already terminated process.
43
44  Args:
45    process: The process to terminate.
46    abort_fun: Function taking two parameters: the process to terminate and
47        an array with a boolean for storing if an abort occured.
48    enabled: If False, this wrapper will be a no-op.
49  """
50  # Variable to communicate with the signal handler.
51  abort_occured = [False]
52  def handler(signum, frame):
53    abort_fun(process, abort_occured)
54
55  if enabled:
56    previous = signal.signal(signal.SIGTERM, handler)
57  try:
58    yield
59  finally:
60    if enabled:
61      signal.signal(signal.SIGTERM, previous)
62
63  if abort_occured[0]:
64    raise AbortException()
65
66
67class BaseCommand(object):
68  def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
69               verbose=False, resources_func=None, handle_sigterm=False):
70    """Initialize the command.
71
72    Args:
73      shell: The name of the executable (e.g. d8).
74      args: List of args to pass to the executable.
75      cmd_prefix: Prefix of command (e.g. a wrapper script).
76      timeout: Timeout in seconds.
77      env: Environment dict for execution.
78      verbose: Print additional output.
79      resources_func: Callable, returning all test files needed by this command.
80      handle_sigterm: Flag indicating if SIGTERM will be used to terminate the
81          underlying process. Should not be used from the main thread, e.g. when
82          using a command to list tests.
83    """
84    assert(timeout > 0)
85
86    self.shell = shell
87    self.args = args or []
88    self.cmd_prefix = cmd_prefix or []
89    self.timeout = timeout
90    self.env = env or {}
91    self.verbose = verbose
92    self.handle_sigterm = handle_sigterm
93
94  def execute(self):
95    if self.verbose:
96      print('# %s' % self)
97
98    process = self._start_process()
99
100    with handle_sigterm(process, self._abort, self.handle_sigterm):
101      # Variable to communicate with the timer.
102      timeout_occured = [False]
103      timer = threading.Timer(
104          self.timeout, self._abort, [process, timeout_occured])
105      timer.start()
106
107      start_time = time.time()
108      stdout, stderr = process.communicate()
109      duration = time.time() - start_time
110
111      timer.cancel()
112
113    return output.Output(
114      process.returncode,
115      timeout_occured[0],
116      stdout.decode('utf-8', 'replace'),
117      stderr.decode('utf-8', 'replace'),
118      process.pid,
119      duration
120    )
121
122  def _start_process(self):
123    try:
124      return subprocess.Popen(
125        args=self._get_popen_args(),
126        stdout=subprocess.PIPE,
127        stderr=subprocess.PIPE,
128        env=self._get_env(),
129      )
130    except Exception as e:
131      sys.stderr.write('Error executing: %s\n' % self)
132      raise e
133
134  def _get_popen_args(self):
135    return self._to_args_list()
136
137  def _get_env(self):
138    env = os.environ.copy()
139    env.update(self.env)
140    # GTest shard information is read by the V8 tests runner. Make sure it
141    # doesn't leak into the execution of gtests we're wrapping. Those might
142    # otherwise apply a second level of sharding and as a result skip tests.
143    env.pop('GTEST_TOTAL_SHARDS', None)
144    env.pop('GTEST_SHARD_INDEX', None)
145    return env
146
147  def _kill_process(self, process):
148    raise NotImplementedError()
149
150  def _abort(self, process, abort_called):
151    abort_called[0] = True
152    started_as = self.to_string(relative=True)
153    process_text = 'process %d started as:\n  %s\n' % (process.pid, started_as)
154    try:
155      print('Attempting to kill ' + process_text)
156      sys.stdout.flush()
157      self._kill_process(process)
158    except OSError as e:
159      print(e)
160      print('Unruly ' + process_text)
161      sys.stdout.flush()
162
163  def __str__(self):
164    return self.to_string()
165
166  def to_string(self, relative=False):
167    def escape(part):
168      # Escape spaces. We may need to escape more characters for this to work
169      # properly.
170      if ' ' in part:
171        return '"%s"' % part
172      return part
173
174    parts = map(escape, self._to_args_list())
175    cmd = ' '.join(parts)
176    if relative:
177      cmd = cmd.replace(os.getcwd() + os.sep, '')
178    return cmd
179
180  def _to_args_list(self):
181    return self.cmd_prefix + [self.shell] + self.args
182
183
184class PosixCommand(BaseCommand):
185  # TODO(machenbach): Use base process start without shell once
186  # https://crbug.com/v8/8889 is resolved.
187  def _start_process(self):
188    def wrapped(arg):
189      if set('() \'"') & set(arg):
190        return "'%s'" % arg.replace("'", "'\"'\"'")
191      return arg
192    try:
193      return subprocess.Popen(
194        args=' '.join(map(wrapped, self._get_popen_args())),
195        stdout=subprocess.PIPE,
196        stderr=subprocess.PIPE,
197        env=self._get_env(),
198        shell=True,
199        # Make the new shell create its own process group. This allows to kill
200        # all spawned processes reliably (https://crbug.com/v8/8292).
201        preexec_fn=os.setsid,
202      )
203    except Exception as e:
204      sys.stderr.write('Error executing: %s\n' % self)
205      raise e
206
207  def _kill_process(self, process):
208    # Kill the whole process group (PID == GPID after setsid).
209    os.killpg(process.pid, signal.SIGKILL)
210
211
212def taskkill_windows(process, verbose=False, force=True):
213  force_flag = ' /F' if force else ''
214  tk = subprocess.Popen(
215      'taskkill /T%s /PID %d' % (force_flag, process.pid),
216      stdout=subprocess.PIPE,
217      stderr=subprocess.PIPE,
218  )
219  stdout, stderr = tk.communicate()
220  if verbose:
221    print('Taskkill results for %d' % process.pid)
222    print(stdout)
223    print(stderr)
224    print('Return code: %d' % tk.returncode)
225    sys.stdout.flush()
226
227
228class WindowsCommand(BaseCommand):
229  def _start_process(self, **kwargs):
230    # Try to change the error mode to avoid dialogs on fatal errors. Don't
231    # touch any existing error mode flags by merging the existing error mode.
232    # See http://blogs.msdn.com/oldnewthing/archive/2004/07/27/198410.aspx.
233    def set_error_mode(mode):
234      prev_error_mode = SEM_INVALID_VALUE
235      try:
236        import ctypes
237        prev_error_mode = (
238            ctypes.windll.kernel32.SetErrorMode(mode))  #@UndefinedVariable
239      except ImportError:
240        pass
241      return prev_error_mode
242
243    error_mode = SEM_NOGPFAULTERRORBOX
244    prev_error_mode = set_error_mode(error_mode)
245    set_error_mode(error_mode | prev_error_mode)
246
247    try:
248      return super(WindowsCommand, self)._start_process(**kwargs)
249    finally:
250      if prev_error_mode != SEM_INVALID_VALUE:
251        set_error_mode(prev_error_mode)
252
253  def _get_popen_args(self):
254    return subprocess.list2cmdline(self._to_args_list())
255
256  def _kill_process(self, process):
257    taskkill_windows(process, self.verbose)
258
259
260class AndroidCommand(BaseCommand):
261  # This must be initialized before creating any instances of this class.
262  driver = None
263
264  def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
265               verbose=False, resources_func=None, handle_sigterm=False):
266    """Initialize the command and all files that need to be pushed to the
267    Android device.
268    """
269    self.shell_name = os.path.basename(shell)
270    self.shell_dir = os.path.dirname(shell)
271    self.files_to_push = (resources_func or (lambda: []))()
272
273    # Make all paths in arguments relative and also prepare files from arguments
274    # for pushing to the device.
275    rel_args = []
276    find_path_re = re.compile(r'.*(%s/[^\'"]+).*' % re.escape(BASE_DIR))
277    for arg in (args or []):
278      match = find_path_re.match(arg)
279      if match:
280        self.files_to_push.append(match.group(1))
281      rel_args.append(
282          re.sub(r'(.*)%s/(.*)' % re.escape(BASE_DIR), r'\1\2', arg))
283
284    super(AndroidCommand, self).__init__(
285        shell, args=rel_args, cmd_prefix=cmd_prefix, timeout=timeout, env=env,
286        verbose=verbose, handle_sigterm=handle_sigterm)
287
288  def execute(self, **additional_popen_kwargs):
289    """Execute the command on the device.
290
291    This pushes all required files to the device and then runs the command.
292    """
293    if self.verbose:
294      print('# %s' % self)
295
296    self.driver.push_executable(self.shell_dir, 'bin', self.shell_name)
297
298    for abs_file in self.files_to_push:
299      abs_dir = os.path.dirname(abs_file)
300      file_name = os.path.basename(abs_file)
301      rel_dir = os.path.relpath(abs_dir, BASE_DIR)
302      self.driver.push_file(abs_dir, file_name, rel_dir)
303
304    start_time = time.time()
305    return_code = 0
306    timed_out = False
307    try:
308      stdout = self.driver.run(
309          'bin', self.shell_name, self.args, '.', self.timeout, self.env)
310    except CommandFailedException as e:
311      return_code = e.status
312      stdout = e.output
313    except TimeoutException as e:
314      return_code = 1
315      timed_out = True
316      # Sadly the Android driver doesn't provide output on timeout.
317      stdout = ''
318
319    duration = time.time() - start_time
320    return output.Output(
321        return_code,
322        timed_out,
323        stdout,
324        '',  # No stderr available.
325        -1,  # No pid available.
326        duration,
327    )
328
329
330Command = None
331def setup(target_os, device):
332  """Set the Command class to the OS-specific version."""
333  global Command
334  if target_os == 'android':
335    AndroidCommand.driver = android_driver(device)
336    Command = AndroidCommand
337  elif target_os == 'windows':
338    Command = WindowsCommand
339  else:
340    Command = PosixCommand
341
342def tear_down():
343  """Clean up after using commands."""
344  if Command == AndroidCommand:
345    AndroidCommand.driver.tear_down()
346