Repository: microsoft/ptvsd

Issue:
`break_into_debugger` does not work in Server Attach mode
## Environment data

- PTVSD version: master
- OS and version: Any
- Python version (& distribution if applicable, e.g. Anaconda): Any
- Using VS Code or Visual Studio: VSC

## Actual behavior

Does not break into debugger

## Expected  behavior

Should break into debugger


## Steps to reproduce:
1. use this file `test.py`:
```python
import time
import ptvsd

def main():
    count = 0
    while count < 50:
        time.sleep(0.3)
        ptvsd.break_into_debugger()
        count += 1
        print(count)
main()
```
2. `python -m ptvsd --port 5678 --server test.py`

Run the following test for repro:
```
py -3.7 -m tests -v tests.system_tests.test_disconnect.ServerAttachDisconnectLifecycleTests.test_attach_break_disconnect
```


Discussion:

Resolving patch:
diff --git a/ptvsd/_util.py b/ptvsd/_util.py
--- a/ptvsd/_util.py
+++ b/ptvsd/_util.py
@@ -395,3 +395,17 @@ def get_line_for_traceback(file_path, line_no):
             return f.readlines()[line_no - 1]
     except Exception:
         return None
+
+
+_enable_debug_break = False
+
+
+def _allow_debug_break(enabled=True):
+    """Enable breaking into debugger feature.
+    """
+    global _enable_debug_break
+    _enable_debug_break = enabled
+
+
+def _is_debug_break_allowed():
+    return _enable_debug_break
diff --git a/ptvsd/attach_server.py b/ptvsd/attach_server.py
--- a/ptvsd/attach_server.py
+++ b/ptvsd/attach_server.py
@@ -2,19 +2,16 @@
 # Licensed under the MIT License. See LICENSE in the project root
 # for license information.
 
-# TODO: Why import run_module & run_file?
-from ptvsd._local import run_module, run_file  # noqa
 from ptvsd._remote import (
     enable_attach as ptvsd_enable_attach, _pydevd_settrace,
 )
-from ptvsd.wrapper import debugger_attached, is_launch
+from ptvsd.wrapper import debugger_attached
 
 WAIT_TIMEOUT = 1.0
 
 DEFAULT_HOST = '0.0.0.0'
 DEFAULT_PORT = 5678
 
-_enabled = False
 _debug_current_thread = None
 _pending_threads = set()
 
@@ -60,10 +57,8 @@ def enable_attach(address=(DEFAULT_HOST, DEFAULT_PORT), redirect_output=True):
     attached. Any threads that are already running before this function is
     called will not be visible.
     """
-    global _enabled
-    if _enabled:
+    if is_attached():
         return
-    _enabled = True
     debugger_attached.clear()
 
     # Ensure port is int
@@ -88,12 +83,9 @@ 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 is_launch():
-        if not is_attached():
-            return
-    else:
-        if not is_attached() or not _enabled:
-            return
+    if not is_attached():
+        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
@@ -44,19 +44,12 @@
 from ptvsd.version import __version__  # noqa
 from ptvsd.socket import TimeoutError  # noqa
 
-
 WAIT_FOR_THREAD_FINISH_TIMEOUT = 1  # seconds
 
 
 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
@@ -278,16 +271,6 @@ def vscode_ids(self):
         return ids
 
 
-class ExceptionInfo(object):
-    # TODO: docstring
-
-    def __init__(self, name, description, stack, source):
-        self.name = name
-        self.description = description
-        self.stack = stack
-        self.source = source
-
-
 class PydevdSocket(object):
     """A dummy socket-like object for communicating with pydevd.
 
@@ -459,7 +442,7 @@ def _find_exception(self, name):
             return name
 
         for ex_name in self.exceptions.keys():
-            # ExceptionInfo.name can be in repr form
+            # exception name can be in repr form
             # here we attempt to find the exception as it
             # is saved in the dictionary
             if ex_name in name:
@@ -1100,9 +1083,6 @@ 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)
@@ -1110,9 +1090,6 @@ 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
@@ -2157,72 +2134,71 @@ def on_setExceptionBreakpoints(self, request, args):
         if request is not None:
             self.send_response(request)
 
+    def _parse_exception_details(self, exc_xml, include_stack=True):
+        exc_name = None
+        exc_desc = None
+        exc_source = None
+        exc_stack = None
+        try:
+            xml = self.parse_xml_response(exc_xml)
+            re_name = r"[\'\"](.*)[\'\"]"
+            exc_type = xml.thread['exc_type']
+            exc_desc = xml.thread['exc_desc']
+            try:
+                exc_name = re.findall(re_name, exc_type)[0]
+            except IndexError:
+                exc_name = exc_type
+
+            if include_stack:
+                xframes = list(xml.thread.frame)
+                frame_data = []
+                for f in xframes:
+                    file_path = unquote(f['file'])
+                    if not self.internals_filter.is_internal_path(file_path) \
+                       and self._should_debug(file_path):
+                        line_no = int(f['line'])
+                        func_name = unquote(f['name'])
+                        if _util.is_py34():
+                            # NOTE: In 3.4.* format_list requires the text
+                            # to be passed in the tuple list.
+                            line_text = _util.get_line_for_traceback(file_path,
+                                                                     line_no)
+                            frame_data.append((file_path, line_no,
+                                               func_name, line_text))
+                        else:
+                            frame_data.append((file_path, line_no,
+                                               func_name, None))
+
+                exc_stack = ''.join(traceback.format_list(frame_data))
+                exc_source = unquote(xframes[0]['file'])
+                if self.internals_filter.is_internal_path(exc_source) or \
+                    not self._should_debug(exc_source):
+                    exc_source = None
+        except Exception:
+            exc_name = 'BaseException'
+            exc_desc = 'exception: no description'
+
+        return exc_name, exc_desc, exc_source, exc_stack
+
     @async_handler
     def on_exceptionInfo(self, request, args):
         # TODO: docstring
         pyd_tid = self.thread_map.to_pydevd(args['threadId'])
 
-        # For exception cases both raise and uncaught, pydevd adds a
-        # __exception__ object to the top most frame. Extracting the
-        # exception name and description from that frame gives accurate
-        # exception information. Get exception info from frame
-        try:
-            cmd = pydevd_comm.CMD_GET_THREAD_STACK
-            _, _, resp_args = yield self.pydevd_request(cmd, pyd_tid)
-            xml = self.parse_xml_response(resp_args)
-            xframes = list(xml.thread.frame)
-
-            xframe = xframes[0]
-            pyd_fid = xframe['id']
+        cmdid = pydevd_comm.CMD_GET_EXCEPTION_DETAILS
+        _, _, resp_args = yield self.pydevd_request(cmdid, pyd_tid)
+        name, description, source, stack  = \
+            self._parse_exception_details(resp_args)
 
-            cmdargs = '{}\t{}\tFRAME\t__exception__'.format(pyd_tid, pyd_fid)
-            cmdid = pydevd_comm.CMD_GET_VARIABLE
-            _, _, resp_args = yield self.pydevd_request(cmdid, cmdargs)
-            xml = self.parse_xml_response(resp_args)
-
-            name = unquote(xml.var[1]['type'])
-            description = unquote(xml.var[1]['value'])
-
-            frame_data = []
-            for f in xframes:
-                file_path = unquote(f['file'])
-                if not self.internals_filter.is_internal_path(file_path) and \
-                    self._should_debug(file_path):
-                    line_no = int(f['line'])
-                    func_name = unquote(f['name'])
-                    if _util.is_py34():
-                        # NOTE: In 3.4.* format_list requires the text
-                        # to be passed in the tuple list.
-                        line_text = _util.get_line_for_traceback(file_path,
-                                                                 line_no)
-                        frame_data.append((file_path, line_no,
-                                           func_name, line_text))
-                    else:
-                        frame_data.append((file_path, line_no,
-                                           func_name, None))
-
-            stack = ''.join(traceback.format_list(frame_data))
-
-            source = unquote(xframe['file'])
-            if self.internals_filter.is_internal_path(source) or \
-                not self._should_debug(source):
-                source = None
-        except Exception:
-            name = 'BaseException'
-            description = 'exception: no description'
-            stack = None
-            source = None
-
-        exc = ExceptionInfo(name, description, stack, source)
         self.send_response(
             request,
-            exceptionId=exc.name,
-            description=exc.description,
-            breakMode=self.exceptions_mgr.get_break_mode(exc.name),
-            details={'typeName': exc.name,
-                     'message': exc.description,
-                     'stackTrace': exc.stack,
-                     'source': exc.source},
+            exceptionId=name,
+            description=description,
+            breakMode=self.exceptions_mgr.get_break_mode(name),
+            details={'typeName': name,
+                     'message': description,
+                     'stackTrace': stack,
+                     'source': source},
         )
 
     # Custom ptvsd message
