From 79c9ab9c6ce0b6cf0356261907de1eb7e3577e1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:27:15 +0000 Subject: [PATCH 1/4] Initial plan From 4061cd99775bd6754883dca326102a74e898fbce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:02:54 +0000 Subject: [PATCH 2/4] Fix multi-thread resume when in-process debug adapter is used Per the DAP spec, a ContinueRequest should resume all threads unless singleThread=True is explicitly set. Previously, only the out-of-process adapter path worked correctly (it transformed threadId to '*' before forwarding to pydevd). With the in-process adapter, the specific threadId reached pydevd directly but was only used to resume that one thread. Fix on_continue_request to set thread_id='*' whenever singleThread is not True, regardless of multi_threads_single_notification. Also update write_continue test helper and add a regression test. Fixes: #2009 Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- .../pydevd_process_net_command_json.py | 8 +++- .../_debugger_case_multi_threads_continue.py | 25 ++++++++++ .../pydevd/tests_python/test_debugger_json.py | 48 ++++++++++++++++--- 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_multi_threads_continue.py diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 8070b549..253e3ffd 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -559,7 +559,13 @@ def on_continue_request(self, py_db, request): """ arguments = request.arguments # : :type arguments: ContinueArguments thread_id = arguments.threadId - if py_db.multi_threads_single_notification: + + # Per the DAP spec, the continue request resumes execution of all threads + # unless singleThread is explicitly true (and the capability + # supportsSingleThreadExecutionRequests is advertised). Only use the + # specific threadId when singleThread is set; otherwise resume all. + single_thread = arguments.singleThread + if not single_thread or py_db.multi_threads_single_notification: thread_id = "*" def on_resumed(): diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_multi_threads_continue.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_multi_threads_continue.py new file mode 100644 index 00000000..b07ad614 --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_multi_threads_continue.py @@ -0,0 +1,25 @@ +""" +Test for verifying that continuing from a breakpoint resumes all threads, +not just the thread that hit the breakpoint. + +When a specific threadId is sent in the ContinueRequest without singleThread=True, +all threads should be resumed per the DAP spec. +""" +import threading + +stop_event = threading.Event() + + +def thread_func(): + stop_event.wait() # Thread 2 line - wait until signaled + print("Thread finished") + + +if __name__ == "__main__": + t = threading.Thread(target=thread_func) + t.start() + + stop_event.set() # Break here - breakpoint on this line + + t.join() + print("TEST SUCEEDED!") # end diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index d1b85481..5f788711 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -390,12 +390,16 @@ def _by_type(self, *msgs): ret[msg.__class__] = msg return ret - def write_continue(self, wait_for_response=True, thread_id="*"): - continue_request = self.write_request(pydevd_schema.ContinueRequest(pydevd_schema.ContinueArguments(threadId=thread_id))) + def write_continue(self, wait_for_response=True, thread_id="*", single_thread=False): + arguments = pydevd_schema.ContinueArguments(threadId=thread_id) + if single_thread: + arguments.singleThread = True + continue_request = self.write_request(pydevd_schema.ContinueRequest(arguments)) if wait_for_response: - if thread_id != "*": - # event, response may be sent in any order + if single_thread: + # When singleThread=True, only the specified thread resumes. + # ContinuedEvent and ContinueResponse may arrive in any order. msg1 = self.wait_for_json_message((ContinuedEvent, ContinueResponse)) msg2 = self.wait_for_json_message((ContinuedEvent, ContinueResponse)) by_type = self._by_type(msg1, msg2) @@ -406,8 +410,10 @@ def write_continue(self, wait_for_response=True, thread_id="*"): assert continued_ev.body.allThreadsContinued == False assert continue_response.body.allThreadsContinued == False else: - # The continued event is received before the response. - self.wait_for_continued_event(all_threads_continued=True) + # Default: all threads resume regardless of the threadId sent. + # Per the DAP spec, singleThread must be explicitly True to + # resume only one thread. Wait for the continue response with + # allThreadsContinued=True. continue_response = self.wait_for_response(continue_request) assert continue_response.body.allThreadsContinued @@ -800,6 +806,9 @@ def test_case_json_suspend_notification(case_setup_dap): json_facade.write_make_initial_run() json_hit = json_facade.wait_for_thread_stopped(line=break1_line) + # Per the DAP spec, a ContinueRequest without singleThread=True must resume + # all threads even when a specific threadId is provided. Verify the response + # has allThreadsContinued=True (the correct behavior). json_facade.write_continue(thread_id=json_hit.thread_id) json_hit = json_facade.wait_for_thread_stopped(line=break1_line) @@ -808,6 +817,33 @@ def test_case_json_suspend_notification(case_setup_dap): writer.finished_ok = True +def test_case_json_continue_all_threads(case_setup_dap): + """Regression test: ContinueRequest with a specific threadId (no singleThread=True) + must resume ALL threads, not just the requested one. This tests the fix for the + in-process debug adapter scenario where the adapter does not transform threadId to '*'. + """ + with case_setup_dap.test_file("_debugger_case_multi_threads_continue.py") as writer: + json_facade = JsonFacade(writer) + # Simulate the in-process adapter: disable single notification mode. + # In this mode the server receives a specific threadId (not '*') directly + # from the client without the adapter transforming it. + json_facade.writer.write_multi_threads_single_notification(False) + break_line = writer.get_line_index_with_content("Break here") + json_facade.write_launch() + json_facade.write_set_breakpoints(break_line) + json_facade.write_make_initial_run() + + # Wait for the breakpoint to be hit. + json_hit = json_facade.wait_for_thread_stopped(line=break_line) + + # Send ContinueRequest with the specific thread's id (no singleThread=True). + # Per the DAP spec this must resume ALL threads, not just the specified one. + # The response must have allThreadsContinued=True. + json_facade.write_continue(thread_id=json_hit.thread_id) + + writer.finished_ok = True + + def test_case_handled_exception_no_break_on_generator(case_setup_dap): with case_setup_dap.test_file("_debugger_case_ignore_exceptions.py") as writer: json_facade = JsonFacade(writer) From 155380188ee3617b7b946b09074488297d6a9d87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:40:15 +0000 Subject: [PATCH 3/4] Use getattr for safe singleThread attribute access in on_continue_request Most DAP clients omit the optional singleThread field entirely. Using getattr(arguments, 'singleThread', False) is more defensive than direct attribute access, guarding against any non-standard arguments objects. Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- .../pydevd/_pydevd_bundle/pydevd_process_net_command_json.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 253e3ffd..1fe207c2 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -564,7 +564,9 @@ def on_continue_request(self, py_db, request): # unless singleThread is explicitly true (and the capability # supportsSingleThreadExecutionRequests is advertised). Only use the # specific threadId when singleThread is set; otherwise resume all. - single_thread = arguments.singleThread + # Use getattr with a default of False since most DAP clients omit this + # optional field entirely. + single_thread = getattr(arguments, "singleThread", False) if not single_thread or py_db.multi_threads_single_notification: thread_id = "*" From 5afe4ad6b62ae5f4abcded9c3c1c3e40afe238d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:43:14 +0000 Subject: [PATCH 4/4] Add timeout failure comment in test; keep intentional TEST SUCEEDED convention The 'TEST SUCEEDED' misspelling is an intentional convention in the pydevd test framework (debugger_unittest.py checks stdout for this exact string). Revert the resource file to preserve the convention while still adding the explanatory comment requested in the test method. Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- .../_vendored/pydevd/tests_python/test_debugger_json.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index 5f788711..2763c0eb 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -839,6 +839,9 @@ def test_case_json_continue_all_threads(case_setup_dap): # Send ContinueRequest with the specific thread's id (no singleThread=True). # Per the DAP spec this must resume ALL threads, not just the specified one. # The response must have allThreadsContinued=True. + # NOTE: If the fix regresses, the secondary thread stays blocked on + # stop_event.wait() and the debuggee hangs on t.join(), causing a test + # timeout rather than an explicit assertion failure. json_facade.write_continue(thread_id=json_hit.thread_id) writer.finished_ok = True