Notify user about gevent before halting debugger. Fixes https://github.com/microsoft/ptvsd/issues/2057

This commit is contained in:
Fabio Zadrozny 2020-02-05 15:11:44 -03:00
parent 5d5f8f42ec
commit 7d34a12644
8 changed files with 201 additions and 10 deletions

View file

@ -37,7 +37,7 @@ if [ "$PYDEVD_PYTHON_VERSION" = "3.6" ]; then
fi
if [ "$PYDEVD_PYTHON_VERSION" = "3.7" ]; then
conda install --yes pyqt=5 matplotlib
conda install --yes pyqt=5 matplotlib gevent
# Note: track the latest web framework versions.
pip install "django"
pip install "cherrypy"
@ -50,7 +50,8 @@ if [ "$PYDEVD_PYTHON_VERSION" = "3.8" ]; then
pip install "psutil"
pip install "numpy"
pip install trio
pip install gevent
# Note: track the latest web framework versions.
pip install "django"
pip install "cherrypy"

View file

@ -1,12 +1,9 @@
from _pydevd_bundle.pydevd_constants import DebugInfoHolder, SHOW_COMPILE_CYTHON_COMMAND_LINE, NULL
from _pydev_imps._pydev_saved_modules import threading
from contextlib import contextmanager
import traceback
import os
import sys
currentThread = threading.currentThread
class _LoggingGlobals(object):
@ -182,6 +179,20 @@ def error_once(msg, *args):
critical(message)
def exception_once(msg, *args):
try:
if args:
message = msg % args
else:
message = str(msg)
except:
message = '%s - %s' % (msg, args)
if message not in _LoggingGlobals._warn_once_map:
_LoggingGlobals._warn_once_map[message] = True
exception(message)
def debug_once(msg, *args):
if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 3:
error_once(msg, *args)

View file

