Repository: microsoft/ptvsd

Issue:
CMD_THREAD_KILL handler doesn't parse the args.
Currently the handler for the PyDevd CMD_THREAD_KILL event treats the message "args" as the thread ID, using it directly to look up the VSC thread ID.  However, under some circumstances the message "args" has a trailing linefeed.  This causes the lookup (for the VSC thread ID) to fail.  A simple fix is to use `args.strip()` instead of just args.

FYI, The handler for CMD_THREAD_RUN appears to be in a similar situation, though it doesn't cause a failure (since the trailing "reason" is never used).  CMD_THREAD_CREATE and CMD_THREAD_SUSPEND do not have any problems since they use the XML parser and it strips linefeeds.


Discussion:

Resolving patch:
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -115,7 +115,7 @@ def to_pydevd(self, vscode_id):
         # TODO: docstring
         return self._vscode_to_pydevd[vscode_id]
 
-    def to_vscode(self, pydevd_id, autogen=True):
+    def to_vscode(self, pydevd_id, autogen):
         # TODO: docstring
         try:
             return self._pydevd_to_vscode[pydevd_id]
@@ -552,26 +552,39 @@ def on_threads(self, request, args):
 
         threads = []
         for xthread in xthreads:
-            tid = self.thread_map.to_vscode(xthread['id'])
             try:
                 name = unquote(xthread['name'])
             except KeyError:
                 name = None
+            
             if not self.is_debugger_internal_thread(name):
-                threads.append({'id': tid, 'name': name})
+                pyd_tid = xthread['id']
+                try:
+                    vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
+                except KeyError:
+                    # This is a previously unseen thread
+                    vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=True)
+                    self.send_event('thread', reason='started', threadId=vsc_tid)
+
+                threads.append({'id': vsc_tid, 'name': name})
 
         self.send_response(request, threads=threads)
 
     @async_handler
     def on_stackTrace(self, request, args):
         # TODO: docstring
-        tid = int(args['threadId'])
+        vsc_tid = int(args['threadId'])
         startFrame = int(args.get('startFrame', 0))
         levels = int(args.get('levels', 0))
 
-        tid = self.thread_map.to_pydevd(tid)
+        pyd_tid = self.thread_map.to_pydevd(vsc_tid)
         with self.stack_traces_lock:
-            xframes = self.stack_traces[tid]
+            try:
+                xframes = self.stack_traces[pyd_tid]
+            except KeyError:
+                # This means the stack was requested before the 
+                # thread was suspended
+                xframes = []
         totalFrames = len(xframes)
 
         if levels == 0:
@@ -585,8 +598,8 @@ def on_stackTrace(self, request, args):
             if levels <= 0:
                 break
             levels -= 1
-            key = (tid, int(xframe['id']))
-            fid = self.frame_map.to_vscode(key)
+            key = (pyd_tid, int(xframe['id']))
+            fid = self.frame_map.to_vscode(key, autogen=True)
             name = unquote(xframe['name'])
             file = unquote(xframe['file'])
             line = int(xframe['line'])
@@ -607,7 +620,7 @@ def on_scopes(self, request, args):
         vsc_fid = int(args['frameId'])
         pyd_tid, pyd_fid = self.frame_map.to_pydevd(vsc_fid)
         pyd_var = (pyd_tid, pyd_fid, 'FRAME')
-        vsc_var = self.var_map.to_vscode(pyd_var)
+        vsc_var = self.var_map.to_vscode(pyd_var, autogen=True)
         scope = {
             'name': 'Locals',
             'expensive': False,
@@ -643,7 +656,7 @@ def on_variables(self, request, args):
             }
             if bool(xvar['isContainer']):
                 pyd_child = pyd_var + (var['name'],)
-                var['variablesReference'] = self.var_map.to_vscode(pyd_child)
+                var['variablesReference'] = self.var_map.to_vscode(pyd_child, autogen=True)
             variables.append(var)
 
         self.send_response(request, variables=variables)
@@ -657,7 +670,7 @@ def on_setVariable(self, request, args):
         # being set, and variable name; but pydevd wants the ID
         # (or rather path) of the variable itself.
         pyd_var += (args['name'],)
