Repository: microsoft/ptvsd

Issue:
DAP: process creation event should come from pydevd



Discussion:
I'll take a look at this.

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py
@@ -83,6 +83,7 @@
 CMD_SET_PROJECT_ROOTS = 202
 
 CMD_MODULE_EVENT = 203
+CMD_PROCESS_EVENT = 204
 
 CMD_VERSION = 501
 CMD_RETURN = 502
@@ -150,7 +151,7 @@
     '147': 'CMD_INPUT_REQUESTED',
     '148': 'CMD_GET_DESCRIPTION',
 
-    '149': 'CMD_PROCESS_CREATED',
+    '149': 'CMD_PROCESS_CREATED',  # Note: this is actually a notification of a sub-process created.
     '150': 'CMD_SHOW_CYTHON_WARNING',
     '151': 'CMD_LOAD_FULL_VALUE',
     '152': 'CMD_GET_THREAD_STACK',
@@ -168,6 +169,7 @@
     '201': 'CMD_GET_NEXT_STATEMENT_TARGETS',
     '202': 'CMD_SET_PROJECT_ROOTS',
     '203': 'CMD_MODULE_EVENT',
+    '204': 'CMD_PROCESS_EVENT',  # DAP process event.
 
     '501': 'CMD_VERSION',
     '502': 'CMD_RETURN',
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
@@ -9,12 +9,14 @@
 from _pydevd_bundle._debug_adapter import pydevd_base_schema
 from _pydevd_bundle._debug_adapter.pydevd_schema import (SourceBreakpoint, ScopesResponseBody, Scope,
     VariablesResponseBody, SetVariableResponseBody, ModulesResponseBody, SourceResponseBody,
-    GotoTargetsResponseBody, ExceptionOptions, SetExpressionResponseBody, EvaluateResponseBody)
+    GotoTargetsResponseBody, ExceptionOptions, SetExpressionResponseBody, EvaluateResponseBody,
+    ProcessEventBody, ProcessEvent)
 from _pydevd_bundle._debug_adapter.pydevd_schema import CompletionsResponseBody
 from _pydevd_bundle.pydevd_api import PyDevdAPI
 from _pydevd_bundle.pydevd_comm_constants import (
     CMD_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_OVER, CMD_STEP_INTO_MY_CODE,
-    CMD_STEP_INTO, CMD_STEP_RETURN_MY_CODE, CMD_STEP_RETURN, CMD_SET_NEXT_STATEMENT)
+    CMD_STEP_INTO, CMD_STEP_RETURN_MY_CODE, CMD_STEP_RETURN, CMD_SET_NEXT_STATEMENT,
+    CMD_PROCESS_EVENT)
 from _pydevd_bundle.pydevd_filtering import ExcludeFilter
 from _pydevd_bundle.pydevd_json_debug_options import _extract_debug_options
 from _pydevd_bundle.pydevd_net_command import NetCommand
@@ -22,6 +24,7 @@
 import pydevd_file_utils
 from _pydev_bundle import pydev_log
 from _pydevd_bundle.pydevd_constants import DebugInfoHolder
+import sys
 
 try:
     import dis
@@ -145,6 +148,7 @@ def __init__(self, from_json):
         self._debug_options = {}
         self._next_breakpoint_id = partial(next, itertools.count(0))
         self._goto_targets_map = IDMap()
