1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this file, 3# You can obtain one at http://mozilla.org/MPL/2.0/. 4from __future__ import absolute_import 5 6import pprint 7import signal 8import sys 9import time 10import traceback 11import subprocess 12from threading import Event 13 14import mozcrash 15import psutil 16from mozlog import get_proxy_logger 17from mozprocess import ProcessHandler 18from talos.utils import TalosError 19 20LOG = get_proxy_logger() 21 22 23class ProcessContext(object): 24 """ 25 Store useful results of the browser execution. 26 """ 27 def __init__(self, is_launcher=False): 28 self.output = None 29 self.process = None 30 self.is_launcher = is_launcher 31 32 @property 33 def pid(self): 34 return self.process and self.process.pid 35 36 def kill_process(self): 37 """ 38 Kill the process, returning the exit code or None if the process 39 is already finished. 40 """ 41 parentProc = self.process 42 # If we're using a launcher process, terminate that instead of us: 43 kids = parentProc and parentProc.is_running() and parentProc.children() 44 if self.is_launcher and kids and len(kids) == 1 and kids[0].is_running(): 45 LOG.debug(("Launcher process {} detected. Terminating parent" 46 " process {} instead.").format(parentProc, kids[0])) 47 parentProc = kids[0] 48 49 if parentProc and parentProc.is_running(): 50 LOG.debug("Terminating %s" % parentProc) 51 try: 52 parentProc.terminate() 53 except psutil.NoSuchProcess: 54 procs = parentProc.children() 55 for p in procs: 56 c = ProcessContext() 57 c.process = p 58 c.kill_process() 59 return parentProc.returncode 60 try: 61 return parentProc.wait(3) 62 except psutil.TimeoutExpired: 63 LOG.debug("Killing %s" % parentProc) 64 parentProc.kill() 65 # will raise TimeoutExpired if unable to kill 66 return parentProc.wait(3) 67 68 69class Reader(object): 70 def __init__(self, event): 71 self.output = [] 72 self.got_end_timestamp = False 73 self.got_timeout = False 74 self.timeout_message = '' 75 self.got_error = False 76 self.event = event 77 self.proc = None 78 79 def __call__(self, line): 80 if line.find('__endTimestamp') != -1: 81 self.got_end_timestamp = True 82 self.event.set() 83 elif line == 'TART: TIMEOUT': 84 self.got_timeout = True 85 self.timeout_message = 'TART' 86 self.event.set() 87 elif line.startswith('TEST-UNEXPECTED-FAIL | '): 88 self.got_error = True 89 self.event.set() 90 91 if not (line.startswith('JavaScript error:') or 92 line.startswith('JavaScript warning:')): 93 LOG.process_output(self.proc.pid, line) 94 self.output.append(line) 95 96 97def run_browser(command, minidump_dir, timeout=None, on_started=None, 98 debug=None, debugger=None, debugger_args=None, **kwargs): 99 """ 100 Run the browser using the given `command`. 101 102 After the browser prints __endTimestamp, we give it 5 103 seconds to quit and kill it if it's still alive at that point. 104 105 Note that this method ensure that the process is killed at 106 the end. If this is not possible, an exception will be raised. 107 108 :param command: the commad (as a string list) to run the browser 109 :param minidump_dir: a path where to extract minidumps in case the 110 browser hang. This have to be the same value 111 used in `mozcrash.check_for_crashes`. 112 :param timeout: if specified, timeout to wait for the browser before 113 we raise a :class:`TalosError` 114 :param on_started: a callback that can be used to do things just after 115 the browser has been started. The callback must takes 116 an argument, which is the psutil.Process instance 117 :param kwargs: additional keyword arguments for the :class:`ProcessHandler` 118 instance 119 120 Returns a ProcessContext instance, with available output and pid used. 121 """ 122 123 debugger_info = find_debugger_info(debug, debugger, debugger_args) 124 if debugger_info is not None: 125 return run_in_debug_mode(command, debugger_info, 126 on_started=on_started, env=kwargs.get('env')) 127 128 is_launcher = sys.platform.startswith('win') and '-wait-for-browser' in command 129 context = ProcessContext(is_launcher) 130 first_time = int(time.time()) * 1000 131 wait_for_quit_timeout = 5 132 event = Event() 133 reader = Reader(event) 134 135 LOG.info("Using env: %s" % pprint.pformat(kwargs['env'])) 136 137 kwargs['storeOutput'] = False 138 kwargs['processOutputLine'] = reader 139 kwargs['onFinish'] = event.set 140 proc = ProcessHandler(command, **kwargs) 141 reader.proc = proc 142 proc.run() 143 144 LOG.process_start(proc.pid, ' '.join(command)) 145 try: 146 context.process = psutil.Process(proc.pid) 147 if on_started: 148 on_started(context.process) 149 # wait until we saw __endTimestamp in the proc output, 150 # or the browser just terminated - or we have a timeout 151 if not event.wait(timeout): 152 LOG.info("Timeout waiting for test completion; killing browser...") 153 # try to extract the minidump stack if the browser hangs 154 kill_and_get_minidump(context, minidump_dir) 155 raise TalosError("timeout") 156 if reader.got_end_timestamp: 157 for i in range(1, wait_for_quit_timeout): 158 if proc.wait(1) is not None: 159 break 160 if proc.poll() is None: 161 LOG.info( 162 "Browser shutdown timed out after {0} seconds, killing" 163 " process.".format(wait_for_quit_timeout) 164 ) 165 kill_and_get_minidump(context, minidump_dir) 166 raise TalosError( 167 "Browser shutdown timed out after {0} seconds, killed" 168 " process.".format(wait_for_quit_timeout) 169 ) 170 elif reader.got_timeout: 171 raise TalosError('TIMEOUT: %s' % reader.timeout_message) 172 elif reader.got_error: 173 raise TalosError("unexpected error") 174 finally: 175 # this also handle KeyboardInterrupt 176 # ensure early the process is really terminated 177 return_code = None 178 try: 179 return_code = context.kill_process() 180 if return_code is None: 181 return_code = proc.wait(1) 182 except Exception: 183 # Maybe killed by kill_and_get_minidump(), maybe ended? 184 LOG.info("Unable to kill process") 185 LOG.info(traceback.format_exc()) 186 187 reader.output.append( 188 "__startBeforeLaunchTimestamp%d__endBeforeLaunchTimestamp" 189 % first_time) 190 reader.output.append( 191 "__startAfterTerminationTimestamp%d__endAfterTerminationTimestamp" 192 % (int(time.time()) * 1000)) 193 194 if return_code is not None: 195 LOG.process_exit(proc.pid, return_code) 196 else: 197 LOG.debug("Unable to detect exit code of the process %s." % proc.pid) 198 context.output = reader.output 199 return context 200 201 202def find_debugger_info(debug, debugger, debugger_args): 203 debuggerInfo = None 204 if debug or debugger or debugger_args: 205 import mozdebug 206 207 if not debugger: 208 # No debugger name was provided. Look for the default ones on 209 # current OS. 210 debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking) 211 212 debuggerInfo = None 213 if debugger: 214 debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) 215 216 if debuggerInfo is None: 217 raise TalosError('Could not find a suitable debugger in your PATH.') 218 219 return debuggerInfo 220 221 222def run_in_debug_mode(command, debugger_info, on_started=None, env=None): 223 signal.signal(signal.SIGINT, lambda sigid, frame: None) 224 context = ProcessContext() 225 command_under_dbg = [debugger_info.path] + debugger_info.args + command 226 227 ttest_process = subprocess.Popen(command_under_dbg, env=env) 228 229 context.process = psutil.Process(ttest_process.pid) 230 if on_started: 231 on_started(context.process) 232 233 return_code = ttest_process.wait() 234 235 if return_code is not None: 236 LOG.process_exit(ttest_process.pid, return_code) 237 else: 238 LOG.debug("Unable to detect exit code of the process %s." % ttest_process.pid) 239 240 return context 241 242 243def kill_and_get_minidump(context, minidump_dir): 244 proc = context.process 245 if context.is_launcher: 246 kids = context.process.children() 247 if len(kids) == 1: 248 LOG.debug(("Launcher process {} detected. Killing parent" 249 " process {} instead.").format(proc, kids[0])) 250 proc = kids[0] 251 LOG.debug("Killing %s and writing a minidump file" % proc) 252 mozcrash.kill_and_get_minidump(proc.pid, minidump_dir) 253