• 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
5
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
19
20BASE_DIR = os.path.normpath(
21    os.path.join(os.path.dirname(os.path.abspath(__file__)), '..' , '..', '..'))
22
23SEM_INVALID_VALUE = -1
24SEM_NOGPFAULTERRORBOX = 0x0002  # Microsoft Platform SDK WinBase.h
25
26
27def 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
35class AbortException(Exception):
36  """Indicates early abort on SIGINT, SIGTERM or internal hard timeout."""
37  pass
38
39
40class 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
155class PosixCommand(BaseCommand):
156  def _kill_process(self, process):
157    process.kill()
158
159
160class 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
206class 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
273Command = None
274def 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
284def tear_down():
285  """Clean up after using commands."""
286  if Command == AndroidCommand:
287    android_driver().tear_down()
288