Skip to content

Commit 8157273

Browse files
committed
New launch option: "onTerminate":"KeyboardInterrupt" allows for a soft-kill. Fixes #1022
1 parent 6132125 commit 8157273

File tree

14 files changed

+4193
-3898
lines changed

14 files changed

+4193
-3898
lines changed

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from _pydevd_bundle.pydevd_collect_bytecode_info import code_to_bytecode_representation
2929
import itertools
3030
import linecache
31-
from _pydevd_bundle.pydevd_utils import DAPGrouper
31+
from _pydevd_bundle.pydevd_utils import DAPGrouper, interrupt_main_thread
3232
from _pydevd_bundle.pydevd_daemon_thread import run_as_pydevd_daemon_thread
3333
from _pydevd_bundle.pydevd_thread_lifecycle import pydevd_find_thread_by_id, resume_threads
3434
import tokenize
@@ -1076,6 +1076,9 @@ def _call(self, cmdline, **kwargs):
10761076
def set_terminate_child_processes(self, py_db, terminate_child_processes):
10771077
py_db.terminate_child_processes = terminate_child_processes
10781078

1079+
def set_terminate_keyboard_interrupt(self, py_db, terminate_keyboard_interrupt):
1080+
py_db.terminate_keyboard_interrupt = terminate_keyboard_interrupt
1081+
10791082
def terminate_process(self, py_db):
10801083
'''
10811084
Terminates the current process (and child processes if the option to also terminate
@@ -1097,6 +1100,12 @@ def _terminate_if_commands_processed(self, py_db):
10971100
self.terminate_process(py_db)
10981101

10991102
def request_terminate_process(self, py_db):
1103+
if py_db.terminate_keyboard_interrupt:
1104+
if not py_db.keyboard_interrupt_requested:
1105+
py_db.keyboard_interrupt_requested = True
1106+
interrupt_main_thread()
1107+
return
1108+
11001109
# We mark with a terminate_requested to avoid that paused threads start running
11011110
# (we should terminate as is without letting any paused thread run).
11021111
py_db.terminate_requested = True

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.c

Lines changed: 4018 additions & 3859 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.pyx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raise
169169
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
170170
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
171171
from _pydevd_bundle.pydevd_comm_constants import constant_to_str, CMD_SET_FUNCTION_BREAK
172+
import sys
172173
try:
173174
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
174175
except ImportError:
@@ -737,10 +738,10 @@ cdef class PyDBFrame:
737738
# cost is still high (maybe we could use code-generation in the future and make the code
738739
# generation be better split among what each part does).
739740

740-
# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
741-
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
742-
# 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))
743741
try:
742+
# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
743+
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
744+
# 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))
744745
info.is_tracing += 1
745746

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

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

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

1367-
except KeyboardInterrupt:
1368-
raise
1369-
except:
1370-
try:
1371-
pydev_log.exception()
1372-
info.pydev_original_step_cmd = -1
1373-
info.pydev_step_cmd = -1
1374-
info.pydev_step_stop = None
1375-
except:
1376+
# if we are quitting, let's stop the tracing
1377+
if main_debugger.quitting:
13761378
return None if is_call else NO_FTRACE
13771379

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

1382-
return self.trace_dispatch
13831392
finally:
13841393
info.is_tracing -= 1
13851394

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
1111
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
1212
from _pydevd_bundle.pydevd_comm_constants import constant_to_str, CMD_SET_FUNCTION_BREAK
13+
import sys
1314
try:
1415
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
1516
except ImportError:
@@ -590,10 +591,10 @@ def trace_dispatch(self, frame, event, arg):
590591
# cost is still high (maybe we could use code-generation in the future and make the code
591592
# generation be better split among what each part does).
592593

593-
# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
594-
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
595-
# 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))
596594
try:
595+
# DEBUG = '_debugger_case_generator.py' in frame.f_code.co_filename
596+
main_debugger, abs_path_canonical_path_and_base, info, thread, frame_skips_cache, frame_cache_key = self._args
597+
# 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))
597598
info.is_tracing += 1
598599

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

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

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

1220-
except KeyboardInterrupt:
1221-
raise
1222-
except:
1223-
try:
1224-
pydev_log.exception()
1225-
info.pydev_original_step_cmd = -1
1226-
info.pydev_step_cmd = -1
1227-
info.pydev_step_stop = None
1228-
except:
1229+
# if we are quitting, let's stop the tracing
1230+
if main_debugger.quitting:
12291231
return None if is_call else NO_FTRACE
12301232

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

1235-
return self.trace_dispatch
12361245
finally:
12371246
info.is_tracing -= 1
12381247

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ def make_io_message(self, msg, ctx):
305305
event = OutputEvent(body)
306306
return NetCommand(CMD_WRITE_TO_CONSOLE, 0, event, is_json=True)
307307

308+
@overrides(NetCommandFactory.make_console_message)
309+
def make_console_message(self, msg):
310+
category = 'console'
311+
body = OutputEventBody(msg, category)
312+
event = OutputEvent(body)
313+
return NetCommand(CMD_WRITE_TO_CONSOLE, 0, event, is_json=True)
314+
308315
_STEP_REASONS = set([
309316
CMD_STEP_INTO,
310317
CMD_STEP_INTO_MY_CODE,

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ def make_variable_changed_message(self, seq, payload):
131131
def make_warning_message(self, msg):
132132
return self.make_io_message(msg, 2)
133133

134+
def make_console_message(self, msg):
135+
return self.make_io_message(msg, 2)
136+
134137
def make_io_message(self, msg, ctx):
135138
'''
136139
@param msg: the message to pass to the debug server

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ def _set_debug_options(self, py_db, args, start_reason):
333333
terminate_child_processes = args.get('terminateChildProcesses', True)
334334
self.api.set_terminate_child_processes(py_db, terminate_child_processes)
335335

336+
terminate_keyboard_interrupt = args.get('onTerminate', 'kill') == 'KeyboardInterrupt'
337+
self.api.set_terminate_keyboard_interrupt(py_db, terminate_keyboard_interrupt)
338+
336339
variable_presentation = args.get('variablePresentation', None)
337340
if isinstance(variable_presentation, dict):
338341

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ def __str__(self):
374374
return ''
375375

376376

377-
def interrupt_main_thread(main_thread):
377+
def interrupt_main_thread(main_thread=None):
378378
'''
379379
Generates a KeyboardInterrupt in the main thread by sending a Ctrl+C
380380
or by calling thread.interrupt_main().
@@ -386,6 +386,9 @@ def interrupt_main_thread(main_thread):
386386
when the next Python instruction is about to be executed (so, it won't interrupt
387387
a sleep(1000)).
388388
'''
389+
if main_thread is None:
390+
main_thread = threading.main_thread()
391+
389392
pydev_log.debug('Interrupt main thread.')
390393
called = False
391394
try:

src/debugpy/_vendored/pydevd/build_tools/build.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,19 @@ def build():
161161
use_cython = os.getenv('PYDEVD_USE_CYTHON', '').lower()
162162
# Note: don't import pydevd during build (so, accept just yes/no in this case).
163163
if use_cython == 'yes':
164+
print("Building")
164165
build()
165166
elif use_cython == 'no':
167+
print("Removing binaries")
166168
remove_binaries(['.pyd', '.so'])
167169
elif not use_cython:
168170
# Regular process
169171
if '--no-regenerate-files' not in sys.argv:
172+
print("Generating dont trace files")
170173
generate_dont_trace_files()
174+
print("Generating cython modules")
171175
generate_cython_module()
176+
print("Building")
172177
build()
173178
else:
174179
raise RuntimeError('Unexpected value for PYDEVD_USE_CYTHON: %s (accepted: yes, no)' % (use_cython,))

