Repository: microsoft/ptvsd

Issue:
steppingResumesAllThreads with true results in incorrect thread state in the UI
## Environment data

- PTVSD version: master
- OS and version: Windows
- Python version (& distribution if applicable, e.g. Anaconda): 3.7
- Using VS Code or Visual Studio: VS Code

## Actual behavior

![image](https://user-images.githubusercontent.com/3840081/59788134-ef038d00-927f-11e9-9289-74190c2e3e6b.png)


## Expected  behavior

Show show all threads as running in that case.

## Steps to reproduce:
1. Set break point before `t.join()` and step over that line.
```py
import threading
import time

class RunThread(threading.Thread):
    kind = 'Thread'

    def __init__(self, name, fn, args=()):
        threading.Thread.__init__(self)
        self.name = name
        self.fn   = fn
        self.args = args
        self.start()

    def run(self):
        self.fn(*(self.args))

def th1():
    while True:
        print('th1')
        time.sleep(1)

def th2():
    while True:
        print('th2')
        time.sleep(2)

def th3():
    while True:
        print('th3')
        time.sleep(4)

def main():
    threads = []
    threads.append(RunThread("aaa", th1))
    threads.append(RunThread("bbb", th2))
    threads.append(RunThread("ccc", th3))

    for t in threads:
        t.join()


if __name__ == '__main__':
    main()

```




Discussion:
We may need to send the `continue event` in this case, since the threads are continued (may be not as a direct result of user request)? Since `stepping` is specific to a thread, any other thread that is continued should report via the `continued` event?

@fabioz @int19h what do you think? 
We actually disable individual hooks in:
(`NetCommandFactoryJson.make_thread_run_message` and `NetCommandFactoryJson.make_thread_suspend_message`) and we have the single one sent before/after all threads are resumed in `AbstractSingleNotificationBehavior.notify_thread_suspended`.

The fix for this would be stop sending the notification from `AbstractSingleNotificationBehavior.notify_thread_suspended` (just stop sending for the `continue`) and enable sending the notification on `NetCommandFactoryJson.make_thread_run_message`.

Still, it seems a bit weird that we send a single suspend notification and multiple continue notifications no? 

Is it the case that `vscode` would like to have notifications sent for each thread while `vs` would like to have a single for all? Do you think we need another flag for this?
> Still, it seems a bit weird that we send a single suspend notification and multiple continue notifications no?

It is weird. VS is the one that needs single suspended event. VSC is more lenient about this. Multiple continued events will definitely break it.

> Is it the case that vscode would like to have notifications sent for each thread while vs would like to have a single for all? Do you think we need another flag for this?

I think the clientid should be enough to manage this behavior. so would not need any additional flags. Also, in this particular case VS will not need this continued event at all. VS  UI is perfectly happy with the current state reported by the debugger. Since it always assumes all threads should be running while stepping. So no need to make changes for VS.
VSC on the other hand has UI issues. So we should probably send `continued` events for all threads except the one that is stepping, or alternatively we could also try sending a single `continued` event with `allThreadsContinued=true`. We may have to try these out as see if we get the right behavior.
Actually, it seems that `pydevd` was already sending the proper continued with `allThreadsContinued` when all the threads were resumed but the wrapper chose not to send it unless an environment variable named `PTVSD_USE_CONTINUED` was set (done in 66deeb1 - Turn off sending continued events by default. (#1360)):  (and it also wasn't forwarding the `allThreadsContinued`).

It seems this was done to fix https://github.com/microsoft/ptvsd/issues/1361 (although I think that the proper fix would be just forwarding the `allThreadsContinued`) and https://github.com/microsoft/ptvsd/issues/1358 (but we can still not send it for visual studio).

So, the fix I'm thinking about is doing:

```
    @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_RESUME_SINGLE_NOTIFICATION)
    def on_pydevd_thread_resume_single_notification(self, seq, args):
        body = args['body']
        if self._client_id != 'visual_studio':
            self.send_event('continued', **body)
```

What do you think? As a note, I don't know what's the visual studio client id (can you provide it?)
Actually, I just found the client id in https://microsoft.github.io/debug-adapter-protocol/implementors/tools/

Resolving patch:
diff --git a/src/ptvsd/wrapper.py b/src/ptvsd/wrapper.py
--- a/src/ptvsd/wrapper.py
+++ b/src/ptvsd/wrapper.py
@@ -665,6 +665,7 @@ def __init__(
         # https://github.com/Microsoft/VSDebugAdapterHost/wiki/Differences-between-Visual-Studio-Code-and-the-Visual-Studio-Debug-Adapter-Host # noqa
         # VS expects a single stopped event in a multi-threaded scenario.
         self._client_id = None
+        self._initialize_received = False
 
         # adapter state
         self.start_reason = None
@@ -730,6 +731,7 @@ def handle_exiting(self, exitcode=None, wait=None):
     def on_initialize(self, request, args):
         self._client_id = args.get('clientID', None)
         self._restart_debugger = False
+        self._initialize_received = True
         self.send_response(request, **INITIALIZE_RESPONSE)
         self.send_event('initialized')
 
@@ -1312,10 +1314,17 @@ def on_pydevd_thread_suspend_single_notification(self, seq, args):
 
     @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_RESUME_SINGLE_NOTIFICATION)
     def on_pydevd_thread_resume_single_notification(self, seq, args):
-        tid = args['body']['threadId']
-
-        if os.getenv('PTVSD_USE_CONTINUED'):
-            self.send_event('continued', threadId=tid)
+        if not self._initialize_received:
+            return  # This may happen when we disconnect and later reconnect too fast.
+        body = args['body']
+        if self._client_id not in ('visualstudio', 'vsformac'):
+            # In visual studio any step/continue action already marks all the
+            # threads as running until a suspend, so, the continued is not
+            # needed (and can in fact break the UI in some cases -- see:
+            # https://github.com/microsoft/ptvsd/issues/1358).
+            # It is however needed in vscode -- see:
+            # https://github.com/microsoft/ptvsd/issues/1530.
+            self.send_event('continued', **body)
 
     @pydevd_events.handler(pydevd_comm.CMD_WRITE_TO_CONSOLE)
     def on_pydevd_cmd_write_to_console2(self, seq, args):


Test patch:
diff --git a/tests/func/test_step.py b/tests/func/test_step.py
--- a/tests/func/test_step.py
+++ b/tests/func/test_step.py
@@ -34,7 +34,7 @@ def func():
             target=(run_as, code_to_debug),
             start_method=start_method,
             ignore_unobserved=[Event('continued')],
-            env={'PTVSD_USE_CONTINUED': '1'},
+            client_id='vscode',
         )
         session.set_breakpoints(code_to_debug, [line_numbers['inner1']])
         session.start_debugging()
diff --git a/tests/func/test_tracing.py b/tests/func/test_tracing.py
--- a/tests/func/test_tracing.py
+++ b/tests/func/test_tracing.py
@@ -11,6 +11,7 @@
 
 
 def test_tracing(pyfile, start_method, run_as):
+
     @pyfile
     def code_to_debug():
         from dbgimporter import import_and_enable_debugger
@@ -61,7 +62,7 @@ def inner2():
             target=(run_as, code_to_debug),
             start_method=start_method,
             ignore_unobserved=[Event('continued')],
-            env={'PTVSD_USE_CONTINUED': '1'},
+            client_id='vscode',
         )
 
         session.set_breakpoints(code_to_debug, line_numbers.values())
diff --git a/tests/helpers/session.py b/tests/helpers/session.py
--- a/tests/helpers/session.py
+++ b/tests/helpers/session.py
@@ -45,6 +45,9 @@ def __init__(self, start_method='launch', ptvsd_port=None, pid=None):
 
         print('New debug session with method %r' % str(start_method))
 
+        # Note: with 'visualstudio' we don't send continued events.
+        self.client_id = 'visualstudio'
+
         self.target = ('code', 'print("OK")')
         self.start_method = start_method
         self.start_method_args = {}
@@ -271,6 +274,8 @@ def initialize(self, **kwargs):
         print('Initializing debug session for ptvsd#%d' % self.ptvsd_port)
         dbg_argv = []
         usr_argv = []
+        if 'client_id' in kwargs:
+            self.client_id = kwargs['client_id']
         if self.start_method == 'launch':
             self._listen()
             dbg_argv += self._get_argv_for_launch()
@@ -523,7 +528,10 @@ def handshake(self):
         to finalize the configuration stage, and start running code.
         """
 
-        self.send_request('initialize', {'adapterID': 'test'}).wait_for_response()
+        self.send_request(
+            'initialize',
+            {'adapterID': 'test', 'clientID': self.client_id}
+        ).wait_for_response()
         self.wait_for_next(Event('initialized', {}))
 
         request = 'launch' if self.start_method == 'launch' else 'attach'
@@ -729,5 +737,6 @@ def connect_with_new_session(self, **kwargs):
             ns.handshake()
         except:
             ns.close()
+            raise
         else:
             return ns