From 7d34a12644f5f46fa75be98192df7a70af21b9ca Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Wed, 5 Feb 2020 15:11:44 -0300 Subject: [PATCH] Notify user about gevent before halting debugger. Fixes https://github.com/microsoft/ptvsd/issues/2057 --- .../pydevd/.travis/install_python_deps.sh | 5 +- .../pydevd/_pydev_bundle/pydev_log.py | 17 +++- .../pydevd/_pydevd_bundle/pydevd_comm.py | 6 +- .../pydevd/_pydevd_bundle/pydevd_constants.py | 10 ++- .../pydevd/_pydevd_bundle/pydevd_utils.py | 30 ++++++- src/debugpy/_vendored/pydevd/conftest.py | 79 +++++++++++++++++++ .../pydevd/tests_python/test_debugger_json.py | 35 ++++++++ .../pydevd/tests_python/test_utilities.py | 29 ++++++- 8 files changed, 201 insertions(+), 10 deletions(-) diff --git a/src/debugpy/_vendored/pydevd/.travis/install_python_deps.sh b/src/debugpy/_vendored/pydevd/.travis/install_python_deps.sh index 3941cab4..7a81e583 100644 --- a/src/debugpy/_vendored/pydevd/.travis/install_python_deps.sh +++ b/src/debugpy/_vendored/pydevd/.travis/install_python_deps.sh @@ -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" diff --git a/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_log.py b/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_log.py index e464a5f1..81a554a0 100644 --- a/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_log.py +++ b/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_log.py @@ -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) diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index 60f4adc3..25b91f68 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -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: diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py index 8f2d74ee..f34f2116 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py @@ -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 diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py index 85e00136..ddb2738c 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py @@ -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 diff --git a/src/debugpy/_vendored/pydevd/conftest.py b/src/debugpy/_vendored/pydevd/conftest.py index f9756f37..99647c5c 100644 --- a/src/debugpy/_vendored/pydevd/conftest.py +++ b/src/debugpy/_vendored/pydevd/conftest.py @@ -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. diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index 12eaf0a6..d4969d69 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -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): diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py b/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py index 0a7b0a6e..11a3e9f0 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py @@ -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'})