From a9e0da53934ef52b392904ab4dbc50894e97b450 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Thu, 31 Mar 2022 16:14:08 -0300 Subject: [PATCH] Send exit message before process is replaced. WIP #865 --- .../pydevd/_pydev_bundle/pydev_monkey.py | 9 +++ .../_pydevd_bundle/pydevd_net_command.py | 14 ++++ .../pydevd_net_command_factory_json.py | 17 ++++- .../pydevd_net_command_factory_xml.py | 3 + src/debugpy/_vendored/pydevd/pydevd.py | 28 +++++++- .../_debugger_case_replace_process.py | 18 +++++ .../pydevd/tests_python/test_debugger_json.py | 65 ++++++++++++++++++- 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_replace_process.py diff --git a/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py b/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py index 1f2fd5006..d998a5bc8 100644 --- a/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py +++ b/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command.py index e00c84926..506f5fd27 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command.py @@ -19,6 +19,9 @@ class _BaseNetCommand(object): def send(self, *args, **kwargs): pass + def call_after_send(self, callback): + pass + class _NullNetCommand(_BaseNetCommand): pass @@ -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 @@ -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 @@ -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: diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py index 9d6afbe8a..a73b189d3 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py @@ -1,6 +1,8 @@ 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 @@ -8,7 +10,8 @@ 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, \ @@ -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 diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py index 2836575d8..072ba9853 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py @@ -61,6 +61,9 @@ def make_process_created_message(self): cmdText = '' 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, '') diff --git a/src/debugpy/_vendored/pydevd/pydevd.py b/src/debugpy/_vendored/pydevd/pydevd.py index 63e7f81ca..4a8d45d77 100644 --- a/src/debugpy/_vendored/pydevd/pydevd.py +++ b/src/debugpy/_vendored/pydevd/pydevd.py @@ -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 @@ -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 = "" diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_replace_process.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_replace_process.py new file mode 100644 index 000000000..bdee69509 --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_replace_process.py @@ -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!') 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 b0a29e94c..6ba9eb336 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -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'])