-        vsc_var = self.var_map.to_vscode(pyd_var)
+        vsc_var = self.var_map.to_vscode(pyd_var, autogen=True)
 
         cmd_args = [str(s) for s in pyd_var] + [args['value']]
         _, _, resp_args = yield self.pydevd_request(
@@ -691,7 +704,7 @@ def on_evaluate(self, request, args):
         xvar = xml.var
 
         pyd_var = (pyd_tid, pyd_fid, 'EXPRESSION', expr)
-        vsc_var = self.var_map.to_vscode(pyd_var)
+        vsc_var = self.var_map.to_vscode(pyd_var, autogen=True)
         response = {
             'type': unquote(xvar['type']),
             'result': unquote(xvar['value']),
@@ -810,29 +823,31 @@ def on_exceptionInfo(self, request, args):
     def on_pydevd_thread_create(self, seq, args):
         # TODO: docstring
         xml = untangle.parse(args).xml
-        tid = self.thread_map.to_vscode(xml.thread['id'])
         try:
             name = unquote(xml.thread['name'])
         except KeyError:
             name = None
         if not self.is_debugger_internal_thread(name):
+            # Any internal pydevd or ptvsd threads will be ignored everywhere
+            tid = self.thread_map.to_vscode(xml.thread['id'], autogen=True)
             self.send_event('thread', reason='started', threadId=tid)
 
     @pydevd_events.handler(pydevd_comm.CMD_THREAD_KILL)
     def on_pydevd_thread_kill(self, seq, args):
         # TODO: docstring
         try:
-            tid = self.thread_map.to_vscode(args, autogen=False)
+            pyd_tid = args.strip()
+            vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
         except KeyError:
             pass
         else:
-            self.send_event('thread', reason='exited', threadId=tid)
+            self.send_event('thread', reason='exited', threadId=vsc_tid)
 
     @pydevd_events.handler(pydevd_comm.CMD_THREAD_SUSPEND)
     def on_pydevd_thread_suspend(self, seq, args):
         # TODO: docstring
         xml = untangle.parse(args).xml
-        tid = xml.thread['id']
+        pyd_tid = xml.thread['id']
         reason = int(xml.thread['stop_reason'])
         STEP_REASONS = {
                 pydevd_comm.CMD_STEP_INTO,
@@ -843,6 +858,12 @@ def on_pydevd_thread_suspend(self, seq, args):
             pydevd_comm.CMD_STEP_CAUGHT_EXCEPTION,
             pydevd_comm.CMD_ADD_EXCEPTION_BREAK
         }
+
+        try:
+            vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
+        except KeyError:
+            return
+
         if reason in STEP_REASONS:
             reason = 'step'
         elif reason in EXCEPTION_REASONS:
@@ -851,21 +872,25 @@ def on_pydevd_thread_suspend(self, seq, args):
             reason = 'breakpoint'
         else:
             reason = 'pause'
+
         with self.stack_traces_lock:
-            self.stack_traces[tid] = xml.thread.frame
-        tid = self.thread_map.to_vscode(tid)
-        self.send_event('stopped', reason=reason, threadId=tid)
+            self.stack_traces[pyd_tid] = xml.thread.frame
+        
+        self.send_event('stopped', reason=reason, threadId=vsc_tid)
 
     @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN)
     def on_pydevd_thread_run(self, seq, args):
         # TODO: docstring
         pyd_tid, reason = args.split('\t')
-        vsc_tid = self.thread_map.to_vscode(pyd_tid)
+        pyd_tid = pyd_tid.strip()
 
         # Stack trace, and all frames and variables for this thread
         # are now invalid; clear their IDs.
         with self.stack_traces_lock:
-            del self.stack_traces[pyd_tid]
+            try:
+                del self.stack_traces[pyd_tid]
+            except KeyError:
+                pass
 
         for pyd_fid, vsc_fid in self.frame_map.pairs():
             if pyd_fid[0] == pyd_tid:
@@ -874,8 +899,13 @@ def on_pydevd_thread_run(self, seq, args):
         for pyd_var, vsc_var in self.var_map.pairs():
             if pyd_var[0] == pyd_tid:
                 self.var_map.remove(pyd_var, vsc_var)
-
-        self.send_event('continued', threadId=vsc_tid)
+        
+        try:
+            vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
+        except KeyError:
+            pass
+        else:
+            self.send_event('continued', threadId=vsc_tid)
 
     @pydevd_events.handler(pydevd_comm.CMD_SEND_CURR_EXCEPTION_TRACE)
     def on_pydevd_send_curr_exception_trace(self, seq, args):


Test patch: