Repository: microsoft/ptvsd

Issue:
Suspend/resume all user threads command in pydevd
@fabioz Would it be possible to provide suspend all and resume all commands Or just extend the existing commands if no thread id is provided then it means suspend all or resume all?



Discussion:
Yes, that should be possible. What's the priority on this issue? 
This is low priority.
Just as a note, I already have a fix for this, but it has conflicts with other pull requests I have open:

https://github.com/Microsoft/ptvsd/pull/764
https://github.com/Microsoft/ptvsd/pull/765
https://github.com/Microsoft/ptvsd/pull/767

So, when those are merged I'll provide a pull request for this one.


Resolving patch:
diff --git a/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py b/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py
--- 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:


Test patch:
diff --git a/ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py b/ptvsd/_vendored/pydevd/tests/test_jysimpleTipper.py
--- 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
--- 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
--- 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
--- /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
--- 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([
+            [
+                '<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')
@@ -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:
@@ -1887,7 +1957,7 @@ 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):
@@ -1895,7 +1965,7 @@ def test_py_37_breakpoint_remote(case_setup_remote):
         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,
         )
@@ -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', '')
@@ -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,
         )
@@ -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):