|
Packit |
f0b94e |
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
Packit |
f0b94e |
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
Packit |
f0b94e |
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
Packit |
f0b94e |
from __future__ import absolute_import
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
import pprint
|
|
Packit |
f0b94e |
import signal
|
|
Packit |
f0b94e |
import time
|
|
Packit |
f0b94e |
import traceback
|
|
Packit |
f0b94e |
import subprocess
|
|
Packit |
f0b94e |
from threading import Event
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
import mozcrash
|
|
Packit |
f0b94e |
import psutil
|
|
Packit |
f0b94e |
from mozlog import get_proxy_logger
|
|
Packit |
f0b94e |
from mozprocess import ProcessHandler
|
|
Packit |
f0b94e |
from utils import TalosError
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
LOG = get_proxy_logger()
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
class ProcessContext(object):
|
|
Packit |
f0b94e |
"""
|
|
Packit |
f0b94e |
Store useful results of the browser execution.
|
|
Packit |
f0b94e |
"""
|
|
Packit |
f0b94e |
def __init__(self):
|
|
Packit |
f0b94e |
self.output = None
|
|
Packit |
f0b94e |
self.process = None
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
@property
|
|
Packit |
f0b94e |
def pid(self):
|
|
Packit |
f0b94e |
return self.process and self.process.pid
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
def kill_process(self):
|
|
Packit |
f0b94e |
"""
|
|
Packit |
f0b94e |
Kill the process, returning the exit code or None if the process
|
|
Packit |
f0b94e |
is already finished.
|
|
Packit |
f0b94e |
"""
|
|
Packit |
f0b94e |
if self.process and self.process.is_running():
|
|
Packit |
f0b94e |
LOG.debug("Terminating %s" % self.process)
|
|
Packit |
f0b94e |
try:
|
|
Packit |
f0b94e |
self.process.terminate()
|
|
Packit |
f0b94e |
except psutil.NoSuchProcess:
|
|
Packit |
f0b94e |
procs = self.process.children()
|
|
Packit |
f0b94e |
for p in procs:
|
|
Packit |
f0b94e |
c = ProcessContext()
|
|
Packit |
f0b94e |
c.process = p
|
|
Packit |
f0b94e |
c.kill_process()
|
|
Packit |
f0b94e |
return self.process.returncode
|
|
Packit |
f0b94e |
try:
|
|
Packit |
f0b94e |
return self.process.wait(3)
|
|
Packit |
f0b94e |
except psutil.TimeoutExpired:
|
|
Packit |
f0b94e |
self.process.kill()
|
|
Packit |
f0b94e |
# will raise TimeoutExpired if unable to kill
|
|
Packit |
f0b94e |
return self.process.wait(3)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
class Reader(object):
|
|
Packit |
f0b94e |
def __init__(self, event):
|
|
Packit |
f0b94e |
self.output = []
|
|
Packit |
f0b94e |
self.got_end_timestamp = False
|
|
Packit |
f0b94e |
self.got_timeout = False
|
|
Packit |
f0b94e |
self.timeout_message = ''
|
|
Packit |
f0b94e |
self.event = event
|
|
Packit |
f0b94e |
self.proc = None
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
def __call__(self, line):
|
|
Packit |
f0b94e |
if line.find('__endTimestamp') != -1:
|
|
Packit |
f0b94e |
self.got_end_timestamp = True
|
|
Packit |
f0b94e |
self.event.set()
|
|
Packit |
f0b94e |
elif line == 'TART: TIMEOUT':
|
|
Packit |
f0b94e |
self.got_timeout = True
|
|
Packit |
f0b94e |
self.timeout_message = 'TART'
|
|
Packit |
f0b94e |
self.event.set()
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
if not (line.startswith('JavaScript error:') or
|
|
Packit |
f0b94e |
line.startswith('JavaScript warning:')):
|
|
Packit |
f0b94e |
LOG.process_output(self.proc.pid, line)
|
|
Packit |
f0b94e |
self.output.append(line)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
def run_browser(command, minidump_dir, timeout=None, on_started=None,
|
|
Packit |
f0b94e |
debug=None, debugger=None, debugger_args=None, **kwargs):
|
|
Packit |
f0b94e |
"""
|
|
Packit |
f0b94e |
Run the browser using the given `command`.
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
After the browser prints __endTimestamp, we give it 5
|
|
Packit |
f0b94e |
seconds to quit and kill it if it's still alive at that point.
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
Note that this method ensure that the process is killed at
|
|
Packit |
f0b94e |
the end. If this is not possible, an exception will be raised.
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
:param command: the commad (as a string list) to run the browser
|
|
Packit |
f0b94e |
:param minidump_dir: a path where to extract minidumps in case the
|
|
Packit |
f0b94e |
browser hang. This have to be the same value
|
|
Packit |
f0b94e |
used in `mozcrash.check_for_crashes`.
|
|
Packit |
f0b94e |
:param timeout: if specified, timeout to wait for the browser before
|
|
Packit |
f0b94e |
we raise a :class:`TalosError`
|
|
Packit |
f0b94e |
:param on_started: a callback that can be used to do things just after
|
|
Packit |
f0b94e |
the browser has been started. The callback must takes
|
|
Packit |
f0b94e |
an argument, which is the psutil.Process instance
|
|
Packit |
f0b94e |
:param kwargs: additional keyword arguments for the :class:`ProcessHandler`
|
|
Packit |
f0b94e |
instance
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
Returns a ProcessContext instance, with available output and pid used.
|
|
Packit |
f0b94e |
"""
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
debugger_info = find_debugger_info(debug, debugger, debugger_args)
|
|
Packit |
f0b94e |
if debugger_info is not None:
|
|
Packit |
f0b94e |
return run_in_debug_mode(command, debugger_info,
|
|
Packit |
f0b94e |
on_started=on_started, env=kwargs.get('env'))
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
context = ProcessContext()
|
|
Packit |
f0b94e |
first_time = int(time.time()) * 1000
|
|
Packit |
f0b94e |
wait_for_quit_timeout = 5
|
|
Packit |
f0b94e |
event = Event()
|
|
Packit |
f0b94e |
reader = Reader(event)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
LOG.info("Using env: %s" % pprint.pformat(kwargs['env']))
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
kwargs['storeOutput'] = False
|
|
Packit |
f0b94e |
kwargs['processOutputLine'] = reader
|
|
Packit |
f0b94e |
kwargs['onFinish'] = event.set
|
|
Packit |
f0b94e |
proc = ProcessHandler(command, **kwargs)
|
|
Packit |
f0b94e |
reader.proc = proc
|
|
Packit |
f0b94e |
proc.run()
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
LOG.process_start(proc.pid, ' '.join(command))
|
|
Packit |
f0b94e |
try:
|
|
Packit |
f0b94e |
context.process = psutil.Process(proc.pid)
|
|
Packit |
f0b94e |
if on_started:
|
|
Packit |
f0b94e |
on_started(context.process)
|
|
Packit |
f0b94e |
# wait until we saw __endTimestamp in the proc output,
|
|
Packit |
f0b94e |
# or the browser just terminated - or we have a timeout
|
|
Packit |
f0b94e |
if not event.wait(timeout):
|
|
Packit |
f0b94e |
LOG.info("Timeout waiting for test completion; killing browser...")
|
|
Packit |
f0b94e |
# try to extract the minidump stack if the browser hangs
|
|
Packit |
f0b94e |
mozcrash.kill_and_get_minidump(proc.pid, minidump_dir)
|
|
Packit |
f0b94e |
raise TalosError("timeout")
|
|
Packit |
f0b94e |
if reader.got_end_timestamp:
|
|
Packit |
f0b94e |
for i in range(1, wait_for_quit_timeout):
|
|
Packit |
f0b94e |
if proc.wait(1) is not None:
|
|
Packit |
f0b94e |
break
|
|
Packit |
f0b94e |
if proc.poll() is None:
|
|
Packit |
f0b94e |
LOG.info(
|
|
Packit |
f0b94e |
"Browser shutdown timed out after {0} seconds, terminating"
|
|
Packit |
f0b94e |
" process.".format(wait_for_quit_timeout)
|
|
Packit |
f0b94e |
)
|
|
Packit |
f0b94e |
elif reader.got_timeout:
|
|
Packit |
f0b94e |
raise TalosError('TIMEOUT: %s' % reader.timeout_message)
|
|
Packit |
f0b94e |
finally:
|
|
Packit |
f0b94e |
# this also handle KeyboardInterrupt
|
|
Packit |
f0b94e |
# ensure early the process is really terminated
|
|
Packit |
f0b94e |
return_code = None
|
|
Packit |
f0b94e |
try:
|
|
Packit |
f0b94e |
return_code = context.kill_process()
|
|
Packit |
f0b94e |
if return_code is None:
|
|
Packit |
f0b94e |
return_code = proc.wait(1)
|
|
Packit |
f0b94e |
except Exception:
|
|
Packit |
f0b94e |
# Maybe killed by kill_and_get_minidump(), maybe ended?
|
|
Packit |
f0b94e |
LOG.info("Unable to kill process")
|
|
Packit |
f0b94e |
LOG.info(traceback.format_exc())
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
reader.output.append(
|
|
Packit |
f0b94e |
"__startBeforeLaunchTimestamp%d__endBeforeLaunchTimestamp"
|
|
Packit |
f0b94e |
% first_time)
|
|
Packit |
f0b94e |
reader.output.append(
|
|
Packit |
f0b94e |
"__startAfterTerminationTimestamp%d__endAfterTerminationTimestamp"
|
|
Packit |
f0b94e |
% (int(time.time()) * 1000))
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
if return_code is not None:
|
|
Packit |
f0b94e |
LOG.process_exit(proc.pid, return_code)
|
|
Packit |
f0b94e |
else:
|
|
Packit |
f0b94e |
LOG.debug("Unable to detect exit code of the process %s." % proc.pid)
|
|
Packit |
f0b94e |
context.output = reader.output
|
|
Packit |
f0b94e |
return context
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
def find_debugger_info(debug, debugger, debugger_args):
|
|
Packit |
f0b94e |
debuggerInfo = None
|
|
Packit |
f0b94e |
if debug or debugger or debugger_args:
|
|
Packit |
f0b94e |
import mozdebug
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
if not debugger:
|
|
Packit |
f0b94e |
# No debugger name was provided. Look for the default ones on
|
|
Packit |
f0b94e |
# current OS.
|
|
Packit |
f0b94e |
debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
debuggerInfo = None
|
|
Packit |
f0b94e |
if debugger:
|
|
Packit |
f0b94e |
debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
if debuggerInfo is None:
|
|
Packit |
f0b94e |
raise TalosError('Could not find a suitable debugger in your PATH.')
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
return debuggerInfo
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
def run_in_debug_mode(command, debugger_info, on_started=None, env=None):
|
|
Packit |
f0b94e |
signal.signal(signal.SIGINT, lambda sigid, frame: None)
|
|
Packit |
f0b94e |
context = ProcessContext()
|
|
Packit |
f0b94e |
command_under_dbg = [debugger_info.path] + debugger_info.args + command
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
ttest_process = subprocess.Popen(command_under_dbg, env=env)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
context.process = psutil.Process(ttest_process.pid)
|
|
Packit |
f0b94e |
if on_started:
|
|
Packit |
f0b94e |
on_started(context.process)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
return_code = ttest_process.wait()
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
if return_code is not None:
|
|
Packit |
f0b94e |
LOG.process_exit(ttest_process.pid, return_code)
|
|
Packit |
f0b94e |
else:
|
|
Packit |
f0b94e |
LOG.debug("Unable to detect exit code of the process %s." % ttest_process.pid)
|
|
Packit |
f0b94e |
|
|
Packit |
f0b94e |
return context
|