Repository: microsoft/ptvsd

Issue:
PTVSD should terminate itself when disconnect request is sent
Over the past few weeks I've discussed this issue on different occasions. The problem is the built-in debug adapter in VS automatically handles termination/disconnecting, as for VSC I need to handcraft this. I.e. implement some workarounds to get around this. 

The current VSC solution works, but if we can fix this in PTVSD, then we should (reduce code and work arounds).

Currently in VSC it's up to the debug adapter to terminate the python process when VSC sends the `disconnect` request.

**Two issues:**
* Killing the process results in the display of the message `Terminated: 15` in the terminal window. 
This is unavoidable, displayed by OS when process is terminated forcefully.
* Debug adapter needs to manually send the `Terminated` event without a sequence number (yes it seems to work, but no idea what VSC would do in the future. Yes the debug adapter can get the right sequence number, still too much work & unnecessary)

I.e. Ideally PTVSD should kill itself when it receives the `disconnect` request (**i.e. it should handle all protocol messages**)

**Solution:**
* Upon receiving the `disconnect` request, PTVSD will send the `exited` and `terminated` event
* Then kill itself
*  **Clean, simple and consistent.**
(this resolves both issues mentioned above and reduces VSC debug adapter work).

Here's the sample code:
```python
    def on_disconnect(self, request, args):
        # TODO: docstring
        if self.start_reason == 'launch':
            self.send_response(request)
            self.close()
            sys.exit(0)
        else:
            self.send_response(request)
```



Discussion:
It still needs to send exited/terminated on its own when the process just runs to completion. Correct me if I'm wrong, but with the above code, won't it end up sending exited and terminated twice (once from this code, and once more from atexit handler)?
> It still needs to send exited/terminated on its own when the process just runs to completion

The `atexit` handler will and already does that.

> but with the above code, won't it end up sending exited and terminated twice

No, the solution is already in place.

```python
    def close(self):
        """Stop the message processor and release its resources."""
        if self._closed:
            return
        self._closed = True
```
If we are solving disconnect then we should also solve #120. That is a case where the disconnect request arrives after the python process has exited.
Will be submitting a PR for #120 (separate issue).
Currently testing that on Mac and Win

Resolving patch:
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -8,6 +8,7 @@
 import errno
 import os
 import platform
+import signal
 import socket
 import sys
 import threading
@@ -37,6 +38,7 @@
 #ipcjson._TRACE = ipcjson_trace
 
 ptvsd_sys_exit_code = 0
+WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT = 2
 WAIT_FOR_THREAD_FINISH_TIMEOUT = 1
 
 def unquote(s):
@@ -389,7 +391,8 @@ def __init__(self, socket, pydevd, logfile=None):
         self.next_var_ref = 0
         self.loop = futures.EventLoop()
         self.exceptions_mgr = ExceptionsManager(self)
-
+        self.disconnect_request = None
+        self.disconnect_request_event = threading.Event()
         pydevd._vscprocessor = self
         self._closed = False
 
@@ -413,6 +416,11 @@ def close(self):
         self.send_event('exited', exitCode=ptvsd_sys_exit_code)
         self.send_event('terminated')
 
+        self.disconnect_request_event.wait(WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT)
+        if self.disconnect_request is not None:
+            self.send_response(self.disconnect_request)
+            self.disconnect_request = None
+
         self.set_exit()    
         self.loop.stop()
         self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT)
@@ -509,7 +517,15 @@ def on_configurationDone(self, request, args):
 
     def on_disconnect(self, request, args):
         # TODO: docstring
-        self.send_response(request)
+        if self.start_reason == 'launch':
+            self.disconnect_request = request
+            self.disconnect_request_event.set()
+            killProcess = not self._closed
+            self.close()
+            if killProcess:
+                os.kill(os.getpid(), signal.SIGTERM)
+        else:
+            self.send_response(request)    
 
     @async_handler
     def on_attach(self, request, args):
@@ -969,6 +985,10 @@ def exit_handler(proc, server_thread):
     if server_thread.is_alive():
         server_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT)
 
+def signal_handler(signum, frame, proc):
+    proc.close()
+    sys.exit(0)
+
 def start_server(port):
     """Return a socket to a (new) local pydevd-handling daemon.
 
@@ -981,6 +1001,8 @@ def start_server(port):
     client, _ = server.accept()
     pydevd, proc, server_thread = _start(client, server)
     atexit.register(lambda: exit_handler(proc, server_thread))
+    if platform.system() != 'Windows':
+        signal.signal(signal.SIGHUP, lambda signum, frame: signal_handler(signum, frame, proc))
     return pydevd
 
 
@@ -996,6 +1018,8 @@ def start_client(host, port):
     client.connect((host, port))
     pydevd, proc, server_thread = _start(client, None)
     atexit.register(lambda: exit_handler(proc, server_thread))
+    if platform.system() != 'Windows':
+        signal.signal(signal.SIGHUP, lambda signum, frame: signal_handler(signum, frame, proc))
     return pydevd
 
 


Test patch: