Skip to content

Commit

Permalink
New launch option: "onTerminate":"KeyboardInterrupt" allows for a sof…
Browse files Browse the repository at this point in the history
…t-kill. Fixes microsoft#1022
  • Loading branch information
fabioz committed Sep 8, 2022
1 parent 6132125 commit 8278103
Show file tree
Hide file tree
Showing 14 changed files with 4,193 additions and 3,898 deletions.
11 changes: 10 additions & 1 deletion src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from _pydevd_bundle.pydevd_collect_bytecode_info import code_to_bytecode_representation
import itertools
import linecache
from _pydevd_bundle.pydevd_utils import DAPGrouper
from _pydevd_bundle.pydevd_utils import DAPGrouper, interrupt_main_thread
from _pydevd_bundle.pydevd_daemon_thread import run_as_pydevd_daemon_thread
from _pydevd_bundle.pydevd_thread_lifecycle import pydevd_find_thread_by_id, resume_threads
import tokenize
Expand Down Expand Up @@ -1076,6 +1076,9 @@ def _call(self, cmdline, **kwargs):
def set_terminate_child_processes(self, py_db, terminate_child_processes):
py_db.terminate_child_processes = terminate_child_processes

def set_terminate_keyboard_interrupt(self, py_db, terminate_keyboard_interrupt):
py_db.terminate_keyboard_interrupt = terminate_keyboard_interrupt