@@ -2378,8 +2354,8 @@ def on_pydevd_thread_suspend(self, seq, args):
         autogen = self.start_reason == 'attach'
         vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=autogen)
 
-        description = None
-        text = None
+        exc_desc = None
+        exc_name = None
         if reason in STEP_REASONS:
             reason = 'step'
         elif reason in EXCEPTION_REASONS:
@@ -2390,26 +2366,17 @@ def on_pydevd_thread_suspend(self, seq, args):
             reason = 'pause'
 
         if reason == 'exception':
-            try:
-                pyd_fid = xframe['id']
-                cmdargs = '{}\t{}\tFRAME\t__exception__'.format(pyd_tid,
-                                                                pyd_fid)
-                cmdid = pydevd_comm.CMD_GET_VARIABLE
-                _, _, resp_args = yield self.pydevd_request(cmdid, cmdargs)
-                xml = self.parse_xml_response(resp_args)
-
-                text = unquote(xml.var[1]['type'])
-                description = unquote(xml.var[1]['value'])
-            except Exception:
-                text = 'BaseException'
-                description = 'exception: no description'
+            cmdid = pydevd_comm.CMD_GET_EXCEPTION_DETAILS
+            _, _, resp_args = yield self.pydevd_request(cmdid, pyd_tid)
+            exc_name, exc_desc, _, _  = \
+                self._parse_exception_details(resp_args, include_stack=False)
 
         self.send_event(
             'stopped',
             reason=reason,
             threadId=vsc_tid,
-            text=text,
-            description=description)
+            text=exc_name,
+            description=exc_desc)
 
     @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN)
     def on_pydevd_thread_run(self, seq, args):


Test patch:
diff --git a/tests/helpers/pydevd/_messages.py b/tests/helpers/pydevd/_messages.py
--- a/tests/helpers/pydevd/_messages.py
+++ b/tests/helpers/pydevd/_messages.py
@@ -101,3 +101,19 @@ def format_exception(self, threadid, exc, frame):
                 frame,
             ),
         )
+
+    def format_exception_details(self, threadid, exc, *frames):
+        name = pydevd_xml.make_valid_xml_value(str(type(exc)))
+        if hasattr(exc, 'args') and len(exc.args) > 0:
+            desc = str(exc.args[0])
+        else:
+            desc = str(exc)
+        desc = pydevd_xml.make_valid_xml_value(desc)
+        info = '<xml>'
+        info += '<thread id="{}" exc_type="{}" exc_desc="{}" >'.format(
+            threadid, name, desc)
+        fmt = '<frame id="{}" name="{}" file="{}" line="{}" />'
+        for frame in frames:  # (fid, func, filename, line)
+            info += fmt.format(*frame)
+        info += '</thread></xml>'
+        return info
diff --git a/tests/highlevel/__init__.py b/tests/highlevel/__init__.py
--- a/tests/highlevel/__init__.py
+++ b/tests/highlevel/__init__.py
@@ -15,9 +15,9 @@
     CMD_STEP_CAUGHT_EXCEPTION,
     CMD_SEND_CURR_EXCEPTION_TRACE,
     CMD_THREAD_CREATE,
-    CMD_GET_VARIABLE,
     CMD_SET_PROJECT_ROOTS,
     CMD_GET_THREAD_STACK,
+    CMD_GET_EXCEPTION_DETAILS,
 )
 
 from ptvsd._util import new_hidden_thread
@@ -503,12 +503,11 @@ def _exception(self, thread, exc, reason, stack):
         self.send_suspend_event(thread, reason, *stack)
         #self.set_exception_var_response(exc)
 
