Skip to content

Commit 7d34a12

Browse files
committed
Notify user about gevent before halting debugger. Fixes microsoft/ptvsd#2057
1 parent 5d5f8f4 commit 7d34a12

8 files changed

Lines changed: 201 additions & 10 deletions

File tree

src/debugpy/_vendored/pydevd/.travis/install_python_deps.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ if [ "$PYDEVD_PYTHON_VERSION" = "3.6" ]; then
3737
fi
3838

3939
if [ "$PYDEVD_PYTHON_VERSION" = "3.7" ]; then
40-
conda install --yes pyqt=5 matplotlib
40+
conda install --yes pyqt=5 matplotlib gevent
4141
# Note: track the latest web framework versions.
4242
pip install "django"
4343
pip install "cherrypy"
@@ -50,7 +50,8 @@ if [ "$PYDEVD_PYTHON_VERSION" = "3.8" ]; then
5050
pip install "psutil"
5151
pip install "numpy"
5252
pip install trio
53-
53+
pip install gevent
54+
5455
# Note: track the latest web framework versions.
5556
pip install "django"
5657
pip install "cherrypy"

src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_log.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
from _pydevd_bundle.pydevd_constants import DebugInfoHolder, SHOW_COMPILE_CYTHON_COMMAND_LINE, NULL
2-
from _pydev_imps._pydev_saved_modules import threading
32
from contextlib import contextmanager
43
import traceback
54
import os
65
import sys
76

8-
currentThread = threading.currentThread
9-
107

118
class _LoggingGlobals(object):
129

@@ -182,6 +179,20 @@ def error_once(msg, *args):
182179
critical(message)
183180

184181