def terminate_process(self, py_db):
'''
Terminates the current process (and child processes if the option to also terminate
Expand All @@ -1097,6 +1100,12 @@ def _terminate_if_commands_processed(self, py_db):
self.terminate_process(py_db)

def request_terminate_process(self, py_db):
if py_db.terminate_keyboard_interrupt:
if not py_db.keyboard_interrupt_requested:
py_db.keyboard_interrupt_requested = True
interrupt_main_thread()
return

# We mark with a terminate_requested to avoid that paused threads start running
# (we should terminate as is without letting any paused thread run).
py_db.terminate_requested = True
Expand Down
7,877 changes: 4,018 additions & 3,859 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.c

Large diffs are not rendered by default.

43 changes: 26 additions & 17 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raise
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
from _pydevd_bundle.pydevd_comm_constants import constant_to_str, CMD_SET_FUNCTION_BREAK
import sys
try:
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
except ImportError:
Expand Down Expand Up @@ -737,10 +738,10 @@ cdef class PyDBFrame:
# cost is still high (maybe we could use code-generation in the future and make the code
# generation be better split among what each part does).

# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
# if DEBUG: print('frame trace_dispatch %s %s %s %s %s %s, stop: %s' % (frame.f_lineno, frame.f_code.co_name, frame.f_code.co_filename, event, constant_to_str(info.pydev_step_cmd), arg, info.pydev_step_stop))
try:
# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
# if DEBUG: print('frame trace_dispatch %s %s %s %s %s %s, stop: %s' % (frame.f_lineno, frame.f_code.co_name, frame.f_code.co_filename, event, constant_to_str(info.pydev_step_cmd), arg, info.pydev_step_stop))
info.is_tracing += 1

# TODO: This shouldn't be needed. The fact that frame.f_lineno
Expand Down Expand Up @@ -1139,7 +1140,15 @@ cdef class PyDBFrame:
frame_skips_cache[line_cache_key] = 0

except:
pydev_log.exception()
# Unfortunately Python itself stops the tracing when it originates from
# the tracing function, so, we can't do much about it (just let the user know).
exc = sys.exc_info()[0]
cmd = main_debugger.cmd_factory.make_console_message(
'%s raised from within the callback set in sys.settrace.\nDebugging will be disabled for this thread (%s).\n' % (exc, thread,))
main_debugger.writer.add_command(cmd)
if not issubclass(exc, (KeyboardInterrupt, SystemExit)):
pydev_log.exception()

raise

# step handling. We stop when we hit the right frame
Expand Down Expand Up @@ -1364,22 +1373,22 @@ cdef class PyDBFrame:
info.pydev_step_cmd = -1
info.pydev_state = 1

except KeyboardInterrupt:
raise
except:
try:
pydev_log.exception()
info.pydev_original_step_cmd = -1
info.pydev_step_cmd = -1
info.pydev_step_stop = None
except:
# if we are quitting, let's stop the tracing
if main_debugger.quitting:
return None if is_call else NO_FTRACE

# if we are quitting, let's stop the tracing
if main_debugger.quitting:
return None if is_call else NO_FTRACE
return self.trace_dispatch
except:
# Unfortunately Python itself stops the tracing when it originates from
# the tracing function, so, we can't do much about it (just let the user know).
exc = sys.exc_info()[0]
cmd = main_debugger.cmd_factory.make_console_message(
'%s raised from within the callback set in sys.settrace.\nDebugging will be disabled for this thread (%s).\n' % (exc, thread,))
main_debugger.writer.add_command(cmd)
if not issubclass(exc, (KeyboardInterrupt, SystemExit)):
pydev_log.exception()
raise

return self.trace_dispatch
finally:
info.is_tracing -= 1

Expand Down
43 changes: 26 additions & 17 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
from _pydevd_bundle.pydevd_comm_constants import constant_to_str, CMD_SET_FUNCTION_BREAK
import sys
try:
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
except ImportError:
Expand Down Expand Up @@ -590,10 +591,10 @@ def trace_dispatch(self, frame, event, arg):
# cost is still high (maybe we could use code-generation in the future and make the code
# generation be better split among what each part does).

# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
# if DEBUG: print('frame trace_dispatch %s %s %s %s %s %s, stop: %s' % (frame.f_lineno, frame.f_code.co_name, frame.f_code.co_filename, event, constant_to_str(info.pydev_step_cmd), arg, info.pydev_step_stop))
try:
# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
# if DEBUG: print('frame trace_dispatch %s %s %s %s %s %s, stop: %s' % (frame.f_lineno, frame.f_code.co_name, frame.f_code.co_filename, event, constant_to_str(info.pydev_step_cmd), arg, info.pydev_step_stop))
info.is_tracing += 1

# TODO: This shouldn't be needed. The fact that frame.f_lineno
Expand Down Expand Up @@ -992,7 +993,15 @@ def trace_dispatch(self, frame, event, arg):
frame_skips_cache[line_cache_key] = 0

except:
pydev_log.exception()
# Unfortunately Python itself stops the tracing when it originates from
# the tracing function, so, we can't do much about it (just let the user know).
exc = sys.exc_info()[0]
cmd = main_debugger.cmd_factory.make_console_message(
'%s raised from within the callback set in sys.settrace.\nDebugging will be disabled for this thread (%s).\n' % (exc, thread,))
main_debugger.writer.add_command(cmd)
if not issubclass(exc, (KeyboardInterrupt, SystemExit)):
pydev_log.exception()

raise

# step handling. We stop when we hit the right frame
Expand Down Expand Up @@ -1217,22 +1226,22 @@ def trace_dispatch(self, frame, event, arg):
info.pydev_step_cmd = -1
info.pydev_state = STATE_RUN

except KeyboardInterrupt:
raise
except:
try:
pydev_log.exception()
info.pydev_original_step_cmd = -1
info.pydev_step_cmd = -1
info.pydev_step_stop = None
except:
# if we are quitting, let's stop the tracing
if main_debugger.quitting:
return None if is_call else NO_FTRACE

# if we are quitting, let's stop the tracing
if main_debugger.quitting:
return None if is_call else NO_FTRACE
return self.trace_dispatch
except:
# Unfortunately Python itself stops the tracing when it originates from
# the tracing function, so, we can't do much about it (just let the user know).
exc = sys.exc_info()[0]
cmd = main_debugger.cmd_factory.make_console_message(
'%s raised from within the callback set in sys.settrace.\nDebugging will be disabled for this thread (%s).\n' % (exc, thread,))
main_debugger.writer.add_command(cmd)
if not issubclass(exc, (KeyboardInterrupt, SystemExit)):
pydev_log.exception()
raise

return self.trace_dispatch
finally:
info.is_tracing -= 1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ def make_io_message(self, msg, ctx):
event = OutputEvent(body)
return NetCommand(CMD_WRITE_TO_CONSOLE, 0, event, is_json=True)

@overrides(NetCommandFactory.make_console_message)
def make_console_message(self, msg):
category = 'console'
body = OutputEventBody(msg, category)
event = OutputEvent(body)
return NetCommand(CMD_WRITE_TO_CONSOLE, 0, event, is_json=True)

_STEP_REASONS = set([
CMD_STEP_INTO,
CMD_STEP_INTO_MY_CODE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ def make_variable_changed_message(self, seq, payload):
def make_warning_message(self, msg):
return self.make_io_message(msg, 2)

def make_console_message(self, msg):
return self.make_io_message(msg, 2)

def make_io_message(self, msg, ctx):
'''
@param msg: the message to pass to the debug server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ def _set_debug_options(self, py_db, args, start_reason):
terminate_child_processes = args.get('terminateChildProcesses', True)
self.api.set_terminate_child_processes(py_db, terminate_child_processes)

terminate_keyboard_interrupt = args.get('onTerminate', 'kill') == 'KeyboardInterrupt'
self.api.set_terminate_keyboard_interrupt(py_db, terminate_keyboard_interrupt)

variable_presentation = args.get('variablePresentation', None)
if isinstance(variable_presentation, dict):