-    def set_exception_var_response(self, exc):
+    def set_exception_var_response(self, threadid, exc, *frames):
         self.set_response(
-            CMD_GET_VARIABLE,
-            self.msgs.format_variables(
-                ('???', '???'),
-                ('???', exc),
+            CMD_GET_EXCEPTION_DETAILS,
+            self.msgs.format_exception_details(
+                threadid, exc, *frames
             ),
         )
 
@@ -824,7 +823,7 @@ def suspend(self, thread, reason, *stack):
         with self.wait_for_event('stopped'):
             if isinstance(reason, Exception):
                 exc = reason
-                self._pydevd.set_exception_var_response(exc)
+                self._pydevd.set_exception_var_response(thread.id, exc, *stack)
                 self._pydevd.send_caught_exception_events(thread, exc, *stack)
             else:
                 self._pydevd.send_suspend_event(thread, reason, *stack)
diff --git a/tests/highlevel/test_live_pydevd.py b/tests/highlevel/test_live_pydevd.py
--- a/tests/highlevel/test_live_pydevd.py
+++ b/tests/highlevel/test_live_pydevd.py
@@ -391,16 +391,12 @@ def test_exception_breakpoints(self):
                 got.append(req['arguments'])
             self.assertNotEqual(req['command'], 'setBreakpoints')
         self.assertEqual(got, config['excbreakpoints'])
-        if sys.version_info >= (3, 7):
-            description = "MyError('ka-boom')"
-        else:
-            description = "MyError('ka-boom',)"
         self.assert_contains(received, [
             self.new_event('stopped', **dict(
                  reason='exception',
                  threadId=tid,
-                 text='MyError',
-                 description=description)),
+                 text='__main__.MyError',
+                 description='ka-boom')),
             self.new_response(req_continue_last, allThreadsContinued=True),
             self.new_event('continued', **dict(threadId=tid, )),
         ])
diff --git a/tests/highlevel/test_messages.py b/tests/highlevel/test_messages.py
--- a/tests/highlevel/test_messages.py
+++ b/tests/highlevel/test_messages.py
@@ -32,6 +32,7 @@
     CMD_WRITE_TO_CONSOLE,
     CMD_STEP_INTO_MY_CODE,
     CMD_GET_THREAD_STACK,
+    CMD_GET_EXCEPTION_DETAILS,
 )
 
 from . import RunningTest
@@ -2113,11 +2114,12 @@ class ExceptionInfoTests(NormalRequestTest, unittest.TestCase):
     #       ],
     #   ),
 
-    PYDEVD_CMD = CMD_GET_THREAD_STACK
+    PYDEVD_CMD = CMD_GET_EXCEPTION_DETAILS
 
     def pydevd_payload(self, threadid, *args):
-        if self.PYDEVD_CMD == CMD_GET_THREAD_STACK:
-            return self.debugger_msgs.format_frames(threadid, 'pause', *args)
+        if self.PYDEVD_CMD == CMD_GET_EXCEPTION_DETAILS:
+            return self.debugger_msgs.format_exception_details(
+                threadid, args[0], *args[1:])
         else:
             return self.debugger_msgs.format_variables(*args)
 
@@ -2128,28 +2130,19 @@ def test_active_exception(self):
         with self.launched():
             with self.hidden():
                 tid, thread = self.error('x', exc, frame)
-            self.set_debugger_response(thread.id, frame)
-            self.PYDEVD_CMD = CMD_GET_VARIABLE
-            self.set_debugger_response(
-                thread.id,
-                ('type', exc.__class__.__name__),
-                ('value', exc))
+            self.set_debugger_response(thread.id, exc, frame)
             self.send_request(
                 threadId=tid,
             )
             received = self.vsc.received
 
-        # TODO: Is this the right str?
-        excstr = "RuntimeError('something went wrong')"
-        if sys.version_info < (3, 7):
-            excstr = excstr[:-1] + ',)'
         self.assert_vsc_received(received, [
             self.expected_response(
                 exceptionId='RuntimeError',
-                description=excstr,
+                description='something went wrong',
                 breakMode='unhandled',
                 details=dict(
-                    message=excstr,
+                    message='something went wrong',
                     typeName='RuntimeError',
                     source=__file__,
                     stackTrace='\n'.join([
@@ -2163,11 +2156,7 @@ def test_active_exception(self):
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_THREAD_STACK,
-                str(thread.id)),
-            self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                '{}\t2\tFRAME\t__exception__'.format(thread.id)),
+                CMD_GET_EXCEPTION_DETAILS, str(thread.id)),
         ])
 
     def test_no_exception(self):
@@ -2177,11 +2166,7 @@ def test_no_exception(self):
         with self.launched():
             with self.hidden():
                 tid, thread = self.error('x', exc, frame)
-            self.set_debugger_response(thread.id, frame)
-            self.PYDEVD_CMD = CMD_GET_VARIABLE
-            # Don't provide exception info to simulate no exception
-            self.set_debugger_response(
-                thread.id)
+            self.set_debugger_response(thread.id, exc, frame)
             self.send_request(
                 threadId=tid,
             )
@@ -2189,24 +2174,25 @@ def test_no_exception(self):
 
         self.assert_vsc_received(received, [
             self.expected_response(
-                exceptionId='BaseException',
-                description='exception: no description',
+                exceptionId='RuntimeError',
+                description='something went wrong',
                 breakMode='unhandled',
                 details=dict(
-                    typeName='BaseException',
-                    message='exception: no description',
-                    stackTrace=None,
-                    source=None
+                    typeName='RuntimeError',
+                    message='something went wrong',
+                    source=__file__,
+                    stackTrace='\n'.join([
+                        '  File "{}", line {}, in fail'.format(__file__,
+                                                               lineno),
+                        '    raise RuntimeError(msg)',
+                        '',
+                    ]),
                 ),
             ),
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_THREAD_STACK,
-                str(thread.id)),
-            self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                '{}\t2\tFRAME\t__exception__'.format(thread.id)),
+                CMD_GET_EXCEPTION_DETAILS, str(thread.id)),
         ])
 
 
