Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ def new_execl(path, *args):
if _get_apply_arg_patching():
args = patch_args(args, is_exec=True)
send_process_created_message()
send_process_about_to_be_replaced()

return getattr(os, original_name)(path, *args)

Expand All @@ -715,6 +716,7 @@ def new_execv(path, args):
if _get_apply_arg_patching():
args = patch_args(args, is_exec=True)
send_process_created_message()
send_process_about_to_be_replaced()

return getattr(os, original_name)(path, args)

Expand All @@ -731,6 +733,7 @@ def new_execve(path, args, env):
if _get_apply_arg_patching():
args = patch_args(args, is_exec=True)
send_process_created_message()
send_process_about_to_be_replaced()

return getattr(os, original_name)(path, args, env)

Expand Down Expand Up @@ -915,6 +918,12 @@ def send_process_created_message():
py_db.send_process_created_message()


def send_process_about_to_be_replaced():
py_db = get_global_debugger()
if py_db is not None:
py_db.send_process_about_to_be_replaced()


def patch_new_process_functions():
# os.execl(path, arg0, arg1, ...)
# os.execle(path, arg0, arg1, ..., env)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class _BaseNetCommand(object):
def send(self, *args, **kwargs):
pass

def call_after_send(self, callback):
pass


class _NullNetCommand(_BaseNetCommand):
pass
Expand Down Expand Up @@ -48,6 +51,8 @@ class NetCommand(_BaseNetCommand):
_showing_debug_info = 0
_show_debug_info_lock = ForkSafeLock(rlock=True)

_after_send = None