src/debugpy/_vendored/pydevd/build_tools/generate_code.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,13 @@ def _generate_cython_from_files(target, modules):
9090
# DO NOT edit manually!
9191
''']
9292

93+
found = []
9394
for mod in modules:
95+
found.append(mod.__file__)
9496
contents.append(get_cython_contents(mod.__file__))
9597

98+
print('Generating cython from: %s' % (found,))
99+
96100
with open(target, 'w') as stream:
97101
stream.write(''.join(contents))
98102

@@ -206,6 +210,7 @@ def remove_if_exists(f):
206210

207211

208212
def generate_cython_module():
213+
print('Removing pydevd_cython.pyx')
209214
remove_if_exists(os.path.join(root_dir, '_pydevd_bundle', 'pydevd_cython.pyx'))
210215

211216
target = os.path.join(root_dir, '_pydevd_bundle', 'pydevd_cython.pyx')

src/debugpy/_vendored/pydevd/pydevd.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,13 @@ def __init__(self, set_as_global=True):
540540
# Determines whether we should terminate child processes when asked to terminate.
541541
self.terminate_child_processes = True
542542

543+
# Determines whether we should try to do a soft terminate (i.e.: interrupt the main
544+
# thread with a KeyboardInterrupt).
545+
self.terminate_keyboard_interrupt = False
546+
547+
# Set to True after a keyboard interrupt is requested the first time.
548+
self.keyboard_interrupt_requested = False
549+
543550
# These are the breakpoints received by the PyDevdAPI. They are meant to store
544551
# the breakpoints in the api -- its actual contents are managed by the api.
545552
self.api_received_breakpoints = {}

src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6370,6 +6370,62 @@ def run(self):
63706370
writer.finished_ok = True
63716371

63726372

6373+
@pytest.mark.parametrize('soft_kill', [False, True])
6374+
def test_soft_terminate(case_setup, pyfile, soft_kill):
6375+
6376+
@pyfile
6377+
def target():
6378+
import time
6379+
try:
6380+
while True:
6381+
time.sleep(.2) # break here
6382+
except KeyboardInterrupt:
6383+
# i.e.: The test succeeds if a keyboard interrupt is received.
6384+
print('TEST SUCEEDED!')
6385+
raise
6386+
6387+
def check_test_suceeded_msg(self, stdout, stderr):
6388+
if soft_kill:
6389+
return 'TEST SUCEEDED' in ''.join(stdout)
6390+
else:
6391+
return 'TEST SUCEEDED' not in ''.join(stdout)
6392+
6393+
def additional_output_checks(writer, stdout, stderr):
6394+
if soft_kill:
6395+
assert "KeyboardInterrupt" in stderr
6396+
else:
6397+
assert not stderr
6398+
6399+
with case_setup.test_file(
6400+
target,
6401+
EXPECTED_RETURNCODE='any',
6402+
check_test_suceeded_msg=check_test_suceeded_msg,
6403+
additional_output_checks=additional_output_checks,
6404+
) as writer:
6405+
json_facade = JsonFacade(writer)
6406+
json_facade.write_launch(
6407+
onTerminate="KeyboardInterrupt" if soft_kill else "kill",
6408+
justMyCode=False
6409+
)
6410+
6411+
break_line = writer.get_line_index_with_content('break here')
6412+
json_facade.write_set_breakpoints(break_line)
6413+
json_facade.write_make_initial_run()
6414+
json_hit = json_facade.wait_for_thread_stopped(line=break_line)
6415+
6416+
# Interrupting when inside a breakpoint will actually make the
6417+
# debugger stop working in that thread (because there's no way
6418+
# to keep debugging after an exception exits the tracing).
6419+
6420+
json_facade.write_terminate()
6421+
6422+
if soft_kill:
6423+
json_facade.wait_for_json_message(
6424+
OutputEvent, lambda output_event: 'raised from within the callback set' in output_event.body.output)
6425+
6426+
writer.finished_ok = True
6427+
6428+
63736429
if __name__ == '__main__':
63746430
pytest.main(['-k', 'test_replace_process', '-s'])
63756431

0 commit comments

Comments
 (0)