Skip to content

Commit

Permalink
Send exit message before process is replaced. WIP microsoft#865
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Mar 31, 2022
1 parent 2ac9538 commit 8c4625e
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 3 deletions.
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
14 changes: 14 additions & 0 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command.py
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,19 @@ 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, reason="processReplaced"))

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

def after_send(socket):
if sys.platform != 'win32':
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!')
65 changes: 64 additions & 1 deletion src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
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['reason'] == "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'])

0 comments on commit 8c4625e

Please sign in to comment.