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