From 43b0d9c55f6093fd3a44d33ac0f145c44765b619 Mon Sep 17 00:00:00 2001 From: fabioz Date: Tue, 28 Aug 2018 15:38:11 -0300 Subject: [PATCH] Provide a way to suspend and resume all threads. Fixes #732 --- ptvsd/_vendored/pydevd/.travis.yml | 2 +- .../pydevd/.travis_install_jython_deps.sh | 2 +- .../pydevd_process_net_command.py | 54 +++++++---- .../pydevd/tests/test_jysimpleTipper.py | 4 +- .../pydevd/tests_python/debugger_unittest.py | 9 +- .../tests_python/resources/_debugger_case4.py | 24 +++-- .../resources/_debugger_case_suspend_all.py | 33 +++++++ .../pydevd/tests_python/test_debugger.py | 89 +++++++++++++++++-- 8 files changed, 177 insertions(+), 40 deletions(-) create mode 100644 ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_suspend_all.py diff --git a/ptvsd/_vendored/pydevd/.travis.yml b/ptvsd/_vendored/pydevd/.travis.yml index 1fb301926..e638279cf 100644 --- a/ptvsd/_vendored/pydevd/.travis.yml +++ b/ptvsd/_vendored/pydevd/.travis.yml @@ -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 diff --git a/ptvsd/_vendored/pydevd/.travis_install_jython_deps.sh b/ptvsd/_vendored/pydevd/.travis_install_jython_deps.sh index f5c478cfa..f3a104fb7 100644 --- a/ptvsd/_vendored/pydevd/.travis_install_jython_deps.sh +++ b/ptvsd/_vendored/pydevd/.travis_install_jython_deps.sh @@ -1,5 +1,5 @@ #!/bin/bash set -ev -pip install pytest==3.6 +pip install pytest pip install untangle \ No newline at end of file diff --git a/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py b/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py index d9dfbe2e6..0c8c46033 100644 --- a/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py +++ b/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py @@ -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 @@ -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: diff --git a/ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py b/ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py index 669a64d52..c74aa39c2 100644 --- a/ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py +++ b/ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py @@ -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) diff --git a/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py b/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py index bb5086615..608234e67 100644 --- a/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py +++ b/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py @@ -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' @@ -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) #======================================================================================================================= @@ -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 @@ -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 = [] diff --git a/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case4.py b/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case4.py index 78fc1406b..661d930fc 100644 --- a/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case4.py +++ b/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case4.py @@ -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') - + diff --git a/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_suspend_all.py b/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_suspend_all.py new file mode 100644 index 000000000..f02ce7a30 --- /dev/null +++ b/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_suspend_all.py @@ -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') diff --git a/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/ptvsd/_vendored/pydevd/tests_python/test_debugger.py index 0a142fc34..4caec3523 100644 --- a/ptvsd/_vendored/pydevd/tests_python/test_debugger.py +++ b/ptvsd/_vendored/pydevd/tests_python/test_debugger.py @@ -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 @@ -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([ + [ + ''], + ['_wait_for_tstate_lock', 'join', ''] + ) + + 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([ + [ + '