Expand Down
5 changes: 4 additions & 1 deletion src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ def __str__(self):
return ''


def interrupt_main_thread(main_thread):
def interrupt_main_thread(main_thread=None):
'''
Generates a KeyboardInterrupt in the main thread by sending a Ctrl+C
or by calling thread.interrupt_main().
Expand All @@ -386,6 +386,9 @@ def interrupt_main_thread(main_thread):
when the next Python instruction is about to be executed (so, it won't interrupt
a sleep(1000)).
'''
if main_thread is None:
main_thread = threading.main_thread()

pydev_log.debug('Interrupt main thread.')
called = False
try:
Expand Down
5 changes: 5 additions & 0 deletions src/debugpy/_vendored/pydevd/build_tools/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,19 @@ def build():
use_cython = os.getenv('PYDEVD_USE_CYTHON', '').lower()
# Note: don't import pydevd during build (so, accept just yes/no in this case).
if use_cython == 'yes':
print("Building")
build()
elif use_cython == 'no':
print("Removing binaries")
remove_binaries(['.pyd', '.so'])
elif not use_cython:
# Regular process
if '--no-regenerate-files' not in sys.argv:
print("Generating dont trace files")
generate_dont_trace_files()
print("Generating cython modules")
generate_cython_module()
print("Building")
build()
else:
raise RuntimeError('Unexpected value for PYDEVD_USE_CYTHON: %s (accepted: yes, no)' % (use_cython,))
Expand Down
5 changes: 5 additions & 0 deletions src/debugpy/_vendored/pydevd/build_tools/generate_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ def _generate_cython_from_files(target, modules):
# DO NOT edit manually!
''']

found = []
for mod in modules:
found.append(mod.__file__)
contents.append(get_cython_contents(mod.__file__))

print('Generating cython from: %s' % (found,))

with open(target, 'w') as stream:
stream.write(''.join(contents))

Expand Down Expand Up @@ -206,6 +210,7 @@ def remove_if_exists(f):


def generate_cython_module():
print('Removing pydevd_cython.pyx')
remove_if_exists(os.path.join(root_dir, '_pydevd_bundle', 'pydevd_cython.pyx'))

target = os.path.join(root_dir, '_pydevd_bundle', 'pydevd_cython.pyx')
Expand Down
7 changes: 7 additions & 0 deletions src/debugpy/_vendored/pydevd/pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,13 @@ def __init__(self, set_as_global=True):
# Determines whether we should terminate child processes when asked to terminate.
self.terminate_child_processes = True

# Determines whether we should try to do a soft terminate (i.e.: interrupt the main
# thread with a KeyboardInterrupt).
self.terminate_keyboard_interrupt = False

# Set to True after a keyboard interrupt is requested the first time.
self.keyboard_interrupt_requested = False

# These are the breakpoints received by the PyDevdAPI. They are meant to store
# the breakpoints in the api -- its actual contents are managed by the api.
self.api_received_breakpoints = {}
Expand Down
56 changes: 56 additions & 0 deletions src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6370,6 +6370,62 @@ def run(self):
writer.finished_ok = True


@pytest.mark.parametrize('soft_kill', [False, True])
def test_soft_terminate(case_setup, pyfile, soft_kill):

@pyfile
def target():
import time
try:
while True:
time.sleep(.2) # break here
except KeyboardInterrupt:
# i.e.: The test succeeds if a keyboard interrupt is received.
print('TEST SUCEEDED!')
raise

def check_test_suceeded_msg(self, stdout, stderr):
if soft_kill:
return 'TEST SUCEEDED' in ''.join(stdout)
else:
return 'TEST SUCEEDED' not in ''.join(stdout)

def additional_output_checks(writer, stdout, stderr):
if soft_kill:
assert "KeyboardInterrupt" in stderr
else:
assert not stderr

with case_setup.test_file(
target,
EXPECTED_RETURNCODE='any',
check_test_suceeded_msg=check_test_suceeded_msg,
additional_output_checks=additional_output_checks,
) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch(
onTerminate="KeyboardInterrupt" if soft_kill else "kill",
justMyCode=False
)

break_line = writer.get_line_index_with_content('break here')
json_facade.write_set_breakpoints(break_line)
json_facade.write_make_initial_run()
json_hit = json_facade.wait_for_thread_stopped(line=break_line)

# Interrupting when inside a breakpoint will actually make the
# debugger stop working in that thread (because there's no way
# to keep debugging after an exception exits the tracing).

json_facade.write_terminate()

if soft_kill:
json_facade.wait_for_json_message(
OutputEvent, lambda output_event: 'raised from within the callback set' in output_event.body.output)

writer.finished_ok = True


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

Loading

0 comments on commit 8278103

Please sign in to comment.