@@ -2666,31 +2652,26 @@ def test_caught_exception(self):
             with self.hidden():
                 _, thread = self.set_thread('x')
             self.set_debugger_response(
-                CMD_GET_VARIABLE,
-                self.debugger_msgs.format_variables(
-                    ('???', '???'),
-                    ('???', exc),
+                CMD_GET_EXCEPTION_DETAILS,
+                self.debugger_msgs.format_exception_details(
+                    thread.id, exc
                 ),
             )
             tid = self.send_event(thread.id, CMD_STEP_CAUGHT_EXCEPTION)
             received = self.vsc.received
 
-        # TODO: Is this the right str?
-        excstr = "RuntimeError('something went wrong')"
-        if sys.version_info[1] < 7:
-            excstr = excstr[:-1] + ',)'
         self.assert_vsc_received(received, [
             self.expected_event(
                 reason='exception',
                 threadId=tid,
                 text='RuntimeError',
-                description=excstr,
+                description='something went wrong',
             ),
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                str(thread.id), '2', 'FRAME', '__exception__'),
+                CMD_GET_EXCEPTION_DETAILS,
+                str(thread.id)),
         ])
 
     def test_exception_break(self):
@@ -2699,31 +2680,26 @@ def test_exception_break(self):
             with self.hidden():
                 _, thread = self.set_thread('x')
             self.set_debugger_response(
-                CMD_GET_VARIABLE,
-                self.debugger_msgs.format_variables(
-                    ('???', '???'),
-                    ('???', exc),
+                CMD_GET_EXCEPTION_DETAILS,
+                self.debugger_msgs.format_exception_details(
+                    thread.id, exc
                 ),
             )
             tid = self.send_event(thread.id, CMD_ADD_EXCEPTION_BREAK)
             received = self.vsc.received
 
-        # TODO: Is this the right str?
-        excstr = "RuntimeError('something went wrong')"
-        if sys.version_info[1] < 7:
-            excstr = excstr[:-1] + ',)'
         self.assert_vsc_received(received, [
             self.expected_event(
                 reason='exception',
                 threadId=tid,
                 text='RuntimeError',
-                description=excstr,
+                description='something went wrong',
             ),
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                str(thread.id), '2', 'FRAME', '__exception__'),
+                CMD_GET_EXCEPTION_DETAILS,
+                str(thread.id)),
         ])
 
     def test_suspend(self):
diff --git a/tests/resources/system_tests/test_break_into_debugger/mypkg/__init__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg/__init__.py
new file mode 100644
diff --git a/tests/resources/system_tests/test_break_into_debugger/mypkg/__main__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg/__main__.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/mypkg/__main__.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/mypkg_attach/__init__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg_attach/__init__.py
new file mode 100644
diff --git a/tests/resources/system_tests/test_break_into_debugger/mypkg_attach/__main__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg_attach/__main__.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/mypkg_attach/__main__.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/mypkg_launch/__init__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg_launch/__init__.py
new file mode 100644
diff --git a/tests/resources/system_tests/test_break_into_debugger/mypkg_launch/__main__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg_launch/__main__.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/mypkg_launch/__main__.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/mypkg_reattach/__init__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg_reattach/__init__.py
new file mode 100644
diff --git a/tests/resources/system_tests/test_break_into_debugger/mypkg_reattach/__main__.py b/tests/resources/system_tests/test_break_into_debugger/mypkg_reattach/__main__.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_break_into_debugger/mypkg_reattach/__main__.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
--- a/tests/system_tests/test_break_into_debugger.py
+++ b/tests/system_tests/test_break_into_debugger.py
@@ -94,6 +94,51 @@ def run_test_reattach(self, debug_info):
             self.new_event('terminated'),
         ])
 