+        self._launch_or_attach_request_done = False
 
     def process_net_command_json(self, py_db, json_contents):
         '''
@@ -200,6 +204,9 @@ def on_configurationdone_request(self, py_db, request):
         '''
         :param ConfigurationDoneRequest request:
         '''
+        if not self._launch_or_attach_request_done:
+            pydev_log.critical('Missing launch request or attach request before configuration done request.')
+
         self.api.run(py_db)
         self.api.notify_configuration_done(py_db)
 
@@ -292,23 +299,39 @@ def _set_debug_options(self, py_db, args, start_reason):
         if self._debug_options.get('STOP_ON_ENTRY', False) and start_reason == 'launch':
             self.api.stop_on_entry()
 
+    def _send_process_event(self, py_db, start_method):
+        if len(sys.argv) > 0:
+            name = sys.argv[0]
+        else:
+            name = ''
+        body = ProcessEventBody(
+            name=name,
+            systemProcessId=os.getpid(),
+            isLocalProcess=True,
+            startMethod=start_method,
+        )
+        event = ProcessEvent(body)
+        py_db.writer.add_command(NetCommand(CMD_PROCESS_EVENT, 0, event, is_json=True))
+
+    def _handle_launch_or_attach_request(self, py_db, request, start_reason):
+        self._send_process_event(py_db, start_reason)
+        self._launch_or_attach_request_done = True
+        self.api.set_enable_thread_notifications(py_db, True)
+        self._set_debug_options(py_db, request.arguments.kwargs, start_reason=start_reason)
+        response = pydevd_base_schema.build_response(request)
+        return NetCommand(CMD_RETURN, 0, response, is_json=True)
+
     def on_launch_request(self, py_db, request):
         '''
         :param LaunchRequest request:
         '''
-        self.api.set_enable_thread_notifications(py_db, True)
-        self._set_debug_options(py_db, request.arguments.kwargs, start_reason='launch')
-        response = pydevd_base_schema.build_response(request)
-        return NetCommand(CMD_RETURN, 0, response, is_json=True)
+        return self._handle_launch_or_attach_request(py_db, request, start_reason='launch')
 
     def on_attach_request(self, py_db, request):
         '''
         :param AttachRequest request:
         '''
-        self.api.set_enable_thread_notifications(py_db, True)
-        self._set_debug_options(py_db, request.arguments.kwargs, start_reason='attach')
-        response = pydevd_base_schema.build_response(request)
-        return NetCommand(CMD_RETURN, 0, response, is_json=True)
+        return self._handle_launch_or_attach_request(py_db, request, start_reason='attach')
 
     def on_pause_request(self, py_db, request):
         '''
@@ -426,6 +449,7 @@ def on_disconnect_request(self, py_db, request):
         '''
         :param DisconnectRequest request:
         '''
+        self._launch_or_attach_request_done = False
         self.api.request_disconnect(py_db, resume_threads=True)
 
         response = pydevd_base_schema.build_response(request)
diff --git a/src/ptvsd/wrapper.py b/src/ptvsd/wrapper.py
--- a/src/ptvsd/wrapper.py
+++ b/src/ptvsd/wrapper.py
@@ -1040,7 +1040,7 @@ def __init__(
         self.event_loop_thread = None
 
         # debugger state
-        self.is_process_created = False
+        self.is_process_created = False  # Note: is_process_created kept as it's used on_pause()
         self.is_process_created_lock = threading.Lock()
         self._success_exitcodes = []
         self.internals_filter = InternalsFilter()
@@ -1187,7 +1187,6 @@ def on_configurationDone(self, request, args):
         with self.is_process_created_lock:
             if not self.is_process_created:
                 self.is_process_created = True
-                self.send_process_event(self.start_reason)
 
         self._notify_ready()
         self.send_response(request)
@@ -1290,16 +1289,6 @@ def _clear_output_redirection(self):
     def _resume_all_threads(self):
         self.pydevd_notify(pydevd_comm.CMD_THREAD_RUN, '*')
 
-    def send_process_event(self, start_method):
-        # TODO: docstring
-        evt = {
-            'name': sys.argv[0],
-            'systemProcessId': os.getpid(),
-            'isLocalProcess': True,
-            'startMethod': start_method,
-        }
-        self.send_event('process', **evt)
-
     @async_handler
     def _forward_request_to_pydevd(self, request, args, send_response=True):
         pydevd_request = copy.deepcopy(request)
@@ -1571,13 +1560,17 @@ def on_pydevd_thread_create(self, seq, args):
                 if not debugger_attached.isSet():
                     return
                 self.is_process_created = True
-                self.send_process_event(self.start_reason)
 
     @pydevd_events.handler(pydevd_comm.CMD_THREAD_KILL)
     def on_pydevd_thread_kill(self, seq, args):
         tid = args['body']['threadId']
         self.send_event('thread', reason='exited', threadId=tid)
 
+    @pydevd_events.handler(pydevd_comm.CMD_PROCESS_EVENT)
+    def on_pydevd_process_event(self, seq, args):
+        body = args['body']
+        self.send_event('process', **body)
+
     @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION)
     def on_pydevd_thread_suspend_single_notification(self, seq, args):
         # NOTE: We should add the thread to VSC thread map only if the


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -5,7 +5,7 @@
 from _pydevd_bundle._debug_adapter.pydevd_base_schema import from_json
 from tests_python.debugger_unittest import IS_JYTHON, IS_APPVEYOR, overrides
 from _pydevd_bundle._debug_adapter.pydevd_schema import (ThreadEvent, ModuleEvent, OutputEvent,
-    ExceptionOptions, Response, StoppedEvent, ContinuedEvent)
+    ExceptionOptions, Response, StoppedEvent, ContinuedEvent, ProcessEvent)
 from tests_python import debugger_unittest
 import json
 from collections import namedtuple
