Repository: microsoft/ptvsd

Issue:
test_flask_breakpoint_multiproc fails with run=attach_by_socket
Python 3.7, Win32.

Logs are unclear - needs investigation.


Discussion:
When this happens, the session logs some error output coming from inside pydevd in the subprocess (that Flask spawns to perform auto-reload):
```
I+06.356: Debuggee-80 stderr:
          'Traceback (most recent call last):'

I+06.356: Debuggee-80 stderr:
          '\r\n'

I+06.357: Debuggee-80 stderr:
          '  File "d:\\a\\1\\s\\.tox\\py27\\lib\\site-packages\\ptvsd\\_vendored\\pydevd\\pydevd.py", line 3108, in <module>'

I+06.357: Debuggee-80 stderr:
          '\r\n'

I+06.361: Debuggee-80 stderr:
          '    '

I+06.361: Debuggee-80 stderr:
          'main()'

I+06.361: Debuggee-80 stderr:
          '\r\n'

I+06.361: Debuggee-80 stderr:
          '  File "d:\\a\\1\\s\\.tox\\py27\\lib\\site-packages\\ptvsd\\_vendored\\pydevd\\pydevd.py", line 3101, in main\r\n'

I+06.366: Debuggee-80 stderr:
          '    '

I+06.366: Debuggee-80 stderr:
          "globals = debugger.run(setup['file'], None, None, is_module)"

I+06.366: Debuggee-80 stderr:
          '\r\n'

I+06.366: Debuggee-80 stderr:
          '  File "d:\\a\\1\\s\\.tox\\py27\\lib\\site-packages\\ptvsd\\_vendored\\pydevd\\pydevd.py", line 2174, in run\r\n'

I+06.372: Debuggee-80 stderr:
          '    '

I+06.373: Debuggee-80 stderr:
          'return self._exec(is_module, entry_point_fn, module_name, file, globals, locals)'

I+06.373: Debuggee-80 stderr:
          '\r\n'

I+06.373: Debuggee-80 stderr:
          '  File "d:\\a\\1\\s\\.tox\\py27\\lib\\site-packages\\ptvsd\\_vendored\\pydevd\\pydevd.py", line 2181, in _exec\r\n'

I+06.376: Debuggee-80 stderr:
          '    '

I+06.376: Debuggee-80 stderr:
          'pydev_imports.execfile(file, globals, locals)  # execute the script\r\n'

I+06.376: Debuggee-80 stderr:
          'IOErro'

I+06.376: Debuggee-80 stderr:
          'r'

I+06.376: Debuggee-80 stderr:
          ': '

I+06.376: Debuggee-80 stderr:
          "[Errno 2] No such file or directory: 'D:\\\\a\\\\1\\\\s\\\\tests\\\\test_data\\\\flask1\\\\-m'"

I+06.376: Debuggee-80 stderr:
          '\r\n'
```
And here's the corresponding part of pydevd log for the parent process, where it patches the arguments before the subprocess gets spawned:
```
File not traced (not in project - force_check_project_scope): c:\hostedtoolcache\windows\python\2.7.16\x64\lib\subprocess.py
File traced: <werkzeug routing>
Patching args: [u'd:\\a\\1\\s\\.tox\\py27\\scripts\\python.exe', u'D:\\a\\1\\s\\tests\\test_data\\flask1\\-m', u'flask', u'run', u'--no-debugger', u'--with-threads', u'--reload', u'--port', u'7006']
File not traced (not in project - force_check_project_scope): d:\a\1\s\.tox\py27\lib\site-packages\jinja2\ext.py
sending cmd -->      CMD_THREAD_KILL {"type": "event", "event": "thread", "pydevd_cmd_id": 104, "seq": 32, "body": {"threadId": 2, "reason": "exited"}}

New args: d:\a\1\s\.tox\py27\scripts\python.exe d:\a\1\s\.tox\py27\lib\site-packages\ptvsd\_vendored\pydevd\pydevd.py --port 4511 --client 127.0.0.1 --multiprocess --json-dap-http --file D:\a\1\s\tests\test_data\flask1\-m flask run --no-debugger --with-threads --reload --port 7006
File not traced (not in project - force_check_project_scope): c:\hostedtoolcache\windows\python\2.7.16\x64\lib\traceback.py
sending cmd -->      CMD_THREAD_KILL {"type": "event", "event": "thread", "pydevd_cmd_id": 104, "seq": 34, "body": {"threadId": 1, "reason": "exited"}}
```
Note that the mangled `-m` argument was already there _before_ pydevd did anything - it came from Flask. So, it looks like something that ptvsd did to `sys.argv` caused Flask to misinterpret it like that. 


Resolving patch:
diff --git a/src/ptvsd/server/cli.py b/src/ptvsd/server/cli.py
--- a/src/ptvsd/server/cli.py
+++ b/src/ptvsd/server/cli.py
@@ -337,7 +337,7 @@ def attach_to_pid():
 
 
 def main():
-    original_argv = sys.argv
+    original_argv = list(sys.argv)
     try:
         sys.argv[1:] = parse(sys.argv[1:])
     except Exception as ex:


Test patch:
diff --git a/tests/debug/session.py b/tests/debug/session.py
--- a/tests/debug/session.py
+++ b/tests/debug/session.py
@@ -561,8 +561,9 @@ def _request_start(self, method):
         )
 
         if start_request.response is not None:
-            # It was an immediate response - configuration is not possible. Just get
-            # the "process" event, and return to caller.
+            # It was an immediate response - either the request failed, or there is
+            # no configuration stage for this debug session.
+            start_request.response.result  # raise exception if failed
             return self.wait_for_process()
 
         # We got "initialized" - now we need to yield to the caller, so that it can
diff --git a/tests/net.py b/tests/net.py
--- a/tests/net.py
+++ b/tests/net.py
@@ -63,11 +63,11 @@ def wait_until_port_is_listening(port, interval=1, max_attempts=1000):
         try:
             log.info("Probing localhost:{0} (attempt {1})...", port, i)
             sock.connect(("localhost", port))
-        except socket.error:
+        except socket.error as exc:
             # The first attempt will almost always fail, because the port isn't
             # open yet. But if it keeps failing after that, we want to know why.
             if i > 1:
-                log.exception()
+                log.warning("Failed to connect to localhost:{0}:\n{1}", port, exc)
             time.sleep(interval)
         else:
             log.info("localhost:{0} is listening - server is up!", port)
diff --git a/tests/test_data/flask1/main.py b/tests/test_data/flask1/main.py
--- a/tests/test_data/flask1/main.py
+++ b/tests/test_data/flask1/main.py
@@ -5,4 +5,4 @@
 import debug_me  # noqa
 import runpy
 
-runpy.run_module("flask", run_name="__main__")
+runpy.run_module("flask", run_name="__main__", alter_sys=True)
diff --git a/tests/timeline.py b/tests/timeline.py
--- a/tests/timeline.py
+++ b/tests/timeline.py
@@ -1104,6 +1104,10 @@ def __init__(self, request_occ, message):
     def body(self):
         return self.message.body
 
+    @property
+    def result(self):
+        return self.message.result
+
     @property
     def success(self):
         return self.message.success