From bb5da4e19fbeb5244a89c90369c9b475ed08c9bc Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Mon, 23 Sep 2019 12:04:17 -0300 Subject: [PATCH] enable_attach() no longer hangs if called from top level of an imported module on Python 2.7. Fixes #1788 --- .../pydevd/_pydevd_bundle/pydevd_comm.py | 8 +-- .../pydevd/_pydevd_bundle/pydevd_constants.py | 5 ++ src/ptvsd/_vendored/pydevd/pydevd.py | 19 ++++++- .../_debugger_case_wait_for_attach.py | 51 +------------------ .../_debugger_case_wait_for_attach_impl.py | 49 ++++++++++++++++++ .../pydevd/tests_python/test_debugger_json.py | 1 + 6 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach_impl.py diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index 0ca77e16..d23a9eb1 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -115,6 +115,9 @@ except: # CMD_XXX constants imported for backward compatibility from _pydevd_bundle.pydevd_comm_constants import * # @UnusedWildImport +if IS_WINDOWS: + from socket import SO_EXCLUSIVEADDRUSE + if IS_JYTHON: import org.python.core as JyCore # @UnresolvedImport @@ -187,10 +190,8 @@ def run_as_pydevd_daemon_thread(func, *args, **kwargs): class ReaderThread(PyDBDaemonThread): ''' reader thread reads and dispatches commands in an infinite loop ''' - def __init__(self, sock, py_db, terminate_on_socket_close=True): + def __init__(self, sock, py_db, PyDevJsonCommandProcessor, process_net_command, terminate_on_socket_close=True): assert sock is not None - from _pydevd_bundle.pydevd_process_net_command_json import PyDevJsonCommandProcessor - from _pydevd_bundle.pydevd_process_net_command import process_net_command PyDBDaemonThread.__init__(self) self._terminate_on_socket_close = terminate_on_socket_close @@ -429,7 +430,6 @@ def create_server_socket(host, port): try: server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) if IS_WINDOWS: - from socket import SO_EXCLUSIVEADDRUSE server.setsockopt(SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1) else: server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py index 10298f5d..3f79fe9d 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py @@ -18,6 +18,11 @@ except NameError: import sys # Note: the sys import must be here anyways (others depend on it) +# Preload codecs to avoid imports to them later on which can potentially halt the debugger. +import codecs as _codecs +for _codec in ["ascii", "utf8", "utf-8", "latin1", "latin-1", "idna"]: + _codecs.lookup(_codec) + class DebugInfoHolder: # we have to put it here because it can be set through the command line (so, the diff --git a/src/ptvsd/_vendored/pydevd/pydevd.py b/src/ptvsd/_vendored/pydevd/pydevd.py index 3c6b63b0..87d9dfd4 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd.py +++ b/src/ptvsd/_vendored/pydevd/pydevd.py @@ -67,6 +67,9 @@ from _pydevd_bundle.pydevd_comm import(InternalConsoleExec, start_client, start_server, InternalGetBreakpointException, InternalSendCurrExceptionTrace, InternalSendCurrExceptionTraceProceeded, run_as_pydevd_daemon_thread) +from _pydevd_bundle.pydevd_process_net_command_json import PyDevJsonCommandProcessor +from _pydevd_bundle.pydevd_process_net_command import process_net_command + from _pydevd_bundle.pydevd_breakpoints import stop_on_unhandled_exception from _pydevd_bundle.pydevd_collect_try_except_info import collect_try_except_info from _pydevd_bundle.pydevd_suspended_frames import SuspendedFramesManager @@ -1127,7 +1130,13 @@ class PyDB(object): curr_writer.do_kill_pydev_thread() self.writer = WriterThread(sock, self, terminate_on_socket_close=terminate_on_socket_close) - self.reader = ReaderThread(sock, self, terminate_on_socket_close=terminate_on_socket_close) + self.reader = ReaderThread( + sock, + self, + PyDevJsonCommandProcessor=PyDevJsonCommandProcessor, + process_net_command=process_net_command, + terminate_on_socket_close=terminate_on_socket_close + ) self.writer.start() self.reader.start() @@ -2568,7 +2577,13 @@ class DispatchReader(ReaderThread): def __init__(self, dispatcher): self.dispatcher = dispatcher - ReaderThread.__init__(self, self.dispatcher.client) + + ReaderThread.__init__( + self, + self.dispatcher.client, + PyDevJsonCommandProcessor=PyDevJsonCommandProcessor, + process_net_command=process_net_command, + ) @overrides(ReaderThread._on_run) def _on_run(self): diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach.py index c09922d3..75f22522 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach.py @@ -1,50 +1,3 @@ if __name__ == '__main__': - import os - import sys - import time - port = int(sys.argv[1]) - root_dirname = os.path.dirname(os.path.dirname(__file__)) - - if root_dirname not in sys.path: - sys.path.append(root_dirname) - - import pydevd - try: - pydevd._wait_for_attach() # Cannot be called before _enable_attach. - except AssertionError: - pass - else: - raise AssertionError('Expected _wait_for_attach to raise exception.') - - assert sys.gettrace() is None - print('enable attach to port: %s' % (port,)) - pydevd._enable_attach(('127.0.0.1', port)) - pydevd._enable_attach(('127.0.0.1', port)) # no-op in practice - - try: - pydevd._enable_attach(('127.0.0.1', port + 15)) # different port: raise error. - except AssertionError: - pass - else: - raise AssertionError('Expected _enable_attach to raise exception (because it is already hearing in another port).') - - assert pydevd.get_global_debugger() is not None - assert sys.gettrace() is not None - - a = 10 # Break 1 - print('wait for attach') - pydevd._wait_for_attach() - print('finished wait for attach') - pydevd._wait_for_attach() # Should promptly return (already connected). - - a = 20 # Break 2 - - pydevd._wait_for_attach() # As we disconnected on the 2nd break, this one should wait until a new configurationDone. - - a = 20 # Break 3 - - while a == 20: # Pause 1 - # The debugger should disconnect/reconnect, pause and then change 'a' to another value. - time.sleep(1 / 20.) # Pause 2 - - print('TEST SUCEEDED!') + # We want to call _enable_attach inside an import to make sure that it works properly that way. + import _debugger_case_wait_for_attach_impl diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach_impl.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach_impl.py new file mode 100644 index 00000000..61e0ebd7 --- /dev/null +++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_wait_for_attach_impl.py @@ -0,0 +1,49 @@ +import os +import sys +import time +port = int(sys.argv[1]) +root_dirname = os.path.dirname(os.path.dirname(__file__)) + +if root_dirname not in sys.path: + sys.path.append(root_dirname) + +import pydevd +try: + pydevd._wait_for_attach() # Cannot be called before _enable_attach. +except AssertionError: + pass +else: + raise AssertionError('Expected _wait_for_attach to raise exception.') + +assert sys.gettrace() is None +print('enable attach to port: %s' % (port,)) +pydevd._enable_attach(('127.0.0.1', port)) +pydevd._enable_attach(('127.0.0.1', port)) # no-op in practice + +try: + pydevd._enable_attach(('127.0.0.1', port + 15)) # different port: raise error. +except AssertionError: + pass +else: + raise AssertionError('Expected _enable_attach to raise exception (because it is already hearing in another port).') + +assert pydevd.get_global_debugger() is not None +assert sys.gettrace() is not None + +a = 10 # Break 1 +print('wait for attach') +pydevd._wait_for_attach() +print('finished wait for attach') +pydevd._wait_for_attach() # Should promptly return (already connected). + +a = 20 # Break 2 + +pydevd._wait_for_attach() # As we disconnected on the 2nd break, this one should wait until a new configurationDone. + +a = 20 # Break 3 + +while a == 20: # Pause 1 + # The debugger should disconnect/reconnect, pause and then change 'a' to another value. + time.sleep(1 / 20.) # Pause 2 + +print('TEST SUCEEDED!') diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py index ad6dc263..141f2cec 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py @@ -2426,6 +2426,7 @@ def test_wait_for_attach(case_setup_remote_attach_to): assert next(iter(process_events)).body.startMethod == start_method with case_setup_remote_attach_to.test_file('_debugger_case_wait_for_attach.py', host_port[1]) as writer: + writer.TEST_FILE = debugger_unittest._get_debugger_test_file('_debugger_case_wait_for_attach_impl.py') time.sleep(.5) # Give some time for it to pass the first breakpoint and wait in 'wait_for_attach'. writer.start_socket_client(*host_port)