182+
def exception_once(msg, *args):
183+
try:
184+
if args:
185+
message = msg % args
186+
else:
187+
message = str(msg)
188+
except:
189+
message = '%s - %s' % (msg, args)
190+
191+
if message not in _LoggingGlobals._warn_once_map:
192+
_LoggingGlobals._warn_once_map[message] = True
193+
exception(message)
194+
195+
185196
def debug_once(msg, *args):
186197
if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 3:
187198
error_once(msg, *args)

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@
9595
from _pydevd_bundle import pydevd_vm_type
9696
import sys
9797
import traceback
98-
from _pydevd_bundle.pydevd_utils import quote_smart as quote, compare_object_attrs_key
98+
from _pydevd_bundle.pydevd_utils import quote_smart as quote, compare_object_attrs_key, \
99+
notify_about_gevent_if_needed
99100
from _pydev_bundle import pydev_log
100101
from _pydev_bundle.pydev_log import exception as pydev_log_exception
101102
from _pydev_bundle import _pydev_completer
@@ -141,6 +142,7 @@ def __init__(self, py_db, target_and_args=None):
141142
-- Note: use through run_as_pydevd_daemon_thread().
142143
'''
143144
threading.Thread.__init__(self)
145+
notify_about_gevent_if_needed()
144146
self._py_db = weakref.ref(py_db)
145147
self._kill_received = False
146148
mark_as_pydevd_daemon_thread(self)
@@ -294,6 +296,7 @@ def _on_run(self):
294296
# client itself closes the connection (although on kill received we stop actually
295297
# processing anything read).
296298
try:
299+
notify_about_gevent_if_needed()
297300
line = self._read_line()
298301

299302
if len(line) == 0:
@@ -436,6 +439,7 @@ def _on_run(self):
436439
for listener in self.py_db.dap_messages_listeners:
437440
listener.before_send(cmd.as_dict)
438441

442+
notify_about_gevent_if_needed()
439443
cmd.send(self.sock)
440444

441445
if cmd.id == CMD_EXIT:

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,15 @@ def version_str(v):
151151
except AttributeError:
152152
PY_IMPL_NAME = ''
153153

154-
SUPPORT_GEVENT = os.getenv('GEVENT_SUPPORT', 'False') == 'True'
154+
SUPPORT_GEVENT = os.getenv('GEVENT_SUPPORT', 'False') in ('True', 'true', '1')
155+
156+
GEVENT_SUPPORT_NOT_SET_MSG = os.getenv(
157+
'GEVENT_SUPPORT_NOT_SET_MSG',
158+
'It seems that the gevent monkey-patching is being used.\n'
159+
'Please set an environment variable with:\n'
160+
'GEVENT_SUPPORT=True\n'
161+
'to enable gevent support in the debugger.'
162+
)
155163

156164
USE_LIB_COPY = SUPPORT_GEVENT
157165

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import nested_scopes
22
import traceback
33
import warnings
4+
from _pydev_bundle import pydev_log
45

56
try:
67
from urllib import quote
@@ -9,7 +10,8 @@
910

1011
import inspect
1112
import sys
12-
from _pydevd_bundle.pydevd_constants import IS_PY3K, USE_CUSTOM_SYS_CURRENT_FRAMES, IS_PYPY
13+
from _pydevd_bundle.pydevd_constants import IS_PY3K, USE_CUSTOM_SYS_CURRENT_FRAMES, IS_PYPY, SUPPORT_GEVENT, \
14+
GEVENT_SUPPORT_NOT_SET_MSG
1315
from _pydev_imps._pydev_saved_modules import threading
1416

1517

@@ -243,3 +245,29 @@ def convert_dap_log_message_to_expression(log_message):
243245
return repr(expression)
244246
# Note: use '%' to be compatible with Python 2.6.
245247
return repr(expression) + ' % (' + ', '.join(str(x) for x in expression_vars) + ',)'
248+
249+
250+
def notify_about_gevent_if_needed(stream=None):
251+
'''
252+
When debugging with gevent check that the gevent flag is used if the user uses the gevent
253+
monkey-patching.
254+
255+
:return bool:
256+
Returns True if a message had to be shown to the user and False otherwise.
257+
'''
258+
stream = stream if stream is not None else sys.stderr
259+
if not SUPPORT_GEVENT:
260+
gevent_monkey = sys.modules.get('gevent.monkey')
261+
if gevent_monkey is not None:
262+
try:
263+
saved = gevent_monkey.saved
264+
except AttributeError:
265+
pydev_log.exception_once('Error checking for gevent monkey-patching.')
266+
return False
267+
268+
if saved:
269+
# Note: print to stderr as it may deadlock the debugger.
270+
sys.stderr.write('%s\n' % (GEVENT_SUPPORT_NOT_SET_MSG,))
271+
return True
272+
273+
return False

src/debugpy/_vendored/pydevd/conftest.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,85 @@ def before_after_each_function(request):
279279

280280
from tests_python.regression_check import data_regression, datadir, original_datadir
281281

282+
283+
@pytest.fixture
284+
def pyfile(request, tmpdir):
285+
"""
286+
Based on debugpy pyfile fixture (adapter for older versions of Python)
287+
288+
A fixture providing a factory function that generates .py files.
289+
290+
The returned factory takes a single function with an empty argument list,
291+
generates a temporary file that contains the code corresponding to the
292+
function body, and returns the full path to the generated file. Idiomatic
293+
use is as a decorator, e.g.:
294+
295+
@pyfile
296+
def script_file():
297+
print('fizz')
298+
print('buzz')
299+
300+
will produce a temporary file named script_file.py containing:
301+
302+
print('fizz')
303+
print('buzz')
304+
305+
and the variable script_file will contain the path to that file.
306+
307+
In order for the factory to be able to extract the function body properly,
308+
function header ("def") must all be on a single line, with nothing after
309+
the colon but whitespace.
310+
311+
Note that because the code is physically in a separate file when it runs,
312+
it cannot reuse top-level module imports - it must import all the modules
313+
that it uses locally. When linter complains, use #noqa.
314+
315+
Returns a py.path.local instance that has the additional attribute "lines".
316+
After the source is writen to disk, tests.code.get_marked_line_numbers() is
317+
invoked on the resulting file to compute the value of that attribute.
318+
"""
319+
import types
320+
import inspect
321+
322+
def factory(source):
323+
assert isinstance(source, types.FunctionType)
324+
name = source.__name__
325+
source, _ = inspect.getsourcelines(source)
326+
327+
# First, find the "def" line.
328+
def_lineno = 0
329+
for line in source:
330+
line = line.strip()
331+
if line.startswith("def") and line.endswith(":"):
332+
break
333+
def_lineno += 1
334+
else:
335+
raise ValueError("Failed to locate function header.")
336+
337+
# Remove everything up to and including "def".
338+
source = source[def_lineno + 1 :]
339+
assert source
340+
341+
# Now we need to adjust indentation. Compute how much the first line of
342+
# the body is indented by, then dedent all lines by that amount. Blank
343+
# lines don't matter indentation-wise, and might not be indented to begin
344+
# with, so just replace them with a simple newline.
345+
line = source[0]
346+
indent = len(line) - len(line.lstrip())
347+
source = [l[indent:] if l.strip() else "\n" for l in source]
348+
source = "".join(source)
349+
350+
# Write it to file.
351+
tmpfile = os.path.join(str(tmpdir), name + ".py")
352+
assert not os.path.exists(tmpfile), '%s already exists.' % (tmpfile,)
353+
with open(tmpfile, 'w') as stream:
354+
stream.write(source)
355+
356+
return tmpfile
357+
358+
return factory
359+
360+
282361
if IS_JYTHON or IS_IRONPYTHON:
283362

284363
# On Jython and IronPython, it's a no-op.

src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2781,6 +2781,41 @@ def check_thread_events(json_facade):
27812781
writer.finished_ok = True
27822782

27832783

2784+
@pytest.mark.skipif(not TEST_GEVENT, reason='Gevent not installed.')
2785+
def test_notify_gevent(case_setup, pyfile):
2786+
2787+
def get_environ(writer):
2788+
# I.e.: Make sure that gevent support is disabled
2789+
env = os.environ.copy()
2790+
env['GEVENT_SUPPORT'] = ''
2791+
return env
2792+
2793+
@pyfile
2794+
def case_gevent():
2795+
from gevent import monkey
2796+
monkey.patch_all()
2797+
print('TEST SUCEEDED')
2798+
2799+
def additional_output_checks(writer, stdout, stderr):
2800+
assert 'environment variable' in stderr
2801+
assert 'GEVENT_SUPPORT=True' in stderr
2802+
2803+
with case_setup.test_file(
2804+
case_gevent,
2805+
get_environ=get_environ,
2806+
additional_output_checks=additional_output_checks,
2807+
EXPECTED_RETURNCODE='any',
2808+
FORCE_KILL_PROCESS_WHEN_FINISHED_OK=True
2809+
) as writer:
2810+
json_facade = JsonFacade(writer)
2811+
json_facade.write_launch()
2812+
json_facade.write_make_initial_run()
2813+
2814+
wait_for_condition(lambda: 'GEVENT_SUPPORT=True' in writer.get_stderr())
2815+
2816+
writer.finished_ok = True
2817+
2818+
27842819
@pytest.mark.skipif(IS_JYTHON, reason='Flaky on Jython.')
27852820
def test_path_translation_and_source_reference(case_setup):
27862821

src/debugpy/_vendored/pydevd/tests_python/test_utilities.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from _pydevd_bundle.pydevd_comm import pydevd_find_thread_by_id
44
from _pydevd_bundle.pydevd_utils import convert_dap_log_message_to_expression
5-
from tests_python.debug_constants import IS_PY26, IS_PY3K
5+
from tests_python.debug_constants import IS_PY26, IS_PY3K, TEST_GEVENT
66
import sys
77
from _pydevd_bundle.pydevd_constants import IS_CPYTHON, IS_WINDOWS
88
import pytest
@@ -277,9 +277,10 @@ def _build_launch_env():
277277
return cwd, environ
278278

279279

280-
def _check_in_separate_process(method_name, module_name='test_utilities'):
280+
def _check_in_separate_process(method_name, module_name='test_utilities', update_env={}):
281281
import subprocess
282282
cwd, environ = _build_launch_env()
283+
environ.update(update_env)
283284

284285
subprocess.check_call(
285286
[sys.executable, '-c', 'import %(module_name)s;%(module_name)s.%(method_name)s()' % dict(
@@ -336,3 +337,27 @@ def test_get_ppid():
336337
else:
337338
assert api._get_windows_ppid() is not None
338339

340+
341+
def _check_gevent(expect_msg):
342+
from _pydevd_bundle.pydevd_utils import notify_about_gevent_if_needed
343+
assert not notify_about_gevent_if_needed()
344+
import gevent
345+
assert not notify_about_gevent_if_needed()
346+
import gevent.monkey
347+
assert not notify_about_gevent_if_needed()
348+
gevent.monkey.patch_all()
349+
assert notify_about_gevent_if_needed() == expect_msg
350+
351+
352+
def check_notify_on_gevent_loaded():
353+
_check_gevent(True)
354+
355+
356+
def check_dont_notify_on_gevent_loaded():
357+
_check_gevent(False)
358+
359+
360+
@pytest.mark.skipif(not TEST_GEVENT, reason='Gevent not installed.')
361+
def test_gevent_notify():
362+
_check_in_separate_process('check_notify_on_gevent_loaded', update_env={'GEVENT_SUPPORT': ''})
363+
_check_in_separate_process('check_dont_notify_on_gevent_loaded', update_env={'GEVENT_SUPPORT': 'True'})

0 commit comments

Comments
 (0)