def __init__(self, cmd_id, seq, text, is_json=False):
"""
If sequence is 0, new sequence will be generated (otherwise, this was the response
Expand Down Expand Up @@ -100,6 +105,9 @@ def send(self, sock):
if get_protocol() in (HTTP_PROTOCOL, HTTP_JSON_PROTOCOL):
sock.sendall(('Content-Length: %s\r\n\r\n' % len(as_bytes)).encode('ascii'))
sock.sendall(as_bytes)
if self._after_send:
for method in self._after_send:
method(sock)
except:
if IS_JYTHON:
# Ignore errors in sock.sendall in Jython (seems to be common for Jython to
Expand All @@ -108,6 +116,12 @@ def send(self, sock):
else:
raise

def call_after_send(self, callback):
if not self._after_send:
self._after_send = [callback]
else:
self._after_send.append(callback)

@classmethod
def _show_debug_info(cls, cmd_id, seq, text):
with cls._show_debug_info_lock:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from functools import partial
import itertools
import os
import sys
import socket as socket_module

from _pydev_bundle._pydev_imports_tipper import TYPE_IMPORT, TYPE_CLASS, TYPE_FUNCTION, TYPE_ATTR, \
TYPE_BUILTIN, TYPE_PARAM
from _pydev_bundle.pydev_is_thread_alive import is_thread_alive
from _pydev_bundle.pydev_override import overrides
from _pydevd_bundle._debug_adapter import pydevd_schema
from _pydevd_bundle._debug_adapter.pydevd_schema import ModuleEvent, ModuleEventBody, Module, \
OutputEventBody, OutputEvent, ContinuedEventBody
OutputEventBody, OutputEvent, ContinuedEventBody, ExitedEventBody, \
ExitedEvent
from _pydevd_bundle.pydevd_comm_constants import CMD_THREAD_CREATE, CMD_RETURN, CMD_MODULE_EVENT, \
CMD_WRITE_TO_CONSOLE, CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, \
CMD_STEP_RETURN, CMD_STEP_CAUGHT_EXCEPTION, CMD_ADD_EXCEPTION_BREAK, CMD_SET_BREAK, \
Expand Down Expand Up @@ -398,6 +401,18 @@ def make_send_breakpoint_exception_message(self, *args, **kwargs):
def make_process_created_message(self, *args, **kwargs):
return NULL_NET_COMMAND # Not a part of the debug adapter protocol

@overrides(NetCommandFactory.make_process_about_to_be_replaced_message)
def make_process_about_to_be_replaced_message(self):
event = ExitedEvent(ExitedEventBody(-1, pydevdReason="processReplaced"))

cmd = NetCommand(CMD_RETURN, 0, event, is_json=True)

def after_send(socket):
socket.setsockopt(socket_module.IPPROTO_TCP, socket_module.TCP_NODELAY, 1)

cmd.call_after_send(after_send)
return cmd

@overrides(NetCommandFactory.make_thread_suspend_message)
def make_thread_suspend_message(self, *args, **kwargs):
return NULL_NET_COMMAND # Not a part of the debug adapter protocol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def make_process_created_message(self):
cmdText = '<process/>'
return NetCommand(CMD_PROCESS_CREATED, 0, cmdText)

def make_process_about_to_be_replaced_message(self):
return NULL_NET_COMMAND

def make_show_cython_warning_message(self):
try:
return NetCommand(CMD_SHOW_CYTHON_WARNING, 0, '')
Expand Down
28 changes: 27 additions & 1 deletion src/debugpy/_vendored/pydevd/pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
from _pydevd_bundle.pydevd_daemon_thread import PyDBDaemonThread, mark_as_pydevd_daemon_thread
from _pydevd_bundle.pydevd_process_net_command_json import PyDevJsonCommandProcessor
from _pydevd_bundle.pydevd_process_net_command import process_net_command
from _pydevd_bundle.pydevd_net_command import NetCommand
from _pydevd_bundle.pydevd_net_command import NetCommand, NULL_NET_COMMAND

from _pydevd_bundle.pydevd_breakpoints import stop_on_unhandled_exception
from _pydevd_bundle.pydevd_collect_bytecode_info import collect_try_except_info, collect_return_info, collect_try_except_info_from_source
Expand Down Expand Up @@ -1909,6 +1909,32 @@ def send_process_created_message(self):
cmd = self.cmd_factory.make_process_created_message()
self.writer.add_command(cmd)

def send_process_about_to_be_replaced(self):
"""Sends a message that a new process has been created.
"""
if self.writer is None or self.cmd_factory is None:
return
cmd = self.cmd_factory.make_process_about_to_be_replaced_message()
if cmd is NULL_NET_COMMAND:
return

sent = [False]

def after_sent(*args, **kwargs):
sent[0] = True

cmd.call_after_send(after_sent)
self.writer.add_command(cmd)

timeout = 5 # Wait up to 5 seconds
initial_time = time.time()
while not sent[0]:
time.sleep(.05)

if (time.time() - initial_time) > timeout:
pydev_log.critical('pydevd: Sending message related to process being replaced timed-out after %s seconds', timeout)
break

def set_next_statement(self, frame, event, func_name, next_line):
stop = False
response_msg = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import sys

if __name__ == '__main__':
if 'in-sub' not in sys.argv:
import os
# These functions all execute a new program, replacing the current process; they do not return.
# os.execl(path, arg0, arg1, ...)
# os.execle(path, arg0, arg1, ..., env)
# os.execlp(file, arg0, arg1, ...)
# os.execlpe(file, arg0, arg1, ..., env)¶
# os.execv(path, args)
# os.execve(path, args, env)
# os.execvp(file, args)
# os.execvpe(file, args, env)
os.execvp(sys.executable, [sys.executable, __file__, 'in-sub'])
else:
print('In sub')
print('TEST SUCEEDED!')
Original file line number Diff line number Diff line change
Expand Up @@ -5937,6 +5937,69 @@ def some_code():
writer.finished_ok = True


@pytest.mark.skipif(sys.platform == 'win32', reason='Windows does not have execvp.')
def test_replace_process(case_setup_multiprocessing):
import threading
from tests_python.debugger_unittest import AbstractWriterThread
from _pydevd_bundle._debug_adapter.pydevd_schema import ExitedEvent

with case_setup_multiprocessing.test_file(
'_debugger_case_replace_process.py',
) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch()

break1_line = writer.get_line_index_with_content("print('In sub')")
json_facade.write_set_breakpoints([break1_line])

server_socket = writer.server_socket
secondary_finished_ok = [False]

class SecondaryProcessWriterThread(AbstractWriterThread):

TEST_FILE = writer.get_main_filename()
_sequence = -1

class SecondaryProcessThreadCommunication(threading.Thread):

def run(self):
from tests_python.debugger_unittest import ReaderThread
server_socket.listen(1)
self.server_socket = server_socket
new_sock, addr = server_socket.accept()

reader_thread = ReaderThread(new_sock)
reader_thread.name = ' *** Multiprocess Reader Thread'
reader_thread.start()

writer2 = SecondaryProcessWriterThread()
writer2.reader_thread = reader_thread
writer2.sock = new_sock
json_facade2 = JsonFacade(writer2)

json_facade2.write_set_breakpoints([break1_line, ])
json_facade2.write_make_initial_run()

json_facade2.wait_for_thread_stopped()
json_facade2.write_continue()
secondary_finished_ok[0] = True

secondary_process_thread_communication = SecondaryProcessThreadCommunication()
secondary_process_thread_communication.start()
time.sleep(.1)

json_facade.write_make_initial_run()
exited_event = json_facade.wait_for_json_message(ExitedEvent)
assert exited_event.body.kwargs['pydevdReason'] == "processReplaced"

secondary_process_thread_communication.join(10)
if secondary_process_thread_communication.is_alive():
raise AssertionError('The SecondaryProcessThreadCommunication did not finish')

assert secondary_finished_ok[0]
writer.finished_ok = True


if __name__ == '__main__':
pytest.main(['-k', 'test_case_skipping_filters', '-s'])
pytest.main(['-k', 'test_replace_process', '-s'])