Repository: microsoft/ptvsd

Issue:
Attach to process: Support pid sent as a string from the extension
For https://github.com/microsoft/vscode-python/issues/8701: Because the pid retrieved from the quick pick is stored in an action variable, it is returned in the debug configuration as a string (see [here](https://github.com/Microsoft/vscode/issues/6569#issuecomment-223068492)). 

Overwriting it with an int didn't work, so it is being passed to ptvsd as a string, even though I initially thought it would be possible in the spike (https://github.com/microsoft/vscode-python/issues/8909).


Discussion:

Resolving patch:
diff --git a/src/ptvsd/adapter/ide.py b/src/ptvsd/adapter/ide.py
--- a/src/ptvsd/adapter/ide.py
+++ b/src/ptvsd/adapter/ide.py
@@ -166,7 +166,7 @@ def handle(self, request):
 
             self.session.no_debug = request("noDebug", json.default(False))
             if self.session.no_debug:
-                servers.dont_expect_connections()
+                servers.dont_wait_for_first_connection()
 
             self.session.debug_options = debug_options = set(
                 request("debugOptions", json.array(unicode))
@@ -304,13 +304,18 @@ def attach_request(self, request):
         # will be the one for the root debuggee process, but if it has exited already,
         # it will be some subprocess.
 
-        pid = request("processId", int, optional=True)
+        pid = request("processId", (int, unicode), optional=True)
         sub_pid = request("subProcessId", int, optional=True)
         if pid != ():
             if sub_pid != ():
                 raise request.isnt_valid(
                     '"processId" and "subProcessId" are mutually exclusive'
                 )
+            if not isinstance(pid, int):
+                try:
+                    pid = int(pid)
+                except Exception:
+                    raise request.isnt_valid('"processId" must be parseable as int')
             ptvsd_args = request("ptvsdArgs", json.array(unicode))
             servers.inject(pid, ptvsd_args)
             timeout = 10
diff --git a/src/ptvsd/adapter/servers.py b/src/ptvsd/adapter/servers.py
--- a/src/ptvsd/adapter/servers.py
+++ b/src/ptvsd/adapter/servers.py
@@ -43,6 +43,8 @@ class Connection(sockets.ClientConnection):
     def __init__(self, sock):
         from ptvsd.adapter import sessions
 
+        self.disconnected = False
+
         self.server = None
         """The Server component, if this debug server belongs to Session.
         """
@@ -101,6 +103,13 @@ def __init__(self, sock):
                 log.exception("Failed to inject ptvsd into {0}:", self, level="warning")
 
             with _lock:
+                # The server can disconnect concurrently before we get here, e.g. if
+                # it was force-killed. If the disconnect() handler has already run,
+                # don't register this server or report it, since there's nothing to
+                # deregister it.
+                if self.disconnected:
+                    return
+
                 if any(conn.pid == self.pid for conn in _connections):
                     raise KeyError(
                         fmt("{0} is already connected to this adapter", self)
@@ -110,10 +119,15 @@ def __init__(self, sock):
 
         except Exception:
             log.exception("Failed to accept incoming server connection:")
+            self.channel.close()
+
+            # If this was the first server to connect, and the main thread is inside
+            # wait_until_disconnected(), we want to unblock it and allow it to exit.
+            dont_wait_for_first_connection()
+
             # If we couldn't retrieve all the necessary info from the debug server,
             # or there's a PID clash, we don't want to track this debuggee anymore,
             # but we want to continue accepting connections.
-            self.channel.close()
             return
 
         parent_session = sessions.get(self.ppid)
@@ -155,10 +169,11 @@ def terminated_event(self, event):
 
     def disconnect(self):
         with _lock:
-            # If the disconnect happened while Server was being instantiated, we need
-            # to tell it, so that it can clean up properly via Session.finalize(). It
-            # will also take care of deregistering the connection in that case.
+            self.disconnected = True
             if self.server is not None:
+                # If the disconnect happened while Server was being instantiated,
+                # we need to tell it, so that it can clean up via Session.finalize().
+                # It will also take care of deregistering the connection in that case.
                 self.server.disconnect()
             elif self in _connections:
                 _connections.remove(self)
@@ -391,7 +406,7 @@ def wait_until_disconnected():
                 return
 
 
-def dont_expect_connections():
+def dont_wait_for_first_connection():
     """Unblocks any pending wait_until_disconnected() call that is waiting on the
     first server to connect.
     """
diff --git a/src/ptvsd/adapter/sessions.py b/src/ptvsd/adapter/sessions.py
--- a/src/ptvsd/adapter/sessions.py
+++ b/src/ptvsd/adapter/sessions.py
@@ -212,6 +212,11 @@ def finalize(self, why, terminate_debuggee=None):
         log.info("{0} finalized.", self)
 
     def _finalize(self, why, terminate_debuggee):
+        # If the IDE started a session, and then disconnected before issuing "launch"
+        # or "attach", the main thread will be blocked waiting for the first server
+        # connection to come in - unblock it, so that we can exit.
+        servers.dont_wait_for_first_connection()
+
         if self.server:
             if self.server.is_connected:
                 try:


Test patch:
diff --git a/tests/debug/session.py b/tests/debug/session.py
--- a/tests/debug/session.py
+++ b/tests/debug/session.py
@@ -9,6 +9,7 @@
 import os
 import psutil
 import py
+import pytest
 import subprocess
 import sys
 import time
@@ -142,6 +143,11 @@ def __init__(self, debug_config=None):
         )
         """The debug configuration for this session."""
 
+        self.before_request = lambda command, arguments: None
+        """Invoked for every outgoing request in this session, allowing any final
+        tweaks to the request before it is sent.
+        """
+
         self.log_dir = (
             None
             if log.log_dir is None
@@ -208,6 +214,8 @@ def __enter__(self):
         return self
 
     def __exit__(self, exc_type, exc_val, exc_tb):
+        log.info("Ending {0}.", self)
+
         if self.timeline.is_frozen:
             self.timeline.unfreeze()
 
@@ -232,11 +240,13 @@ def __exit__(self, exc_type, exc_val, exc_tb):
             # all the processes immediately. Don't close or finalize the timeline,
             # either, since it'll likely have unobserved events in it.
             if self.adapter is not None:
+                log.info("Killing {0}.", self.adapter_id)
                 try:
                     self.adapter.kill()
                 except Exception:
                     pass
             if self.debuggee is not None:
+                log.info("Killing {0}.", self.debuggee_id)
                 try:
                     self.debuggee.kill()
                 except Exception:
@@ -437,6 +447,12 @@ def request(self, *args, **kwargs):
         )
 
     def send_request(self, command, arguments=None, proceed=True):
+        if command == "attach":
+            if sys.version_info[:2] == (3, 6) and sys.platform == "darwin":
+                pytest.skip("https://github.com/microsoft/ptvsd/issues/1967")
+
+        self.before_request(command, arguments)
+
         if self.timeline.is_frozen and proceed:
             self.proceed()
 
@@ -757,14 +773,13 @@ def wait_for_disconnect(self):
 
     def wait_for_exit(self):
         if self.debuggee is not None:
+            log.info("Waiting for {0} to exit ...", self.debuggee_id)
             try:
                 self.debuggee.wait()
             except Exception:
                 pass
             finally:
                 watchdog.unregister_spawn(self.debuggee.pid, self.debuggee_id)
-            # if self.captured_output:
-            #     self.captured_output.wait()
 
         self.timeline.wait_until_realized(timeline.Event("terminated"))
 
diff --git a/tests/ptvsd/server/test_attach.py b/tests/ptvsd/server/test_attach.py
--- a/tests/ptvsd/server/test_attach.py
+++ b/tests/ptvsd/server/test_attach.py
@@ -130,7 +130,8 @@ def code_to_debug():
         session2.request_continue()
 
 
-def test_attach_by_pid(pyfile, target):
+@pytest.mark.parametrize("pid_type", ["int", "str"])
+def test_attach_by_pid(pyfile, target, pid_type):
     @pyfile
     def code_to_debug():
         import debug_me  # noqa
@@ -147,6 +148,14 @@ def do_something(i):
                 break
 
     with debug.Session() as session:
+
+        def before_request(command, arguments):
+            if command == "attach":
+                assert isinstance(arguments["processId"], int)
+                if pid_type == "str":
+                    arguments["processId"] = str(arguments["processId"])
+
+        session.before_request = before_request
         session.config["redirectOutput"] = True
 
         with session.attach_by_pid(target(code_to_debug), wait=False):
diff --git a/tests/ptvsd/server/test_exception.py b/tests/ptvsd/server/test_exception.py
--- a/tests/ptvsd/server/test_exception.py
+++ b/tests/ptvsd/server/test_exception.py
@@ -5,6 +5,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import pytest
+import sys
 
 from tests import debug
 from tests.debug import runners, targets
@@ -143,6 +144,10 @@ def raise_without_except():
             session.request_continue()
 
 
+@pytest.mark.skipif(
+    sys.platform == "darwin" and sys.version_info >= (3,),
+    reason="https://github.com/microsoft/ptvsd/issues/1988",
+)
 @pytest.mark.parametrize("target", targets.all_named)
 @pytest.mark.parametrize("run", runners.all)
 @pytest.mark.parametrize("raised", ["raised", ""])
diff --git a/tests/ptvsd/server/test_exclude_rules.py b/tests/ptvsd/server/test_exclude_rules.py
--- a/tests/ptvsd/server/test_exclude_rules.py
+++ b/tests/ptvsd/server/test_exclude_rules.py
@@ -57,6 +57,8 @@ def code_to_debug():
             )
 
         # No exceptions should be seen.
+        session.wait_for_next_event("terminated")
+        session.proceed()
 
 
 @pytest.mark.parametrize("scenario", ["exclude_code_to_debug", "exclude_callback_dir"])
diff --git a/tests/ptvsd/server/test_multiproc.py b/tests/ptvsd/server/test_multiproc.py
--- a/tests/ptvsd/server/test_multiproc.py
+++ b/tests/ptvsd/server/test_multiproc.py
@@ -13,8 +13,6 @@
 from tests.debug import runners
 from tests.patterns import some
 
-pytestmark = pytest.mark.timeout(30)
-
 
 @pytest.fixture(params=[runners.launch, runners.attach_by_socket["api"]])
 def run(request):