@@ -43,8 +43,9 @@ def __init__(self, writer):
         writer.write_set_protocol('http_json')
         writer.write_multi_threads_single_notification(True)
         self._all_json_messages_found = []
+        self._sent_launch_or_attach = False
 
-    def mark_messages(self, expected_class, accept_message):
+    def mark_messages(self, expected_class, accept_message=lambda obj:True):
         ret = []
         for message_with_mark in self._all_json_messages_found:
             if not message_with_mark.marked:
@@ -95,6 +96,9 @@ def write_request(self, request):
         return request
 
     def write_make_initial_run(self):
+        if not self._sent_launch_or_attach:
+            self.write_launch()
+
         configuration_done_request = self.write_request(pydevd_schema.ConfigurationDoneRequest())
         return self.wait_for_response(configuration_done_request)
 
@@ -178,11 +182,19 @@ def write_set_exception_breakpoints(self, filters=None, exception_options=None):
         assert response.success
 
     def write_launch(self, **arguments):
+        self._sent_launch_or_attach = True
         arguments['noDebug'] = False
         request = {'type': 'request', 'command': 'launch', 'arguments': arguments, 'seq':-1}
         self.wait_for_response(self.write_request(request))
 
+    def write_attach(self, **arguments):
+        self._sent_launch_or_attach = True
+        arguments['noDebug'] = False
+        request = {'type': 'request', 'command': 'attach', 'arguments': arguments, 'seq':-1}
+        self.wait_for_response(self.write_request(request))
+
     def write_disconnect(self, wait_for_response=True):
+        self._sent_launch_or_attach = False
         arguments = pydevd_schema.DisconnectArguments(terminateDebuggee=False)
         request = pydevd_schema.DisconnectRequest(arguments)
         self.write_request(request)
@@ -360,6 +372,16 @@ def test_case_json_logpoints(case_setup):
         writer.finished_ok = True
 
 
+def test_case_process_event(case_setup):
+    with case_setup.test_file('_debugger_case_change_breaks.py') as writer:
+        json_facade = JsonFacade(writer)
+
+        json_facade.write_launch()
+        assert len(json_facade.mark_messages(ProcessEvent)) == 1
+        json_facade.write_make_initial_run()
+        writer.finished_ok = True
+
+
 def test_case_json_change_breaks(case_setup):
     with case_setup.test_file('_debugger_case_change_breaks.py') as writer:
         json_facade = JsonFacade(writer)
@@ -765,8 +787,8 @@ def test_return_value(case_setup):
         json_facade = JsonFacade(writer)
 
         break_line = writer.get_line_index_with_content('break here')
-        writer.write_add_breakpoint(break_line)
-        writer.write_show_return_vars()
+        json_facade.write_set_breakpoints(break_line)
+        json_facade.write_launch(debugOptions=['ShowReturnValue'])
         json_facade.write_make_initial_run()
 
         json_hit = json_facade.wait_for_thread_stopped()
@@ -1409,9 +1431,11 @@ def test_stepping(case_setup):
     with case_setup.test_file('_debugger_case_stepping.py') as writer:
         json_facade = JsonFacade(writer)
 
-        writer.write_add_breakpoint(writer.get_line_index_with_content('Break here 1'))
-        writer.write_add_breakpoint(writer.get_line_index_with_content('Break here 2'))
-
+        json_facade.write_set_breakpoints([
+            writer.get_line_index_with_content('Break here 1'),
+            writer.get_line_index_with_content('Break here 2')
+        ])
+        json_facade.write_launch(debugOptions=['DebugStdLib'])
         json_facade.write_make_initial_run()
 
         json_hit = json_facade.wait_for_thread_stopped()
