Repository navigation
Restore process creation functions in pydevd.stoptrace() - #2085
Open
Yashvardhan Thanvi (Thanatos9404) wants to merge 1 commit into
Open
Yashvardhan Thanvi (Thanatos9404) wants to merge 1 commit into
Yashvardhan Thanvi (Thanatos9404) wants to merge 1 commit into
Conversation
settrace(patch_multiprocessing=True) replaces os.exec*, os.spawn*, os.posix_spawn*, os.fork, _posixsubprocess.fork_exec, subprocess._fork_exec and, on Windows, _winapi.CreateProcess, so that child Python processes connect to the debugger. stoptrace() undid the thread patches but left these in place. After it ran, every new Python child was still rewritten to call pydevd.settrace() against an adapter that no longer existed, and the child exited with ConnectionRefusedError before running its own code. A later debugpy.listen() (with listen.called reset by hand, as in the issue) timed out as well, since the adapter it launches goes through the same patched function. Add undo_patch_new_process_functions() and call it from stoptrace() next to undo_patch_thread_modules(). It puts each original function back only while pydevd's wrapper is still the installed one, so a layer added on top later (gevent's os.fork, for example) stays in place. The original_* attributes are kept, because such a layer, or any other saved reference, still calls the wrapper, which looks the original up by that name. monkey_patch_module() now tags its wrappers so that the undo can recognise them, and patches again when it finds the original restored, so a later settrace() installs the wrappers again. Fixes microsoft#2014
Yashvardhan Thanvi (Thanatos9404)
requested a review
from a team
as a code owner
October 16, 2026 04:03
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2014
With
subProcessenabled (the default),settrace()replaces the functions that start new processes so that Python children connect to the debugger:_winapi.CreateProcesson Windows,os.fork,_posixsubprocess.fork_execandsubprocess._fork_execon POSIX, andos.exec*,os.spawn*andos.posix_spawn*where they exist.pydevd.stoptrace()undoes the thread patches but not these. After it runs, every new Python child is still rewritten to callpydevd.settrace()against the adapter, which is gone by then, so the child exits withConnectionRefusedErrorbefore running any of its own code.On current
main(Windows 11, CPython 3.13.2), afterdebugpy.listen()andpydevd.stoptrace(),subprocess.Popen([sys.executable, "-c", "print('child ok')"])exits with code 1 and that traceback, with or without a client having been attached first. Resettinglisten.calledby hand, as in the issue, and callingdebugpy.listen()again fails after 30 seconds with "timed out waiting for adapter to connect", because the adapter it launches goes through the same patchedCreateProcess.This adds
undo_patch_new_process_functions()topydev_monkey.py, andstoptrace()calls it right afterundo_patch_thread_modules(). It covers bothpatch_new_process_functions()andpatch_new_process_functions_with_warning(). For each function it puts the original back only while pydevd's wrapper is still the installed one, so a layer another library added on top later stays in place. gevent'spatch_os()is one: it replacesos.forkand keeps the function it found, which is pydevd's wrapper. Theoriginal_*attributes are kept, because the wrapper looks the original up by that name and such saved references keep calling it. For this,monkey_patch_module()tags its wrappers withpydevd_original_name, and patches again when it finds the original back in place, so a latersettrace()installs the wrappers again.On the question in the issue: with this change
stoptrace()alone is enough forPopento work again, and the seconddebugpy.listen()above (withlisten.calledreset by hand) returns in under a second. Two things are unchanged.listen()still refuses a second call because oflisten.called, andSetupHolder.setupkeeps the first adapter's port, so Python children started during a second session still try the old adapter. ClearingSetupHolder.setupinstoptrace()fixed the latter in a local experiment. I left both out because they decide whether listening again is supported; I can add them here or in a follow-up if you want that.Tests:
test_subprocess_after_stoptraceintests/debugpy/test_multiproc.pyruns with the four attach runners. The debuggee callspydevd.stoptrace()after the client disconnects, startspython -c "print('child ok')"and reports the exit code and output. Onmainall four fail withassert [1, ''] == [0, 'child ok']on Windows (3.13) and Linux (3.12); with this change all four pass.tests_python/test_pydev_monkey.py,test_undo_patch_new_process_functionsapplies each patch function, undoes it and checks that the functions inos,subprocess,_posixsubprocessand_winapiare the ones from before.test_undo_monkey_patch_moduleuses a stand-in module: patch, undo, patch again, then wrap pydevd's wrapper in another function and check that the undo leaves that layer in place and that it still works.test_pydev_monkey.py: 33 passed on both platforms.test_multiproc.py,test_disconnect.py,test_attach.py,test_api.pyandserver/test_api.py, run with pytest againstsrc: the only failures (launch tests that time out in my environment, and attach to PID without the native binaries) fail the same way onmain.ruff check .passes.