Repository: microsoft/ptvsd

Issue:
Provide a public API to register threads not created by Python for debugging
This can be done by simply wrapping `pydevd.settrace(suspend=False)`


Discussion:
Looking at it, we never actually had it as a *public* API in ptvsd. It was an internal function that just happened to do the right thing, and so we suggested it as a workaround for people who ran into issued, but it was not a stable API. You could see that it was internal, because you had to import it from `ptvsd.debugger` submodule, rather than the top-level package.

Since we're making it a public API here, we might as well revisit the naming and the placement, so that it looks the same as other functions, and the name is more expressive. I suggest `ptvsd.debug_this_thread()`.

Resolving patch:
diff --git a/src/ptvsd/__init__.py b/src/ptvsd/__init__.py
--- a/src/ptvsd/__init__.py
+++ b/src/ptvsd/__init__.py
@@ -12,7 +12,12 @@
 # that the ptvsd-vendored copy of pydevd gets used.
 from ._vendored import force_pydevd
 from ptvsd.version import __version__, __author__
-from ptvsd.attach_server import (
-    enable_attach, wait_for_attach, break_into_debugger, is_attached,
+from ptvsd.attach_server import (  # noqa
+    attach,
+    break_into_debugger,
+    debug_this_thread,
+    enable_attach,
+    is_attached,
+    wait_for_attach,
 )
 del force_pydevd
diff --git a/src/ptvsd/attach_server.py b/src/ptvsd/attach_server.py
--- a/src/ptvsd/attach_server.py
+++ b/src/ptvsd/attach_server.py
@@ -9,6 +9,8 @@
 )
 from ptvsd.wrapper import debugger_attached
 import sys
+
+import pydevd
 from _pydevd_bundle.pydevd_constants import get_global_debugger
 from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
 
@@ -128,3 +130,7 @@ def break_into_debugger():
         stop_at_frame=stop_at_frame,
     )
     stop_at_frame = None
+
+
+def debug_this_thread():
+    pydevd.settrace(suspend=False)


Test patch:
diff --git a/tests/func/test_threads.py b/tests/func/test_threads.py
--- a/tests/func/test_threads.py
+++ b/tests/func/test_threads.py
@@ -3,7 +3,11 @@
 # for license information.
 
 from __future__ import print_function, with_statement, absolute_import
+
+import platform
 import pytest
+
+from tests.helpers import get_marked_line_numbers
 from tests.helpers.timeline import Event
 from tests.helpers.session import DebugSession
 
@@ -49,3 +53,56 @@ def worker(tid, offset):
 
         session.send_request('continue').wait_for_response()
         session.wait_for_exit()
+
+
+@pytest.mark.skipif(
+    platform.system() not in ['Windows', 'Linux', 'Darwin'],
+    reason='Test not implemented on ' + platform.system())
+def test_debug_this_thread(pyfile, run_as, start_method):
+    @pyfile
+    def code_to_debug():
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+
+        import platform
+        import ptvsd
+        import threading
+
+        def foo(x):
+            ptvsd.debug_this_thread()
+            event.set() #@bp
+            return 0
+
+        event = threading.Event()
+
+        if platform.system() == 'Windows':
+            from ctypes import CFUNCTYPE, c_void_p, c_size_t, c_uint32, windll
+            thread_func_p = CFUNCTYPE(c_uint32, c_void_p)
+            thread_func = thread_func_p(foo) # must hold a reference to wrapper during the call
+            assert windll.kernel32.CreateThread(c_void_p(0), c_size_t(0), thread_func, c_void_p(0), c_uint32(0), c_void_p(0))
+        elif platform.system() == 'Linux' or platform.system() == 'Darwin':
+            from ctypes import CDLL, CFUNCTYPE, byref, c_void_p, c_ulong
+            from ctypes.util import find_library
+            libpthread = CDLL(find_library('libpthread'))
+            thread_func_p = CFUNCTYPE(c_void_p, c_void_p)
+            thread_func = thread_func_p(foo) # must hold a reference to wrapper during the call
+            assert not libpthread.pthread_create(byref(c_ulong(0)), c_void_p(0), thread_func, c_void_p(0))
+        else:
+            assert False
+
+        event.wait()
+
+    line_numbers = get_marked_line_numbers(code_to_debug)
+
+    with DebugSession() as session:
+        session.initialize(
+            target=(run_as, code_to_debug),
+            start_method=start_method,
+            ignore_unobserved=[Event('continued')],
+        )
+        session.set_breakpoints(code_to_debug, [line_numbers['bp']])
+        session.start_debugging()
+
+        session.wait_for_thread_stopped()
+        session.send_request('continue').wait_for_response()
+        session.wait_for_exit()