mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
New launch option: "onTerminate":"KeyboardInterrupt" allows for a soft-kill. Fixes #1022
This commit is contained in:
parent
61321253e7
commit
8157273a28
14 changed files with 4541 additions and 4246 deletions
|
|
@ -28,7 +28,7 @@ import ctypes
|
|||
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
|
||||
|
|
@ -1076,6 +1076,9 @@ class PyDevdAPI(object):
|
|||
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
|
||||
|
|
@ -1097,6 +1100,12 @@ class PyDevdAPI(object):
|
|||
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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,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:
|
||||
|
|
@ -590,10 +591,10 @@ 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
|
||||
|
|
@ -992,7 +993,15 @@ 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
|
||||
|
|
@ -1217,22 +1226,22 @@ class PyDBFrame:
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -305,6 +305,13 @@ class NetCommandFactoryJson(NetCommandFactory):
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ class NetCommandFactory(object):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -333,6 +333,9 @@ class PyDevJsonCommandProcessor(object):
|
|||
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):
|
||||
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ class DAPGrouper(object):
|
|||
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().
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -161,14 +161,19 @@ if __name__ == '__main__':
|
|||
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,))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -540,6 +540,13 @@ class PyDB(object):
|
|||
# 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 = {}
|
||||
|
|
|
|||
|
|
@ -6370,6 +6370,62 @@ def test_logging_api(case_setup_multiprocessing, tmpdir):
|
|||
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'])
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ class Client(components.Component):
|
|||
only if and when the "launch" or "attach" response is sent.
|
||||
"""
|
||||
|
||||
self._forward_terminate_request = False
|
||||
|
||||
self.known_subprocesses = set()
|
||||
|
||||
session.client = self
|
||||
|
|
@ -180,7 +182,7 @@ class Client(components.Component):
|
|||
"supportsSetExpression": True,
|
||||
"supportsSetVariable": True,
|
||||
"supportsValueFormattingOptions": True,
|
||||
"supportsTerminateDebuggee": True,
|
||||
"supportsTerminateRequest": True,
|
||||
"supportsGotoTargetsRequest": True,
|
||||
"supportsClipboardContext": True,
|
||||
"exceptionBreakpointFilters": exception_breakpoint_filters,
|
||||
|
|
@ -192,6 +194,7 @@ class Client(components.Component):
|
|||
# See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
|
||||
# for the sequence of request and events necessary to orchestrate the start.
|
||||
def _start_message_handler(f):
|
||||
|
||||
@components.Component.message_handler
|
||||
def handle(self, request):
|
||||
assert request.is_request("launch", "attach")
|
||||
|
|
@ -404,6 +407,11 @@ class Client(components.Component):
|
|||
if sudo and sys.platform == "win32":
|
||||
raise request.cant_handle('"sudo":true is not supported on Windows.')
|
||||
|
||||
on_terminate = request("onTerminate", str, optional=True)
|
||||
|
||||
if on_terminate:
|
||||
self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
|
||||
|
||||
launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__))
|
||||
adapter_host = request("debugAdapterHost", "127.0.0.1")
|
||||
|
||||
|
|
@ -441,6 +449,10 @@ class Client(components.Component):
|
|||
connect = request("connect", dict, optional=True)
|
||||
pid = request("processId", (int, str), optional=True)
|
||||
sub_pid = request("subProcessId", int, optional=True)
|
||||
on_terminate = request("onTerminate", bool, optional=True)
|
||||
|
||||
if on_terminate:
|
||||
self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
|
||||
|
||||
if host != () or port != ():
|
||||
if listen != ():
|
||||
|
|
@ -637,6 +649,15 @@ class Client(components.Component):
|
|||
|
||||
@message_handler
|
||||
def terminate_request(self, request):
|
||||
if self._forward_terminate_request:
|
||||
# According to the spec, terminate should try to do a gracefull shutdown.
|
||||
# We do this in the server by interrupting the main thread with a Ctrl+C.
|
||||
# To force the kill a subsequent request would do a disconnect.
|
||||
#
|
||||
# We only do this if the onTerminate option is set though (the default
|
||||
# is a hard-kill for the process and subprocesses).
|
||||
return self.server.channel.delegate(request)
|
||||
|
||||
self.session.finalize('client requested "terminate"', terminate_debuggee=True)
|
||||
return {}
|
||||
|
||||
|
|
|
|||
|
|
@ -280,8 +280,7 @@ class Server(components.Component):
|
|||
"supportsSetVariable": False,
|
||||
"supportsStepBack": False,
|
||||
"supportsStepInTargetsRequest": False,
|
||||
"supportsTerminateDebuggee": False,
|
||||
"supportsTerminateRequest": False,
|
||||
"supportsTerminateRequest": True,
|
||||
"supportsTerminateThreadsRequest": False,
|
||||
"supportsValueFormattingOptions": False,
|
||||
"exceptionBreakpointFilters": [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue