Skip to content

Commit

Permalink
Improve dealing with blocking evaluate requests. Fixes microsoft#157
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Jul 17, 2020
1 parent 4069c67 commit a403e5a
Show file tree
Hide file tree
Showing 27 changed files with 4,825 additions and 4,657 deletions.
44 changes: 4 additions & 40 deletions src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_console_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import traceback
from _pydev_bundle.pydev_imports import xmlrpclib, _queue, Exec
from _pydev_bundle._pydev_calltip_util import get_description
from _pydev_imps._pydev_saved_modules import thread
from _pydevd_bundle import pydevd_vars
from _pydevd_bundle import pydevd_xml
from _pydevd_bundle.pydevd_constants import (IS_JYTHON, dict_iter_items, NEXT_VALUE_SEPARATOR, Null,
get_global_debugger)
import signal
from _pydevd_bundle.pydevd_constants import (IS_JYTHON, dict_iter_items, NEXT_VALUE_SEPARATOR, get_global_debugger)
from contextlib import contextmanager
from _pydev_bundle import pydev_log
from _pydevd_bundle.pydevd_utils import interrupt_main_thread

try:
import cStringIO as StringIO # may not always be available @UnusedImport
Expand Down Expand Up @@ -402,43 +400,9 @@ def interrupt(self):
self.buffer = None # Also clear the buffer when it's interrupted.
try:
if self.interruptable:
called = False
try:
# Fix for #PyDev-500: Console interrupt can't interrupt on sleep
if os.name == 'posix':
# On Linux we can't interrupt 0 as in Windows because it's
# actually owned by a process -- on the good side, signals
# work much better on Linux!
os.kill(os.getpid(), signal.SIGINT)
called = True

elif os.name == 'nt':
# Stupid windows: sending a Ctrl+C to a process given its pid
# is absurdly difficult.
# There are utilities to make it work such as
# http://www.latenighthacking.com/projects/2003/sendSignal/
# but fortunately for us, it seems Python does allow a CTRL_C_EVENT
# for the current process in Windows if pid 0 is passed... if we needed
# to send a signal to another process the approach would be
# much more difficult.
# Still, note that CTRL_C_EVENT is only Python 2.7 onwards...
# Also, this doesn't seem to be documented anywhere!? (stumbled
# upon it by chance after digging quite a lot).
os.kill(0, signal.CTRL_C_EVENT)
called = True
except:
# Many things to go wrong (from CTRL_C_EVENT not being there
# to failing import signal)... if that's the case, ask for
# forgiveness and go on to the approach which will interrupt
# the main thread (but it'll only work when it's executing some Python
# code -- not on sleep() for instance).
pass
# Fix for #PyDev-500: Console interrupt can't interrupt on sleep
interrupt_main_thread(self.mainThread)

if not called:
if hasattr(thread, 'interrupt_main'): # Jython doesn't have it
thread.interrupt_main()
else:
self.mainThread._thread.interrupt() # Jython
self.finish_exec(False)
return True
except:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import sys
from _pydevd_bundle.pydevd_constants import (STATE_RUN, PYTHON_SUSPEND, IS_JYTHON,
USE_CUSTOM_SYS_CURRENT_FRAMES, USE_CUSTOM_SYS_CURRENT_FRAMES_MAP, SUPPORT_GEVENT, ForkSafeLock)
from _pydevd_bundle.pydevd_constants import (STATE_RUN, PYTHON_SUSPEND, SUPPORT_GEVENT, ForkSafeLock,
_current_frames)
from _pydev_bundle import pydev_log
# IFDEF CYTHON
# pydev_log.debug("Using Cython speedups")
Expand All @@ -10,49 +9,6 @@

version = 11

if USE_CUSTOM_SYS_CURRENT_FRAMES:

# Some versions of Jython don't have it (but we can provide a replacement)
if IS_JYTHON:
from java.lang import NoSuchFieldException
from org.python.core import ThreadStateMapping
try:
cachedThreadState = ThreadStateMapping.getDeclaredField('globalThreadStates') # Dev version
except NoSuchFieldException:
cachedThreadState = ThreadStateMapping.getDeclaredField('cachedThreadState') # Release Jython 2.7.0
cachedThreadState.accessible = True
thread_states = cachedThreadState.get(ThreadStateMapping)

def _current_frames():
as_array = thread_states.entrySet().toArray()
ret = {}
for thread_to_state in as_array:
thread = thread_to_state.getKey()
if thread is None:
continue
thread_state = thread_to_state.getValue()
if thread_state is None:
continue

frame = thread_state.frame
if frame is None:
continue

