Repository: microsoft/ptvsd

Issue:
ptvsd.break_into_debugger only works when attaching
Its a public api, hence must be possible to use even when debugging using launch.


Discussion:
via #706

Resolving patch:
diff --git a/ptvsd/attach_server.py b/ptvsd/attach_server.py
--- a/ptvsd/attach_server.py
+++ b/ptvsd/attach_server.py
@@ -7,7 +7,7 @@
 from ptvsd._remote import (
     enable_attach as ptvsd_enable_attach, _pydevd_settrace,
 )
-from ptvsd.wrapper import debugger_attached
+from ptvsd.wrapper import debugger_attached, is_launch
 
 WAIT_TIMEOUT = 1.0
 
@@ -88,8 +88,12 @@ def break_into_debugger():
     """If a remote debugger is attached, pauses execution of all threads,
     and breaks into the debugger with current thread as active.
     """
-    if not debugger_attached.isSet() or not _enabled:
-        return
+    if is_launch():
+        if not is_attached():
+            return
+    else:
+        if not is_attached() or not _enabled:
+            return
     import sys
     _pydevd_settrace(
         suspend=True,
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -51,6 +51,12 @@
 debug = _util.debug
 debugger_attached = threading.Event()
 
+_debugger_start_reason = None
+
+
+def is_launch():
+    return _debugger_start_reason == 'launch'
+
 #def ipcjson_trace(s):
 #    print(s)
 #ipcjson._TRACE = ipcjson_trace
@@ -1094,6 +1100,9 @@ def on_launch(self, request, args):
     def on_configurationDone(self, request, args):
         # TODO: docstring
         debugger_attached.set()
+        if self.start_reason == 'launch':
+            global _debugger_start_reason
+            _debugger_start_reason = 'launch'
         self.send_response(request)
         self._process_debug_options(self.debug_options)
         self._handle_configurationDone(args)
@@ -1101,6 +1110,9 @@ def on_configurationDone(self, request, args):
 
     def on_disconnect(self, request, args):
         debugger_attached.clear()
+        if self.start_reason == 'launch':
+            global _debugger_start_reason
+            _debugger_start_reason = None
         self._restart_debugger = args.get('restart', False)
 
         # TODO: docstring


Test patch:
diff --git a/tests/helpers/proc.py b/tests/helpers/proc.py
--- a/tests/helpers/proc.py
+++ b/tests/helpers/proc.py
@@ -222,6 +222,14 @@ def wait(self, timeout=5.0):
     # internal methods
 
     def _close(self):
+        self.terminate()
+        if self.VERBOSE:
+            out = self.output
+            if out is not None:
+                lines = out.decode('utf-8').splitlines()
+                print(' + ' + '\n + '.join(lines))
+
+    def terminate(self):
         if self._proc is not None:
             try:
                 self._proc.kill()
@@ -231,8 +239,3 @@ def _close(self):
             else:
                 if self.VERBOSE:
                     print('proc killed')
-        if self.VERBOSE:
-            out = self.output
-            if out is not None:
-                lines = out.decode('utf-8').splitlines()
-                print(' + ' + '\n + '.join(lines))
diff --git a/tests/resources/system_tests/test_break_into_debugger/attach_test.py b/tests/resources/system_tests/test_break_into_debugger/attach_test.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/attach_test.py
@@ -0,0 +1,36 @@
+import sys
+import ptvsd
+import os
+import time
+
+ptvsd.enable_attach((sys.argv[1], sys.argv[2]))
+
+
+loopy = False
+if os.getenv('PTVSD_WAIT_FOR_ATTACH', None) is not None:
+    print('waiting for attach')
+    ptvsd.wait_for_attach()
+elif os.getenv('PTVSD_IS_ATTACHED', None) is not None:
+    print('checking is attached')
+    while not ptvsd.is_attached():
+        time.sleep(0.1)
+else:
+    loopy = True
+
+
+def main():
+    if loopy:
+        count = 0
+        while count < 50:
+            print('one')
+            ptvsd.break_into_debugger()
+            time.sleep(0.1)
+            print('two')
+            count += 1
+    else:
+        print('one')
+        ptvsd.break_into_debugger()
+        print('two')
+
+
+main()
diff --git a/tests/resources/system_tests/test_break_into_debugger/launch_test.py b/tests/resources/system_tests/test_break_into_debugger/launch_test.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/launch_test.py
@@ -0,0 +1,10 @@
+import ptvsd
+
+
+def main():
+    print('one')
+    ptvsd.break_into_debugger()
+    print('two')
+
+
+main()
diff --git a/tests/resources/system_tests/test_break_into_debugger/reattach_test.py b/tests/resources/system_tests/test_break_into_debugger/reattach_test.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/reattach_test.py
@@ -0,0 +1,28 @@
+import sys
+import ptvsd
+import os
+import time
+
+ptvsd.enable_attach((sys.argv[1], sys.argv[2]))
+
+
+def main():
+    count = 0
+    while count < 50:
+        if os.getenv('PTVSD_WAIT_FOR_ATTACH', None) is not None:
+            print('waiting for attach')
+            ptvsd.wait_for_attach()
+        elif os.getenv('PTVSD_IS_ATTACHED', None) is not None:
+            print('checking is attached')
+            while not ptvsd.is_attached():
+                time.sleep(0.1)
+        else:
+            pass
+        print('one')
+        ptvsd.break_into_debugger()
+        time.sleep(0.5)
+        print('two')
+        count += 1
+
+
+main()
diff --git a/tests/system_tests/test_break_into_debugger.py b/tests/system_tests/test_break_into_debugger.py
new file mode 100644
--- /dev/null
+++ b/tests/system_tests/test_break_into_debugger.py
@@ -0,0 +1,189 @@
+import os
+import os.path
+import time
+
+from tests.helpers.debugsession import Awaitable
+from tests.helpers.resource import TestResources
+from . import (
+    _strip_newline_output_events, lifecycle_handshake,
+    LifecycleTestsBase, DebugInfo, PORT
+)
+
+TEST_FILES = TestResources.from_module(__name__)
+
+
+class BreakIntoDebuggerTests(LifecycleTestsBase):
+    def run_test_attach_or_launch(self, debug_info, end_loop=False):
+        options = {'debugOptions': ['RedirectOutput']}
+        with self.start_debugging(debug_info) as dbg:
+            session = dbg.session
+            stopped = session.get_awaiter_for_event('stopped')
+            (_, req_launch_attach, _, _, _, _) = lifecycle_handshake(
+                session,
+                debug_info.starttype,
+                options=options)
+            Awaitable.wait_all(req_launch_attach, stopped)
+            thread_id = stopped.event.body['threadId']
+            if end_loop:
+                self.set_var_to_end_loop(session, thread_id)
+            session.send_request('continue', threadId=thread_id)
+
+        received = list(_strip_newline_output_events(dbg.session.received))
+        self.assert_contains(received, [
+            self.new_event('output', category='stdout', output='one'),
+            self.new_event('output', category='stdout', output='two'),
+            self.new_event('continued', threadId=thread_id),
+            self.new_event('exited', exitCode=0),
+            self.new_event('terminated'),
+        ])
+
+    def set_var_to_end_loop(self, session, thread_id):
+        # Set count > 1000 to end the loop
+        req_stacktrace = session.send_request(
+            'stackTrace',
+            threadId=thread_id,
+        )
+        req_stacktrace.wait()
+        frames = req_stacktrace.resp.body['stackFrames']
+        frame_id = frames[0]['id']
+        req_scopes = session.send_request(
+            'scopes',
+            frameId=frame_id,
+        )
+        req_scopes.wait()
+        scopes = req_scopes.resp.body['scopes']
+        variables_reference = scopes[0]['variablesReference']
+        req_setvar = session.send_request(
+                'setVariable',
+                variablesReference=variables_reference,
+                name='count',
+                value='1000'
+            )
+        req_setvar.wait()
+
+    def run_test_reattach(self, debug_info):
+        options = {'debugOptions': ['RedirectOutput']}
+        with self.start_debugging(debug_info) as dbg:
+            session = dbg.session
+            stopped1 = session.get_awaiter_for_event('stopped')
+            (_, req_launch_attach, _, _, _, _) = lifecycle_handshake(
+                session,
+                debug_info.starttype,
+                options=options,
+                threads=True)
+            Awaitable.wait_all(req_launch_attach, stopped1)
+
+            thread_id = stopped1.event.body['threadId']
+            req_disconnect = session.send_request('disconnect', restart=False)
+            req_disconnect.wait()
+
+            time.sleep(1)
+            (_, req_launch_attach, _, _, _, _) = lifecycle_handshake(
+                session,
+                debug_info.starttype,
+                options=options,
+                threads=True)
+            Awaitable.wait_all(req_launch_attach)
+
+            self.set_var_to_end_loop(session, thread_id)
+            session.send_request('continue', threadId=thread_id)
+
+        received = list(_strip_newline_output_events(session.received))
+        self.assert_contains(received, [
+            self.new_event('exited', exitCode=0),
+            self.new_event('terminated'),
+        ])
+
+    def test_attach_enable_wait_and_break(self):
+        # Uses enable_attach followed by wait_for_attach
+        # before calling break_into_debugger
+        filename = TEST_FILES.resolve('attach_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            env={'PTVSD_WAIT_FOR_ATTACH': 'True'},
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_attach_or_launch(debug_info)
+
+    def test_attach_enable_check_and_break(self):
+        # Uses enable_attach followed by a loop that checks if the
+        # debugger is attached before calling break_into_debugger
+        filename = TEST_FILES.resolve('attach_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            env={'PTVSD_IS_ATTACHED': 'True'},
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_attach_or_launch(debug_info)
+
+    def test_attach_enable_and_break(self):
+        # Uses enable_attach followed by break_into_debugger
+        # not is_attached check or wait_for_debugger
+        filename = TEST_FILES.resolve('attach_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_attach_or_launch(debug_info, end_loop=True)
+
+    def test_launch(self):
+        filename = TEST_FILES.resolve('launch_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(filename=filename, cwd=cwd)
+        self.run_test_attach_or_launch(debug_info)
+
+    def test_reattach_enable_wait_and_break(self):
+        # Uses enable_attach followed by wait_for_attach
+        # before calling break_into_debugger
+        filename = TEST_FILES.resolve('reattach_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            env={'PTVSD_WAIT_FOR_ATTACH': 'True'},
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_reattach(debug_info)
+
+    def test_reattach_enable_check_and_break(self):
+        # Uses enable_attach followed by a loop that checks if the
+        # debugger is attached before calling break_into_debugger
+        filename = TEST_FILES.resolve('reattach_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            env={'PTVSD_IS_ATTACHED': 'True'},
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_reattach(debug_info)
+
+    def test_reattach_enable_and_break(self):
+        # Uses enable_attach followed by break_into_debugger
+        # not is_attached check or wait_for_debugger
+        filename = TEST_FILES.resolve('reattach_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_reattach(debug_info)