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