@ -95,7 +95,8 @@ from _pydevd_bundle import pydevd_xml
from _pydevd_bundle import pydevd_vm_type
import sys
import traceback
from _pydevd_bundle.pydevd_utils import quote_smart as quote, compare_object_attrs_key
from _pydevd_bundle.pydevd_utils import quote_smart as quote, compare_object_attrs_key, \
notify_about_gevent_if_needed
from _pydev_bundle import pydev_log
from _pydev_bundle.pydev_log import exception as pydev_log_exception
from _pydev_bundle import _pydev_completer
@ -141,6 +142,7 @@ class PyDBDaemonThread(threading.Thread):
-- Note: use through run_as_pydevd_daemon_thread().
'''
threading.Thread.__init__(self)
notify_about_gevent_if_needed()
self._py_db = weakref.ref(py_db)
self._kill_received = False
mark_as_pydevd_daemon_thread(self)
@ -294,6 +296,7 @@ class ReaderThread(PyDBDaemonThread):
# client itself closes the connection (although on kill received we stop actually
# processing anything read).
try:
notify_about_gevent_if_needed()
line = self._read_line()
if len(line) == 0:
@ -436,6 +439,7 @@ class WriterThread(PyDBDaemonThread):
for listener in self.py_db.dap_messages_listeners:
listener.before_send(cmd.as_dict)
notify_about_gevent_if_needed()
cmd.send(self.sock)
if cmd.id == CMD_EXIT:

View file

@ -151,7 +151,15 @@ try:
except AttributeError:
PY_IMPL_NAME = ''
SUPPORT_GEVENT = os.getenv('GEVENT_SUPPORT', 'False') == 'True'
SUPPORT_GEVENT = os.getenv('GEVENT_SUPPORT', 'False') in ('True', 'true', '1')
GEVENT_SUPPORT_NOT_SET_MSG = os.getenv(
'GEVENT_SUPPORT_NOT_SET_MSG',
'It seems that the gevent monkey-patching is being used.\n'
'Please set an environment variable with:\n'
'GEVENT_SUPPORT=True\n'
'to enable gevent support in the debugger.'
)
USE_LIB_COPY = SUPPORT_GEVENT

View file

@ -1,6 +1,7 @@
from __future__ import nested_scopes
import traceback
import warnings
from _pydev_bundle import pydev_log
try:
from urllib import quote
@ -9,7 +10,8 @@ except:
import inspect
import sys
from _pydevd_bundle.pydevd_constants import IS_PY3K, USE_CUSTOM_SYS_CURRENT_FRAMES, IS_PYPY
from _pydevd_bundle.pydevd_constants import IS_PY3K, USE_CUSTOM_SYS_CURRENT_FRAMES, IS_PYPY, SUPPORT_GEVENT, \
GEVENT_SUPPORT_NOT_SET_MSG
from _pydev_imps._pydev_saved_modules import threading
@ -243,3 +245,29 @@ def convert_dap_log_message_to_expression(log_message):
return repr(expression)
# Note: use '%' to be compatible with Python 2.6.
return repr(expression) + ' % (' + ', '.join(str(x) for x in expression_vars) + ',)'
def notify_about_gevent_if_needed(stream=None):
'''
When debugging with gevent check that the gevent flag is used if the user uses the gevent
monkey-patching.
:return bool:
Returns True if a message had to be shown to the user and False otherwise.
'''
stream = stream if stream is not None else sys.stderr
if not SUPPORT_GEVENT:
gevent_monkey = sys.modules.get('gevent.monkey')
if gevent_monkey is not None:
try:
saved = gevent_monkey.saved
except AttributeError:
pydev_log.exception_once('Error checking for gevent monkey-patching.')
return False
if saved:
# Note: print to stderr as it may deadlock the debugger.
sys.stderr.write('%s\n' % (GEVENT_SUPPORT_NOT_SET_MSG,))
return True
return False

View file

@ -279,6 +279,85 @@ Memory after: %s
from tests_python.regression_check import data_regression, datadir, original_datadir
@pytest.fixture
def pyfile(request, tmpdir):
"""
Based on debugpy pyfile fixture (adapter for older versions of Python)
A fixture providing a factory function that generates .py files.
The returned factory takes a single function with an empty argument list,
generates a temporary file that contains the code corresponding to the
function body, and returns the full path to the generated file. Idiomatic
use is as a decorator, e.g.:
@pyfile
def script_file():
print('fizz')
print('buzz')
will produce a temporary file named script_file.py containing:
print('fizz')
print('buzz')
and the variable script_file will contain the path to that file.
In order for the factory to be able to extract the function body properly,
function header ("def") must all be on a single line, with nothing after
the colon but whitespace.
Note that because the code is physically in a separate file when it runs,
it cannot reuse top-level module imports - it must import all the modules
that it uses locally. When linter complains, use #noqa.
Returns a py.path.local instance that has the additional attribute "lines".
After the source is writen to disk, tests.code.get_marked_line_numbers() is
invoked on the resulting file to compute the value of that attribute.
"""
import types
import inspect
def factory(source):
assert isinstance(source, types.FunctionType)
name = source.__name__
source, _ = inspect.getsourcelines(source)
# First, find the "def" line.
def_lineno = 0
for line in source:
line = line.strip()
if line.startswith("def") and line.endswith(":"):
break
def_lineno += 1
else:
raise ValueError("Failed to locate function header.")
# Remove everything up to and including "def".
source = source[def_lineno + 1 :]
assert source
# Now we need to adjust indentation. Compute how much the first line of
# the body is indented by, then dedent all lines by that amount. Blank
# lines don't matter indentation-wise, and might not be indented to begin
# with, so just replace them with a simple newline.
line = source[0]
indent = len(line) - len(line.lstrip())
source = [l[indent:] if l.strip() else "\n" for l in source]
source = "".join(source)
# Write it to file.
tmpfile = os.path.join(str(tmpdir), name + ".py")
assert not os.path.exists(tmpfile), '%s already exists.' % (tmpfile,)
with open(tmpfile, 'w') as stream:
stream.write(source)
return tmpfile
return factory
if IS_JYTHON or IS_IRONPYTHON:
# On Jython and IronPython, it's a no-op.

View file

@ -2781,6 +2781,41 @@ def test_wait_for_attach_gevent(case_setup_remote_attach_to):
writer.finished_ok = True
@pytest.mark.skipif(not TEST_GEVENT, reason='Gevent not installed.')
def test_notify_gevent(case_setup, pyfile):
def get_environ(writer):
# I.e.: Make sure that gevent support is disabled
env = os.environ.copy()
env['GEVENT_SUPPORT'] = ''
return env
@pyfile
def case_gevent():
from gevent import monkey
monkey.patch_all()
print('TEST SUCEEDED')
def additional_output_checks(writer, stdout, stderr):
assert 'environment variable' in stderr
assert 'GEVENT_SUPPORT=True' in stderr
with case_setup.test_file(
case_gevent,
get_environ=get_environ,
additional_output_checks=additional_output_checks,
EXPECTED_RETURNCODE='any',
FORCE_KILL_PROCESS_WHEN_FINISHED_OK=True
) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch()
json_facade.write_make_initial_run()
wait_for_condition(lambda: 'GEVENT_SUPPORT=True' in writer.get_stderr())
writer.finished_ok = True
@pytest.mark.skipif(IS_JYTHON, reason='Flaky on Jython.')
def test_path_translation_and_source_reference(case_setup):

View file

@ -2,7 +2,7 @@ import threading
from _pydevd_bundle.pydevd_comm import pydevd_find_thread_by_id
from _pydevd_bundle.pydevd_utils import convert_dap_log_message_to_expression
from tests_python.debug_constants import IS_PY26, IS_PY3K
from tests_python.debug_constants import IS_PY26, IS_PY3K, TEST_GEVENT
import sys
from _pydevd_bundle.pydevd_constants import IS_CPYTHON, IS_WINDOWS
import pytest
@ -277,9 +277,10 @@ def _build_launch_env():
return cwd, environ
def _check_in_separate_process(method_name, module_name='test_utilities'):
def _check_in_separate_process(method_name, module_name='test_utilities', update_env={}):
import subprocess
cwd, environ = _build_launch_env()
environ.update(update_env)
subprocess.check_call(
[sys.executable, '-c', 'import %(module_name)s;%(module_name)s.%(method_name)s()' % dict(
@ -336,3 +337,27 @@ def test_get_ppid():
else:
assert api._get_windows_ppid() is not None
def _check_gevent(expect_msg):
from _pydevd_bundle.pydevd_utils import notify_about_gevent_if_needed
assert not notify_about_gevent_if_needed()
import gevent
assert not notify_about_gevent_if_needed()
import gevent.monkey
assert not notify_about_gevent_if_needed()
gevent.monkey.patch_all()
assert notify_about_gevent_if_needed() == expect_msg
def check_notify_on_gevent_loaded():
_check_gevent(True)
def check_dont_notify_on_gevent_loaded():
_check_gevent(False)
@pytest.mark.skipif(not TEST_GEVENT, reason='Gevent not installed.')
def test_gevent_notify():
_check_in_separate_process('check_notify_on_gevent_loaded', update_env={'GEVENT_SUPPORT': ''})
_check_in_separate_process('check_dont_notify_on_gevent_loaded', update_env={'GEVENT_SUPPORT': 'True'})