Repository: microsoft/ptvsd

Issue:
No-Debug run (ctrl+F5) VSCode is unable to establish connection with debugger
## Environment data

- PTVSD version: master
- OS and version: Any
- Python version (& distribution if applicable, e.g. Anaconda): Any
- Using VS Code or Visual Studio: Any

## Actual behavior

Debugger runs user code but does not connect with VSCode. This causes VSCode to think the session never ended. Eventually VSCode will pop up a message saying failed to establish connection whit the debugger. 

## Expected  behavior

Should connect to the debugger but run with no-debug option.

## Steps to reproduce:
1. Run any code with 'Ctrl+F5'

Create system tests for running without debugging



Discussion:
@int19h This issue takes highest priority for now. 
See `NoDebugRunTests` tests under `test_locals.py`. Let me know if additional tests are needed.
In light of recent regressions, I'd feel far more confident with system tests.
All of the regressions we had recently were no caught by the tests, either lack of or other wise.

Resolving patch:
diff --git a/src/ptvsd/__main__.py b/src/ptvsd/__main__.py
--- a/src/ptvsd/__main__.py
+++ b/src/ptvsd/__main__.py
@@ -38,6 +38,7 @@
 
 import ptvsd._remote
 import ptvsd.options
+import ptvsd.runner
 import ptvsd.version
 
 
