diff --git a/src/debugpy/server/cli.py b/src/debugpy/server/cli.py index 48bd90946..aa506e0ce 100644 --- a/src/debugpy/server/cli.py +++ b/src/debugpy/server/cli.py @@ -465,7 +465,7 @@ def attach_to_pid(): ) tmp_file.write(python_code.encode()) tmp_file.write( - """import os;os.remove("{tmp_file_path}");""".format( + """import os;os.remove({tmp_file_path!r});""".format( tmp_file_path=tmp_file_path ).encode() ) diff --git a/tests/debugpy/server/test_cli.py b/tests/debugpy/server/test_cli.py index 45fed7527..da6117d7c 100644 --- a/tests/debugpy/server/test_cli.py +++ b/tests/debugpy/server/test_cli.py @@ -248,3 +248,64 @@ def test_script_parent_pid_with_listen_failure(cli): cli(["--listen", "8888", "--parent-session-pid", "1234", "spam.py"]) assert "--parent-session-pid requires --connect" in str(ex.value) + + +def test_pep_768_remote_exec_called_with_backslash_path(): + """Test that attach_to_pid() calls sys.remote_exec and writes valid Python + to the temp file even when the temp path contains backslashes (Windows).""" + import contextlib + from debugpy.server import cli + + mock_windows_tmp_path = r"C:\Users\test\AppData\Local\Temp\tmp0_vuee4s" + pid = os.getpid() + + # A fake file object that captures writes via a side_effect list. + # Using MagicMock avoids the BytesIO.close() issue where getvalue() + # raises ValueError on a closed buffer. + written_chunks = [] + fake_file = mock.MagicMock() + fake_file.name = mock_windows_tmp_path + fake_file.write.side_effect = written_chunks.append + + fake_file_cm = mock.MagicMock() + fake_file_cm.__enter__ = mock.Mock(return_value=fake_file) + fake_file_cm.__exit__ = mock.Mock(return_value=False) + + # Configure cli.options with the minimum fields attach_to_pid() needs. + original_options = { + attr: getattr(cli.options, attr) + for attr in ("mode", "address", "wait_for_client", "log_to", + "adapter_access_token", "disable_sys_remote_exec", "target") + } + try: + cli.options.mode = "connect" + cli.options.address = ("127.0.0.1", 5678) + cli.options.wait_for_client = False + cli.options.log_to = None + cli.options.adapter_access_token = None + cli.options.disable_sys_remote_exec = False + cli.options.target = pid + + with contextlib.ExitStack() as stack: + stack.enter_context( + mock.patch("tempfile.NamedTemporaryFile", return_value=fake_file_cm) + ) + mock_remote_exec = stack.enter_context( + mock.patch.object(sys, "remote_exec", create=True) + ) + cli.attach_to_pid() + + # sys.remote_exec must have been called with the original (backslash) path + # because that is the actual path created on disk. + mock_remote_exec.assert_called_once_with(pid, mock_windows_tmp_path) + + # The Python code written to the temp file must be syntactically valid. + injected_code = b"".join(written_chunks).decode() + compile(injected_code, "", "exec") + + # The os.remove() call inside the injected code must use the repr of the path + # so that it is a valid Python string literal on all platforms. + assert "import os;os.remove({});".format(repr(mock_windows_tmp_path)) in injected_code + finally: + for attr, value in original_options.items(): + setattr(cli.options, attr, value)