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