ret[thread.getId()] = frame
return ret

elif USE_CUSTOM_SYS_CURRENT_FRAMES_MAP:
_tid_to_last_frame = {}

# IronPython doesn't have it. Let's use our workaround...
def _current_frames():
return _tid_to_last_frame

else:
raise RuntimeError('Unable to proceed (sys._current_frames not available in this Python implementation).')
else:
_current_frames = sys._current_frames


#=======================================================================================================================
# PyDBAdditionalThreadInfo
Expand Down
22 changes: 5 additions & 17 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
from _pydevd_bundle import pydevd_utils, pydevd_source_mapping
from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info
from _pydevd_bundle.pydevd_comm import (InternalGetThreadStack, internal_get_completions,
pydevd_find_thread_by_id, InternalSetNextStatementThread, internal_reload_code,
InternalSetNextStatementThread, internal_reload_code,
InternalGetVariable, InternalGetArray, InternalLoadFullValue,
internal_get_description, internal_get_frame, internal_evaluate_expression, InternalConsoleExec,
internal_get_variable_json, internal_change_variable, internal_change_variable_json,
internal_evaluate_expression_json, internal_set_expression_json, internal_get_exception_details_json,
internal_step_in_thread, internal_run_thread, run_as_pydevd_daemon_thread)
internal_step_in_thread)
from _pydevd_bundle.pydevd_comm_constants import (CMD_THREAD_SUSPEND, file_system_encoding,
CMD_STEP_INTO_MY_CODE, CMD_STOP_ON_START)
from _pydevd_bundle.pydevd_constants import (get_current_thread_id, set_protocol, get_protocol,
Expand All @@ -29,6 +29,8 @@
import itertools
import linecache
from _pydevd_bundle.pydevd_utils import DAPGrouper
from _pydevd_bundle.pydevd_daemon_thread import run_as_pydevd_daemon_thread
from _pydevd_bundle.pydevd_thread_lifecycle import pydevd_find_thread_by_id, resume_threads

try:
import dis
Expand Down Expand Up @@ -187,21 +189,7 @@ def request_disconnect(self, py_db, resume_threads):
self.request_resume_thread(thread_id='*')

def request_resume_thread(self, thread_id):
threads = []
if thread_id == '*':
threads = pydevd_utils.get_non_pydevd_threads()

elif thread_id.startswith('__frame__:'):
sys.stderr.write("Can't make tasklet run: %s\n" % (thread_id,))

else:
threads = [pydevd_find_thread_by_id(thread_id)]

for t in threads:
if t is None:
continue

internal_run_thread(t, set_additional_thread_info=set_additional_thread_info)
resume_threads(thread_id)

def request_completions(self, py_db, seq, thread_id, frame_id, act_tok, line=-1, column=-1):
py_db.post_method_as_internal_command(
Expand Down
121 changes: 12 additions & 109 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@
from _pydevd_bundle.pydevd_net_command import NetCommand
from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate
from _pydevd_bundle.pydevd_constants import ForkSafeLock, NULL
from _pydevd_bundle.pydevd_daemon_thread import PyDBDaemonThread
from _pydevd_bundle.pydevd_thread_lifecycle import pydevd_find_thread_by_id, resume_threads
try:
from urllib import quote_plus, unquote_plus
from urllib import quote_plus, unquote_plus # @UnresolvedImport
except:
from urllib.parse import quote_plus, unquote_plus # @Reimport @UnresolvedImport

import pydevconsole
from _pydevd_bundle import pydevd_vars, pydevd_utils, pydevd_io
import pydevd_tracing
from _pydevd_bundle import pydevd_xml
from _pydevd_bundle import pydevd_vm_type
import sys
Expand All @@ -108,7 +109,7 @@
import cStringIO as StringIO # may not always be available @UnusedImport
except:
try:
import StringIO # @Reimport
import StringIO # @Reimport @UnresolvedImport
except:
import io as StringIO

Expand All @@ -129,81 +130,6 @@
if IS_WINDOWS and not IS_JYTHON:
SO_EXCLUSIVEADDRUSE = socket_module.SO_EXCLUSIVEADDRUSE

if IS_JYTHON:
import org.python.core as JyCore # @UnresolvedImport


class PyDBDaemonThread(threading.Thread):

def __init__(self, py_db, target_and_args=None):
'''
:param target_and_args:
tuple(func, args, kwargs) if this should be a function and args to run.
-- 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)
self._target_and_args = target_and_args

@property
def py_db(self):
return self._py_db()

def run(self):
created_pydb_daemon = self.py_db.created_pydb_daemon_threads
created_pydb_daemon[self] = 1
try:
try:
if IS_JYTHON and not isinstance(threading.currentThread(), threading._MainThread):
# we shouldn't update sys.modules for the main thread, cause it leads to the second importing 'threading'
# module, and the new instance of main thread is created
ss = JyCore.PySystemState()
# Note: Py.setSystemState() affects only the current thread.
JyCore.Py.setSystemState(ss)

self._stop_trace()
self._on_run()
except:
if sys is not None and pydev_log_exception is not None:
pydev_log_exception()
finally:
del created_pydb_daemon[self]

def _on_run(self):
if self._target_and_args is not None:
target, args, kwargs = self._target_and_args
target(*args, **kwargs)
else:
raise NotImplementedError('Should be reimplemented by: %s' % self.__class__)

def do_kill_pydev_thread(self):
if not self._kill_received:
pydev_log.debug('%s received kill signal', self.getName())
self._kill_received = True

def _stop_trace(self):
if self.pydev_do_not_trace:
pydevd_tracing.SetTrace(None) # no debugging on this thread


def mark_as_pydevd_daemon_thread(thread):
thread.pydev_do_not_trace = True
thread.is_pydev_daemon_thread = True
thread.daemon = True


def run_as_pydevd_daemon_thread(py_db, func, *args, **kwargs):
'''
Runs a function as a pydevd daemon thread (without any tracing in place).
'''
t = PyDBDaemonThread(py_db, target_and_args=(func, args, kwargs))
t.name = '%s (pydevd daemon thread)' % (func.__name__,)
t.start()
return t


class ReaderThread(PyDBDaemonThread):
''' reader thread reads and dispatches commands in an infinite loop '''
Expand Down Expand Up @@ -573,6 +499,11 @@ def do_it(self, dbg):
self.args = None
self.kwargs = None

def __str__(self):
return 'InternalThreadCommands(%s, %s, %s)' % (self.method, self.args, self.kwargs)

__repr__ = __str__


class InternalThreadCommandForAnyThread(InternalThreadCommand):

Expand Down Expand Up @@ -672,14 +603,6 @@ def do_it(self, dbg):
self._cmd = None


def internal_run_thread(thread, set_additional_thread_info):
info = set_additional_thread_info(thread)
info.pydev_original_step_cmd = -1
info.pydev_step_cmd = -1
info.pydev_step_stop = None
info.pydev_state = STATE_RUN


def internal_step_in_thread(py_db, thread_id, cmd_id, set_additional_thread_info):
thread_to_step = pydevd_find_thread_by_id(thread_id)
if thread_to_step:
Expand All @@ -690,10 +613,7 @@ def internal_step_in_thread(py_db, thread_id, cmd_id, set_additional_thread_info
info.pydev_state = STATE_RUN

if py_db.stepping_resumes_all_threads:
threads = pydevd_utils.get_non_pydevd_threads()
for t in threads:
if t is not thread_to_step:
internal_run_thread(t, set_additional_thread_info)
resume_threads('*', except_thread=thread_to_step)


class InternalSetNextStatementThread(InternalThreadCommand):
Expand Down Expand Up @@ -995,7 +915,7 @@ def internal_evaluate_expression_json(py_db, request, thread_id):
if IS_PY2 and isinstance(expression, unicode):
try:
expression.encode('utf-8')
except:
except Exception:
_evaluate_response(py_db, request, '', error_message='Expression is not valid utf-8.')
raise

Expand Down Expand Up @@ -1033,7 +953,7 @@ def __create_frame():
if try_exec:
try:
pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True)
except Exception as ex:
except (Exception, KeyboardInterrupt) as ex:
err = ''.join(traceback.format_exception_only(type(ex), ex))
# Currently there is an issue in VSC where returning success=false for an
# eval request, in repl context, VSC does not show the error response in
Expand Down Expand Up @@ -1600,20 +1520,3 @@ def send_result(self, xml):
if self.frame_accessor is not None:
self.frame_accessor.ReturnFullValue(self.seq, xml.getvalue())


def pydevd_find_thread_by_id(thread_id):
try:
# there was a deadlock here when I did not remove the tracing function when thread was dead
threads = threading.enumerate()
for i in threads:
tid = get_thread_id(i)
if thread_id == tid or thread_id.endswith('|' + tid):
return i

# This can happen when a request comes for a thread which was previously removed.
pydev_log.info("Could not find thread %s.", thread_id)
pydev_log.info("Available: %s.", ([get_thread_id(t) for t in threads],))
except:
pydev_log.exception()

return None
Loading

0 comments on commit a403e5a

Please sign in to comment.