@@ -1761,6 +1785,21 @@ def check_thread_events(json_facade):
         started_events = json_facade.mark_messages(ThreadEvent, lambda x: x.body.reason == 'started')
         assert len(started_events) == 1
 
+    def check_process_event(json_facade, start_method):
+        if start_method == 'attach':
+            json_facade.write_attach()
+
+        elif start_method == 'launch':
+            json_facade.write_launch()
+
+        else:
+            raise AssertionError('Unexpected: %s' % (start_method,))
+
+        process_events = json_facade.mark_messages(ProcessEvent)
+        assert len(process_events) == 1
+        assert next(iter(process_events)).body.startMethod == start_method
+        json_facade.write_make_initial_run()
+
     with case_setup_remote_attach_to.test_file('_debugger_case_wait_for_attach.py', host_port[1]) as writer:
         time.sleep(.5)  # Give some time for it to pass the first breakpoint and wait in 'wait_for_attach'.
         writer.start_socket_client(*host_port)
@@ -1776,7 +1815,7 @@ def check_thread_events(json_facade):
         pause2_line = writer.get_line_index_with_content('Pause 2')
 
         json_facade.write_set_breakpoints([break1_line, break2_line, break3_line])
-        json_facade.write_make_initial_run()
+        check_process_event(json_facade, start_method='launch')
         json_facade.wait_for_thread_stopped(line=break2_line)
 
         # Upon disconnect, all threads should be running again.
@@ -1787,7 +1826,7 @@ def check_thread_events(json_facade):
         json_facade = JsonFacade(writer)
         check_thread_events(json_facade)
         json_facade.write_set_breakpoints([break1_line, break2_line, break3_line])
-        json_facade.write_make_initial_run()
+        check_process_event(json_facade, start_method='attach')
         json_facade.wait_for_thread_stopped(line=break3_line)
 
         # Upon disconnect, all threads should be running again.
@@ -1797,11 +1836,13 @@ def check_thread_events(json_facade):
         writer.start_socket_client(*host_port)
         json_facade = JsonFacade(writer)
         check_thread_events(json_facade)
+        check_process_event(json_facade, start_method='attach')
 
         # Connect back without a disconnect (auto-disconnects previous and connects new client).
         writer.start_socket_client(*host_port)
         json_facade = JsonFacade(writer)
         check_thread_events(json_facade)
+        check_process_event(json_facade, start_method='attach')
 
         json_facade.write_pause()
         json_hit = json_facade.wait_for_thread_stopped(reason='pause', line=[pause1_line, pause2_line])
@@ -1900,6 +1941,7 @@ def get_environ(writer):
         json_facade = JsonFacade(writer)
 
         writer.write_add_breakpoint(writer.get_line_index_with_content('breakpoint'))
+        json_facade.write_launch(debugOptions=['DebugStdLib'])
         json_facade.write_make_initial_run()
 
         # First hit is for breakpoint reached via a stack frame that doesn't have source.
@@ -2027,7 +2069,7 @@ def test_case_flask_exceptions(case_setup_flask, jmc):
                     {'names': ['IndexError']},
                 ])
             ])
-        writer.write_make_initial_run()
+        json_facade.write_make_initial_run()
 
         t = writer.create_request_thread('/bad_template')
         time.sleep(2)  # Give flask some time to get to startup before requesting the page
diff --git a/tests/helpers/session.py b/tests/helpers/session.py
--- a/tests/helpers/session.py
+++ b/tests/helpers/session.py
@@ -517,9 +517,21 @@ def handshake(self):
         })
         if self.success_exitcodes is not None:
             self.start_method_args['successExitCodes'] = self.success_exitcodes
-        self.send_request(request, self.start_method_args).wait_for_response()
+        launch_or_attach_request = self.send_request(request, self.start_method_args)
+
+        if self.no_debug:
+            launch_or_attach_request.wait_for_response()
+        else:
+            self.wait_for_next(Event('process') & Response(launch_or_attach_request))
+
+            # 'process' is expected right after 'launch' or 'attach'.
+            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 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.
             # Note: it's actually possible that the 'thread' event was sent before the 'threads'
@@ -537,20 +549,7 @@ def start_debugging(self, freeze=True):
 
         configurationDone_request = self.send_request('configurationDone')
 
-        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,
-            }))
+        start = self.wait_for_next(Response(configurationDone_request))
 
         if not freeze:
             self.proceed()