Send exit message before process is replaced. WIP #865

This commit is contained in:
Fabio Zadrozny 2022-03-31 16:14:08 -03:00
parent 1b568ab86a
commit 7f25cb22b8
7 changed files with 152 additions and 4 deletions

View file

@ -699,6 +699,7 @@ def create_execl(original_name):
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 create_execv(original_name):
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 create_execve(original_name):
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)

View file

@ -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 @@ class NetCommand(_BaseNetCommand):
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 @@ class NetCommand(_BaseNetCommand):
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:

View file

@ -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_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, \
@ -398,6 +401,18 @@ class NetCommandFactoryJson(NetCommandFactory):
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

View file

@ -61,6 +61,9 @@ class NetCommandFactory(object):
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, '')

View file

@ -84,7 +84,7 @@ from _pydevd_bundle.pydevd_comm import(InternalConsoleExec,
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 @@ class PyDB(object):
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 = ""

View file

@ -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!')

View file

@ -5937,6 +5937,69 @@ def test_same_lineno_and_filename(case_setup, pyfile):
writer.finished_ok = True
if __name__ == '__main__':
pytest.main(['-k', 'test_case_skipping_filters', '-s'])
@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_replace_process', '-s'])