Skip to content

Commit

Permalink
Provide a way to suspend and resume all threads. Fixes microsoft#732
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Aug 28, 2018
1 parent f453ed1 commit 43b0d9c
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 40 deletions.
2 changes: 1 addition & 1 deletion ptvsd/_vendored/pydevd/.travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ matrix:
env:
- PYDEVD_USE_CYTHON=NO
- PYDEVD_TEST_JYTHON=YES
- JYTHON_URL=http://search.maven.org/remotecontent?filepath=org/python/jython-installer/2.7.0/jython-installer-2.7.0.jar
- JYTHON_URL=http://search.maven.org/remotecontent?filepath=org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar

# Python 2.6 (with and without cython)
# - python: 2.7
Expand Down
2 changes: 1 addition & 1 deletion ptvsd/_vendored/pydevd/.travis_install_jython_deps.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash
set -ev

pip install pytest==3.6
pip install pytest
pip install untangle
54 changes: 35 additions & 19 deletions ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from _pydevd_bundle.pydevd_constants import get_thread_id, IS_PY3K, DebugInfoHolder, dict_keys, STATE_RUN, \
NEXT_VALUE_SEPARATOR, IS_WINDOWS
from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info
from _pydev_imps._pydev_saved_modules import threading

def process_net_command(py_db, cmd_id, seq, text):
'''Processes a command received from the Java side
Expand Down Expand Up @@ -115,31 +116,46 @@ def process_net_command(py_db, cmd_id, seq, text):
py_db.post_internal_command(int_cmd, text)

elif cmd_id == CMD_THREAD_SUSPEND:
# Yes, thread suspend is still done at this point, not through an internal command!
t = pydevd_find_thread_by_id(text)
if t and not getattr(t, 'pydev_do_not_trace', None):
additional_info = set_additional_thread_info(t)
frame = additional_info.get_topmost_frame(t)
if frame is not None:
try:
py_db.set_trace_for_frame_and_parents(frame, overwrite_prev_trace=True)
finally:
frame = None

py_db.set_suspend(t, CMD_THREAD_SUSPEND)
# Yes, thread suspend is done at this point, not through an internal command.
threads = []
if text.strip() == '*':
threads = threading.enumerate()

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

else:
threads = [pydevd_find_thread_by_id(text)]

for t in threads:
if t and not getattr(t, 'pydev_do_not_trace', None):
additional_info = set_additional_thread_info(t)
frame = additional_info.get_topmost_frame(t)
if frame is not None:
try:
py_db.set_trace_for_frame_and_parents(frame, overwrite_prev_trace=True)
finally:
frame = None

py_db.set_suspend(t, CMD_THREAD_SUSPEND)

elif cmd_id == CMD_THREAD_RUN:
t = pydevd_find_thread_by_id(text)
if t:
t.additional_info.pydev_step_cmd = -1
t.additional_info.pydev_step_stop = None
t.additional_info.pydev_state = STATE_RUN

threads = []
if text.strip() == '*':
threads = threading.enumerate()

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


else:
threads = [pydevd_find_thread_by_id(text)]

for t in threads:
if t and not getattr(t, 'pydev_do_not_trace', None):
additional_info = set_additional_thread_info(t)
additional_info.pydev_step_cmd = -1
additional_info.pydev_step_stop = None
additional_info.pydev_state = STATE_RUN

elif cmd_id == CMD_STEP_INTO or cmd_id == CMD_STEP_OVER or cmd_id == CMD_STEP_RETURN or \
cmd_id == CMD_STEP_INTO_MY_CODE:
Expand Down
4 changes: 2 additions & 2 deletions ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ def test_imports5(self):
assert 'byte[]' in tup[1]

f, tip = _pydev_jy_imports_tipper.generate_tip('__builtin__.str')
assert f.endswith('jython.jar')
assert f is None or f.endswith('jython.jar') # Depends on jython version
self.assert_in('find' , tip)

f, tip = _pydev_jy_imports_tipper.generate_tip('__builtin__.dict')
assert f.endswith('jython.jar')
assert f is None or f.endswith('jython.jar') # Depends on jython version
self.assert_in('get' , tip)


Expand Down
9 changes: 5 additions & 4 deletions ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ def wrapper(func):
TIMEOUT = 20


def wait_for_condition(condition, msg=None):
def wait_for_condition(condition, msg=None, timeout=TIMEOUT, sleep=.05):
curtime = time.time()
while True:
if condition():
break
if time.time() - curtime > TIMEOUT:
if time.time() - curtime > timeout:
error_msg = 'Condition not reached in %s seconds'
if msg is not None:
error_msg += '\n'
Expand All @@ -151,7 +151,7 @@ def wait_for_condition(condition, msg=None):
error_msg += str(msg)

raise AssertionError(error_msg)
time.sleep(.05)
time.sleep(sleep)


#=======================================================================================================================
Expand Down Expand Up @@ -477,6 +477,7 @@ def _ignore_stderr_line(self, line):
'An event executor terminated with non-empty task',
'java.lang.UnsupportedOperationException',
"RuntimeWarning: Parent module '_pydevd_bundle' not found while handling absolute import",
'from _pydevd_bundle.pydevd_additional_thread_info_regular import _current_frames',
):
if expected in line:
return True
Expand Down Expand Up @@ -697,7 +698,7 @@ def wait_for_multiple_vars(self, expected_vars):
if v not in all_found:
missing.append(v)
raise ValueError('Not Found:\n%s\nNot found messages: %s\nFound messages: %s\nExpected messages: %s\nIgnored messages:\n%s' % (
'\n'.join(missing), len(missing), len(all_found), len(expected_vars), '\n'.join(ignored)))
'\n'.join(str(x) for x in missing), len(missing), len(all_found), len(expected_vars), '\n'.join(str(x) for x in ignored)))

was_message_used = False
new_expected = []
Expand Down
24 changes: 19 additions & 5 deletions ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case4.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import time


class ProceedContainer:
proceed = False


def exit_while_loop():
ProceedContainer.proceed = True
return 'ok'


def sleep():
while not ProceedContainer.proceed: # The debugger should change the proceed to True to exit the loop.
time.sleep(.1)


if __name__ == '__main__':
for i in range(6):
print('here %s' % i)
time.sleep(1)

sleep()

print('TEST SUCEEDED')

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import time
import threading


class ProceedContainer:
tid_to_proceed = {
1: False,
2: False,
}


def exit_while_loop(tid):
ProceedContainer.tid_to_proceed[tid] = True
return 'ok'


def thread_func(tid):
while not ProceedContainer.tid_to_proceed[tid]: # The debugger should change the proceed to True to exit the loop.
time.sleep(.1)


if __name__ == '__main__':
threads = [
threading.Thread(target=thread_func, args=(1,)),
threading.Thread(target=thread_func, args=(2,)),
]
for t in threads:
t.start()

for t in threads:
t.join()

print('TEST SUCEEDED')
89 changes: 81 additions & 8 deletions ptvsd/_vendored/pydevd/tests_python/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from tests_python.debugger_unittest import (CMD_SET_PROPERTY_TRACE, REASON_CAUGHT_EXCEPTION,
REASON_UNCAUGHT_EXCEPTION, REASON_STOP_ON_BREAKPOINT, REASON_THREAD_SUSPEND, overrides, CMD_THREAD_CREATE,
CMD_GET_THREAD_STACK, REASON_STEP_INTO_MY_CODE, CMD_GET_EXCEPTION_DETAILS, IS_IRONPYTHON, IS_JYTHON, IS_CPYTHON,
IS_APPVEYOR)
IS_APPVEYOR, wait_for_condition)
from _pydevd_bundle.pydevd_constants import IS_WINDOWS
try:
from urllib import unquote
Expand Down Expand Up @@ -157,24 +157,93 @@ def test_case_3(case_setup):
writer.finished_ok = True


@pytest.mark.skipif(IS_JYTHON, reason='This test is flaky on Jython, so, skipping it.')
def test_case_4(case_setup):
def test_case_suspend_thread(case_setup):
with case_setup.test_file('_debugger_case4.py') as writer:
writer.FORCE_KILL_PROCESS_WHEN_FINISHED_OK = True
writer.write_make_initial_run()

thread_id = writer.wait_for_new_thread()

writer.write_suspend_thread(thread_id)

hit = writer.wait_for_breakpoint_hit(REASON_THREAD_SUSPEND)
while True:
hit = writer.wait_for_breakpoint_hit(REASON_THREAD_SUSPEND)
if hit.name == 'sleep':
break # Ok, broke on 'sleep'.
else:
# i.e.: if it doesn't hit on 'sleep', release and pause again.
writer.write_run_thread(thread_id)
time.sleep(.1)
writer.write_suspend_thread(thread_id)

assert hit.thread_id == thread_id

writer.write_evaluate_expression('%s\t%s\t%s' % (hit.thread_id, hit.frame_id, 'LOCAL'), 'exit_while_loop()')
writer.wait_for_evaluation([
[
'<var name="exit_while_loop()" type="str" qualifier="{0}" value="str: ok'.format(builtin_qualifier),
'<var name="exit_while_loop()" type="str" value="str: ok"', # jython
]
])

writer.write_run_thread(thread_id)

writer.finished_ok = True


# Jython has a weird behavior: it seems it has fine-grained locking so that when
# we're inside the tracing other threads don't run (so, we can have only one
# thread paused in the debugger).
@pytest.mark.skipif(IS_JYTHON, reason='Jython can only have one thread stopped at each time.')
def test_case_suspend_all_thread(case_setup):
with case_setup.test_file('_debugger_case_suspend_all.py') as writer:
writer.write_make_initial_run()

main_thread_id = writer.wait_for_new_thread() # Main thread
thread_id1 = writer.wait_for_new_thread() # Thread 1
thread_id2 = writer.wait_for_new_thread() # Thread 2

# Ok, all threads created, let's wait for the main thread to get to the join.
def get_frame_names():
writer.write_get_thread_stack(main_thread_id)
msg = writer.wait_for_message(lambda msg:msg.startswith('%s\t' % (CMD_GET_THREAD_STACK,)))
if msg.thread.frame:
frame_names = [frame['name'] for frame in msg.thread.frame]
return frame_names
return [msg.thread.frame['name']]

def condition():
return get_frame_names() in (
['wait', 'join', '<module>'],
['_wait_for_tstate_lock', 'join', '<module>']
)

def msg():
return 'Found stack: %s' % (get_frame_names(),)

wait_for_condition(condition, msg, timeout=5, sleep=.5)

writer.write_suspend_thread('*')

# Wait for 2 threads to be suspended (the main thread is already in a join, so, it can't actually
# break out of it while others don't proceed).
hit0 = writer.wait_for_breakpoint_hit(REASON_THREAD_SUSPEND)
hit1 = writer.wait_for_breakpoint_hit(REASON_THREAD_SUSPEND)

writer.write_evaluate_expression('%s\t%s\t%s' % (hit0.thread_id, hit0.frame_id, 'LOCAL'), 'exit_while_loop(1)')
writer.wait_for_evaluation([
[
'<var name="exit_while_loop(1)" type="str" qualifier="{0}" value="str: ok'.format(builtin_qualifier)
]
])

writer.write_evaluate_expression('%s\t%s\t%s' % (hit1.thread_id, hit1.frame_id, 'LOCAL'), 'exit_while_loop(2)')
writer.wait_for_evaluation('<var name="exit_while_loop(2)" type="str" qualifier="{0}" value="str: ok'.format(builtin_qualifier))

writer.write_run_thread('*')

writer.finished_ok = True


def test_case_5(case_setup):
with case_setup.test_file('_debugger_case56.py') as writer:
breakpoint_id = writer.write_add_breakpoint(2, 'Call2')
Expand Down Expand Up @@ -1777,6 +1846,7 @@ def _get_breakpoint_cases():
# Check breakpoint() and sys.__breakpointhook__ replacement.
return ('_debugger_case_breakpoint.py', '_debugger_case_breakpoint2.py')


@pytest.mark.parametrize("filename", _get_breakpoint_cases())
def test_py_37_breakpoint(case_setup, filename):
with case_setup.test_file(filename) as writer:
Expand Down Expand Up @@ -1887,15 +1957,15 @@ def test_remote_debugger_basic(case_setup_remote):
writer.log.append('asserted')

writer.finished_ok = True


@pytest.mark.skipif(not IS_CPYTHON, reason='CPython only test.')
def test_py_37_breakpoint_remote(case_setup_remote):
with case_setup_remote.test_file('_debugger_case_breakpoint_remote.py') as writer:
writer.write_make_initial_run()

hit = writer.wait_for_breakpoint_hit(
REASON_THREAD_SUSPEND,
REASON_THREAD_SUSPEND,
filename='_debugger_case_breakpoint_remote.py',
line=13,
)
Expand All @@ -1911,8 +1981,10 @@ def test_py_37_breakpoint_remote(case_setup_remote):

writer.finished_ok = True


@pytest.mark.skipif(not IS_CPYTHON, reason='CPython only test.')
def test_py_37_breakpoint_remote_no_import(case_setup_remote):

def get_environ(writer):
env = os.environ.copy()
curr_pythonpath = env.get('PYTHONPATH', '')
Expand All @@ -1931,7 +2003,7 @@ def get_environ(writer):
writer.write_make_initial_run()

hit = writer.wait_for_breakpoint_hit(
"108",
"108",
filename='_debugger_case_breakpoint_remote_no_import.py',
line=12,
)
Expand All @@ -1947,6 +2019,7 @@ def get_environ(writer):

writer.finished_ok = True


@pytest.mark.skipif(not IS_CPYTHON, reason='CPython only test.')
def test_remote_debugger_multi_proc(case_setup_remote):

Expand Down

0 comments on commit 43b0d9c

Please sign in to comment.