@@ -57,9 +58,9 @@
 HELP = ('''ptvsd %s
 See https://aka.ms/ptvsd for documentation.
 
-Usage: ptvsd --host <address> --port <port> [--wait] [--multiprocess]
+Usage: ptvsd --host <address> [--port <port>] [--wait] [--multiprocess]
              ''' + TARGET + '''
-''') % (ptvsd.version.__version__)
+''') % (ptvsd.version.__version__,)
 
 
 # In Python 2, arguments are passed as bytestrings in locale encoding
@@ -109,6 +110,11 @@ def do(arg, it):
         ptvsd.options.target = arg if parser is None else parser(next(it))
     return do
 
+def set_nodebug(arg, it):
+    # --nodebug implies --client
+    ptvsd.options.no_debug = True
+    ptvsd.options.client = True
+
 
 switches = [
     # Switch                    Placeholder         Action                                  Required
@@ -123,7 +129,7 @@ def do(arg, it):
     ('--multiprocess',          None,               set_true('multiprocess'),               False),
 
     # Switches that are used internally by the IDE or ptvsd itself.
-    ('--nodebug',               None,               set_true('no_debug'),                   False),
+    ('--nodebug',               None,               set_nodebug,                            False),
     ('--client',                None,               set_true('client'),                     False),
     ('--subprocess-of',         '<pid>',            set_arg('subprocess_of', pid),          False),
     ('--subprocess-notify',     '<port>',           set_arg('subprocess_notify', port),     False),
@@ -185,9 +191,6 @@ def parse(args):
 
 def setup_connection():
     opts = ptvsd.options
-    if opts.no_debug:
-        return
-
     pydevd.apply_debugger_options({
         'server': not opts.client,
         'client': opts.host,
@@ -224,7 +227,11 @@ def setup_connection():
     addr = (opts.host, opts.port)
 
     global daemon
-    if opts.client:
+    if opts.no_debug:
+        daemon = ptvsd.runner.Daemon()
+        if not daemon.wait_for_launch(addr):
+            return
+    elif opts.client:
         daemon = ptvsd._remote.attach(addr)
     else:
         daemon = ptvsd._remote.enable_attach(addr)
diff --git a/src/ptvsd/_remote.py b/src/ptvsd/_remote.py
--- a/src/ptvsd/_remote.py
+++ b/src/ptvsd/_remote.py
@@ -41,9 +41,12 @@ def _pydevd_settrace(redirect_output=None, _pydevd=pydevd, **kwargs):
 global_next_session = lambda: None
 
 
-def enable_attach(address, redirect_output=True,
-                  _pydevd=pydevd, _install=install,
-                  on_attach=lambda: None, **kwargs):
+def enable_attach(address,
+                  redirect_output=True,
+                  _pydevd=pydevd,
+                  _install=install,
+                  on_attach=lambda: None,
+                  **kwargs):
 
     ptvsd.main_thread = threading.current_thread()
 
@@ -84,12 +87,16 @@ def start_daemon():
                                           args=(daemon, host, port))
     connection_thread.start()
 
-    _pydevd.settrace(host=host,
-                     stdoutToServer=redirect_output,
-                     stderrToServer=redirect_output,
-                     port=port,
-                     suspend=False,
-                     patch_multiprocessing=ptvsd.options.multiprocess)
+    if ptvsd.options.no_debug:
+        _setup_nodebug()
+    else:
+        _pydevd.settrace(host=host,
+                         stdoutToServer=redirect_output,
+                         stderrToServer=redirect_output,
+                         port=port,
+                         suspend=False,
+                         patch_multiprocessing=ptvsd.options.multiprocess)
+
     return daemon
 
 
@@ -103,10 +110,32 @@ def attach(address,
 
     host, port = address
     daemon = _install(_pydevd, address, singlesession=False, **kwargs)
-    _pydevd.settrace(host=host,
-                     port=port,
-                     stdoutToServer=redirect_output,
-                     stderrToServer=redirect_output,
-                     suspend=False,
-                     patch_multiprocessing=ptvsd.options.multiprocess)
+
+    if ptvsd.options.no_debug:
+        _setup_nodebug()
+    else:
+        _pydevd.settrace(host=host,
+                         port=port,
+                         stdoutToServer=redirect_output,
+                         stderrToServer=redirect_output,
+                         suspend=False,
+                         patch_multiprocessing=ptvsd.options.multiprocess)
+
     return daemon
+
+
+def _setup_nodebug():
+    debugger = pydevd.PyDB()
+    debugger.init_matplotlib_support = lambda *arg: None
+    # We are invoking run() solely for side effects here - setting up the
+    # debugger and connecting to our socket - so the code run is a no-op.
+    debugger.run(
+        file='ptvsd._remote:_nop',
+        globals=None,
+        locals=None,
+        is_module=True,
+        set_trace=False)
+
+
+def _nop():
+    pass
diff --git a/src/ptvsd/pydevd_hooks.py b/src/ptvsd/pydevd_hooks.py
--- a/src/ptvsd/pydevd_hooks.py
+++ b/src/ptvsd/pydevd_hooks.py
@@ -131,7 +131,7 @@ def install(pydevd_module, address,
 
     # This is invoked when a child process is spawned with multiproc debugging enabled.
     pydev_monkey.patch_args = multiproc.patch_and_quote_args
-    if not options.multiprocess:
+    if not options.multiprocess and not options.no_debug:
         # This means '--multiprocess' flag was not passed via command line args. Patch the
         # new process functions here to handle multiprocess being enabled via debug options.
         pydev_monkey.patch_new_process_functions()


Test patch:
diff --git a/tests/func/test_run.py b/tests/func/test_run.py
--- a/tests/func/test_run.py
+++ b/tests/func/test_run.py
@@ -47,3 +47,29 @@ def code_to_debug():
         assert re.match(re.escape(expected_ptvsd_path) + r'(c|o)?$', ptvsd_path)
 
         session.wait_for_exit()
+
+
+@pytest.mark.parametrize('run_as', ['file', 'module', 'code'])
+def test_nodebug(pyfile, run_as):
+    @pyfile
+    def code_to_debug():
+        # import_and_enable_debugger
+        import backchannel
+        backchannel.read_json()
+        print('ok')
+
+    with DebugSession() as session:
+        session.no_debug = True
+        session.initialize(target=(run_as, code_to_debug), start_method='launch', use_backchannel=True)
+        session.set_breakpoints(code_to_debug, [3])
+        session.start_debugging()
+
+        session.write_json(None)
+
+        # Breakpoint shouldn't be hit.
+        session.wait_for_exit()
+
+        session.expect_realized(Event('output', ANY.dict_with({
+            'category': 'stdout',
+            'output': 'ok',
+        })))
diff --git a/tests/helpers/session.py b/tests/helpers/session.py
--- a/tests/helpers/session.py
+++ b/tests/helpers/session.py
@@ -47,6 +47,7 @@ def __init__(self, start_method='launch', ptvsd_port=None, pid=None):
 
         self.target = ('code', 'print("OK")')
         self.start_method = start_method
+        self.no_debug = False
         self.ptvsd_port = ptvsd_port or PTVSD_PORT
         self.multiprocess = False
         self.multiprocess_port_range = None
@@ -244,6 +245,9 @@ def initialize(self, **kwargs):
         else:
             pytest.fail()
 
+        if self.no_debug:
+            argv += ['--nodebug']
+
         argv += self._get_target()
 
         if self.program_args:
@@ -445,11 +449,12 @@ def handshake(self):
             'pathMappings': self.path_mappings,
         }).wait_for_response()
 
-        # Issue 'threads' so that we get the 'thread' event for the main thread now,
-        # rather than at some random time later during the test.
-        (self.send_request('threads')
-            .causing(Event('thread'))
-            .wait_for_response())
+        if not self.no_debug:
+            # Issue 'threads' so that we get the 'thread' event for the main thread now,
+            # rather than at some random time later during the test.
+            (self.send_request('threads')
+                .causing(Event('thread'))
+                .wait_for_response())
 
     def start_debugging(self, freeze=True):
         """Finalizes the configuration stage, and issues a 'configurationDone' request
@@ -461,17 +466,20 @@ def start_debugging(self, freeze=True):
 
         configurationDone_request = self.send_request('configurationDone')
 
-        # The relative ordering of 'process' and 'configurationDone' is not deterministic.
-        # (implementation varies depending on whether it's launch or attach, but in any
-        # case, it is an implementation detail).
-        start = self.wait_for_next(Event('process') & Response(configurationDone_request))
-
-        self.expect_new(Event('process', {
-            'name': ANY.str,
-            'isLocalProcess': True,
-            'startMethod': 'launch' if self.start_method == 'launch' else 'attach',
-            'systemProcessId': self.pid if self.pid is not None else ANY.int,
-        }))
+        if self.no_debug:
+            start = self.wait_for_next(Response(configurationDone_request))
+        else:
+            # The relative ordering of 'process' and 'configurationDone' is not deterministic.
+            # (implementation varies depending on whether it's launch or attach, but in any
+            # case, it is an implementation detail).
+            start = self.wait_for_next(Event('process') & Response(configurationDone_request))
+
+            self.expect_new(Event('process', {
+                'name': ANY.str,
+                'isLocalProcess': True,
+                'startMethod': 'launch' if self.start_method == 'launch' else 'attach',
+                'systemProcessId': self.pid if self.pid is not None else ANY.int,
+            }))
 
         if not freeze:
             self.proceed()