+
+class LaunchFileBreakIntoDebuggerTests(BreakIntoDebuggerTests):
+    def test_launch_and_break(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)
+
+
+class LaunchModuleBreakIntoDebuggerTests(BreakIntoDebuggerTests):
+    def test_launch_and_break(self):
+        module_name = 'mypkg_launch'
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.parent.root
+        self.run_test_attach_or_launch(
+            DebugInfo(modulename=module_name, env=env, cwd=cwd))
+
+
+class ServerAttachBreakIntoDebuggerTests(BreakIntoDebuggerTests):
+    def test_attach_and_break(self):
+        filename = TEST_FILES.resolve('launch_test.py')
+        cwd = os.path.dirname(filename)
+        debug_info = DebugInfo(
+            filename=filename,
+            cwd=cwd,
+            starttype='attach',
+        )
+        self.run_test_attach_or_launch(debug_info)
+
+
+class ServerAttachModuleBreakIntoDebuggerTests(BreakIntoDebuggerTests):
+    def test_attach_and_break(self):
+        module_name = 'mypkg_launch'
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            cwd=cwd,
+            env=env,
+            starttype='attach',
+        )
+        self.run_test_attach_or_launch(debug_info)
+
+
+class PTVSDAttachBreakIntoDebuggerTests(BreakIntoDebuggerTests):
     def test_attach_enable_wait_and_break(self):
         # Uses enable_attach followed by wait_for_attach
         # before calling break_into_debugger
@@ -138,12 +183,6 @@ def test_attach_enable_and_break(self):
             )
         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
@@ -187,3 +226,105 @@ def test_reattach_enable_and_break(self):
             attachtype='import',
             )
         self.run_test_reattach(debug_info)
+
+
+class PTVSDAttachModuleBreakIntoDebuggerTests(BreakIntoDebuggerTests):
+    def test_attach_enable_wait_and_break(self):
+        # Uses enable_attach followed by wait_for_attach
+        # before calling break_into_debugger
+        module_name = 'mypkg_attach'
+        env = TEST_FILES.env_with_py_path()
+        env['PTVSD_WAIT_FOR_ATTACH'] = 'True'
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            env=env,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            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
+        module_name = 'mypkg_attach'
+        env = TEST_FILES.env_with_py_path()
+        env['PTVSD_IS_ATTACHED'] = 'True'
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            env=env,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            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
+        module_name = 'mypkg_attach'
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            env=env,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_attach_or_launch(debug_info, end_loop=True)
+
+    def test_reattach_enable_wait_and_break(self):
+        # Uses enable_attach followed by wait_for_attach
+        # before calling break_into_debugger
+        module_name = 'mypkg_reattach'
+        env = TEST_FILES.env_with_py_path()
+        env['PTVSD_WAIT_FOR_ATTACH'] = 'True'
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            env=env,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            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
+        module_name = 'mypkg_reattach'
+        env = TEST_FILES.env_with_py_path()
+        env['PTVSD_IS_ATTACHED'] = 'True'
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            env=env,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            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
+        module_name = 'mypkg_reattach'
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        debug_info = DebugInfo(
+            modulename=module_name,
+            env=env,
+            cwd=cwd,
+            argv=['localhost', str(PORT)],
+            starttype='attach',
+            attachtype='import',
+            )
+        self.run_test_reattach(debug_info)
diff --git a/tests/system_tests/test_exceptions.py b/tests/system_tests/test_exceptions.py
--- a/tests/system_tests/test_exceptions.py
+++ b/tests/system_tests/test_exceptions.py
@@ -77,8 +77,7 @@ def run_test_breaking_into_handled_exceptions(self, debug_info,
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -129,8 +128,7 @@ def run_test_breaking_into_unhandled_exceptions(self, debug_info,
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -191,8 +189,7 @@ def run_test_breaking_into_raised_exceptions_only(self, debug_info,
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -247,8 +244,7 @@ def run_test_breaking_into_raised_and_unhandled_exceptions(
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -267,8 +263,8 @@ def run_test_breaking_into_raised_and_unhandled_exceptions(
 
             # Second hit on uncaught exception
             self.assertEqual(stopped2.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped2.event.body['description'])
+            self.assertEqual(stopped2.event.body['description'], 'Hello')
+
             req_exc_info2 = dbg.session.send_request(
                 'exceptionInfo',
                 threadId=thread_id,