mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Notify user about gevent before halting debugger. Fixes https://github.com/microsoft/ptvsd/issues/2057
This commit is contained in:
parent
5d5f8f42ec
commit
7d34a12644
8 changed files with 201 additions and 10 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue