Merge pull request #349 from goodboy/prompt_on_ctrlc

Re-draw `pdbpp` prompt on `SIGINT`
deprecate_arbiter_addr
goodboy 2023-01-26 15:56:37 -05:00 committed by GitHub
commit de93c8257c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 120 additions and 50 deletions

View File

@ -0,0 +1,10 @@
Always redraw the `pdbpp` prompt on `SIGINT` during REPL use.
There was recent changes todo with Python 3.10 that required us to pin
to a specific commit in `pdbpp` which have recently been fixed minus
this last issue with `SIGINT` shielding: not clobbering or not
showing the `(Pdb++)` prompt on ctlr-c by the user. This repairs all
that by firstly removing the standard KBI intercepting of the std lib's
`pdb.Pdb._cmdloop()` as well as ensuring that only the actor with REPL
control ever reports `SIGINT` handler log msgs and prompt redraws. With
this we move back to using pypi `pdbpp` release.

View File

@ -60,6 +60,9 @@ setup(
# serialization # serialization
'msgspec', 'msgspec',
# debug mode REPL
'pdbpp',
# pip ref docs on these specs: # pip ref docs on these specs:
# https://pip.pypa.io/en/stable/reference/requirement-specifiers/#examples # https://pip.pypa.io/en/stable/reference/requirement-specifiers/#examples
# and pep: # and pep:
@ -70,10 +73,6 @@ setup(
# https://github.com/pdbpp/fancycompleter/issues/37 # https://github.com/pdbpp/fancycompleter/issues/37
'pyreadline3 ; platform_system == "Windows"', 'pyreadline3 ; platform_system == "Windows"',
# 3.10 has an outstanding unreleased issue and `pdbpp` itself
# pins to patched forks of its own dependencies as well..and
# we need a specific patch on master atm.
'pdbpp @ git+https://github.com/pdbpp/pdbpp@76c4be5#egg=pdbpp ; python_version > "3.9"', # noqa: E501
], ],
tests_require=['pytest'], tests_require=['pytest'],

View File

@ -165,15 +165,14 @@ def ctlc(
# be 3.10+ mega-asap. # be 3.10+ mega-asap.
pytest.skip('Py3.9 and `pdbpp` son no bueno..') pytest.skip('Py3.9 and `pdbpp` son no bueno..')
if ci_env:
node = request.node node = request.node
markers = node.own_markers markers = node.own_markers
for mark in markers: for mark in markers:
if mark.name == 'has_nested_actors': if mark.name == 'has_nested_actors':
pytest.skip( pytest.skip(
f'Test for {node} uses nested actors and fails in CI\n' f'Test {node} has nested actors and fails with Ctrl-C.\n'
f'The test seems to run fine locally but until we solve' f'The test can sometimes run fine locally but until'
'this issue this CI test will be xfail:\n' ' we solve' 'this issue this CI test will be xfail:\n'
'https://github.com/goodboy/tractor/issues/320' 'https://github.com/goodboy/tractor/issues/320'
) )

View File

@ -20,9 +20,13 @@ Multi-core debugging for da peeps!
""" """
from __future__ import annotations from __future__ import annotations
import bdb import bdb
import os
import sys import sys
import signal import signal
from functools import partial from functools import (
partial,
cached_property,
)
from contextlib import asynccontextmanager as acm from contextlib import asynccontextmanager as acm
from typing import ( from typing import (
Any, Any,
@ -73,6 +77,7 @@ class Lock:
Mostly to avoid a lot of ``global`` declarations for now XD. Mostly to avoid a lot of ``global`` declarations for now XD.
''' '''
repl: MultiActorPdb | None = None
# placeholder for function to set a ``trio.Event`` on debugger exit # placeholder for function to set a ``trio.Event`` on debugger exit
# pdb_release_hook: Optional[Callable] = None # pdb_release_hook: Optional[Callable] = None
@ -111,7 +116,7 @@ class Lock:
def shield_sigint(cls): def shield_sigint(cls):
cls._orig_sigint_handler = signal.signal( cls._orig_sigint_handler = signal.signal(
signal.SIGINT, signal.SIGINT,
shield_sigint, shield_sigint_handler,
) )
@classmethod @classmethod
@ -146,6 +151,7 @@ class Lock:
finally: finally:
# restore original sigint handler # restore original sigint handler
cls.unshield_sigint() cls.unshield_sigint()
cls.repl = None
class TractorConfig(pdbpp.DefaultConfig): class TractorConfig(pdbpp.DefaultConfig):
@ -184,6 +190,35 @@ class MultiActorPdb(pdbpp.Pdb):
finally: finally:
Lock.release() Lock.release()
# XXX NOTE: we only override this because apparently the stdlib pdb
# bois likes to touch the SIGINT handler as much as i like to touch
# my d$%&.
def _cmdloop(self):
self.cmdloop()
@cached_property
def shname(self) -> str | None:
'''
Attempt to return the login shell name with a special check for
the infamous `xonsh` since it seems to have some issues much
different from std shells when it comes to flushing the prompt?
'''
# SUPER HACKY and only really works if `xonsh` is not used
# before spawning further sub-shells..
shpath = os.getenv('SHELL', None)
if shpath:
if (
os.getenv('XONSH_LOGIN', default=False)
or 'xonsh' in shpath
):
return 'xonsh'
return os.path.basename(shpath)
return None
@acm @acm
async def _acquire_debug_lock_from_root_task( async def _acquire_debug_lock_from_root_task(
@ -388,6 +423,7 @@ async def wait_for_parent_stdin_hijack(
except ContextCancelled: except ContextCancelled:
log.warning('Root actor cancelled debug lock') log.warning('Root actor cancelled debug lock')
raise
finally: finally:
Lock.local_task_in_debug = None Lock.local_task_in_debug = None
@ -435,7 +471,10 @@ async def _breakpoint(
# with trio.CancelScope(shield=shield): # with trio.CancelScope(shield=shield):
# await trio.lowlevel.checkpoint() # await trio.lowlevel.checkpoint()
if not Lock.local_pdb_complete or Lock.local_pdb_complete.is_set(): if (
not Lock.local_pdb_complete
or Lock.local_pdb_complete.is_set()
):
Lock.local_pdb_complete = trio.Event() Lock.local_pdb_complete = trio.Event()
# TODO: need a more robust check for the "root" actor # TODO: need a more robust check for the "root" actor
@ -484,6 +523,7 @@ async def _breakpoint(
wait_for_parent_stdin_hijack, wait_for_parent_stdin_hijack,
actor.uid, actor.uid,
) )
Lock.repl = pdb
except RuntimeError: except RuntimeError:
Lock.release() Lock.release()
@ -522,6 +562,7 @@ async def _breakpoint(
Lock.global_actor_in_debug = actor.uid Lock.global_actor_in_debug = actor.uid
Lock.local_task_in_debug = task_name Lock.local_task_in_debug = task_name
Lock.repl = pdb
try: try:
# block here one (at the appropriate frame *up*) where # block here one (at the appropriate frame *up*) where
@ -545,10 +586,10 @@ async def _breakpoint(
# # signal.signal = pdbpp.hideframe(signal.signal) # # signal.signal = pdbpp.hideframe(signal.signal)
def shield_sigint( def shield_sigint_handler(
signum: int, signum: int,
frame: 'frame', # type: ignore # noqa frame: 'frame', # type: ignore # noqa
pdb_obj: Optional[MultiActorPdb] = None, # pdb_obj: Optional[MultiActorPdb] = None,
*args, *args,
) -> None: ) -> None:
@ -565,6 +606,7 @@ def shield_sigint(
uid_in_debug = Lock.global_actor_in_debug uid_in_debug = Lock.global_actor_in_debug
actor = tractor.current_actor() actor = tractor.current_actor()
# print(f'{actor.uid} in HANDLER with ')
def do_cancel(): def do_cancel():
# If we haven't tried to cancel the runtime then do that instead # If we haven't tried to cancel the runtime then do that instead
@ -598,6 +640,9 @@ def shield_sigint(
) )
return do_cancel() return do_cancel()
# only set in the actor actually running the REPL
pdb_obj = Lock.repl
# root actor branch that reports whether or not a child # root actor branch that reports whether or not a child
# has locked debugger. # has locked debugger.
if ( if (
@ -612,10 +657,12 @@ def shield_sigint(
): ):
# we are root and some actor is in debug mode # we are root and some actor is in debug mode
# if uid_in_debug is not None: # if uid_in_debug is not None:
if pdb_obj:
name = uid_in_debug[0] name = uid_in_debug[0]
if name != 'root': if name != 'root':
log.pdb( log.pdb(
f"Ignoring SIGINT while child in debug mode: `{uid_in_debug}`" f"Ignoring SIGINT, child in debug mode: `{uid_in_debug}`"
) )
else: else:
@ -625,19 +672,19 @@ def shield_sigint(
elif ( elif (
is_root_process() is_root_process()
): ):
if pdb_obj:
log.pdb( log.pdb(
"Ignoring SIGINT since debug mode is enabled" "Ignoring SIGINT since debug mode is enabled"
) )
# revert back to ``trio`` handler asap!
Lock.unshield_sigint()
if ( if (
Lock._root_local_task_cs_in_debug Lock._root_local_task_cs_in_debug
and not Lock._root_local_task_cs_in_debug.cancel_called and not Lock._root_local_task_cs_in_debug.cancel_called
): ):
Lock._root_local_task_cs_in_debug.cancel() Lock._root_local_task_cs_in_debug.cancel()
# raise KeyboardInterrupt # revert back to ``trio`` handler asap!
Lock.unshield_sigint()
# child actor that has locked the debugger # child actor that has locked the debugger
elif not is_root_process(): elif not is_root_process():
@ -653,7 +700,10 @@ def shield_sigint(
return do_cancel() return do_cancel()
task = Lock.local_task_in_debug task = Lock.local_task_in_debug
if task: if (
task
and pdb_obj
):
log.pdb( log.pdb(
f"Ignoring SIGINT while task in debug mode: `{task}`" f"Ignoring SIGINT while task in debug mode: `{task}`"
) )
@ -668,14 +718,21 @@ def shield_sigint(
raise KeyboardInterrupt raise KeyboardInterrupt
# NOTE: currently (at least on ``fancycompleter`` 0.9.2) # NOTE: currently (at least on ``fancycompleter`` 0.9.2)
# it lookks to be that the last command that was run (eg. ll) # it looks to be that the last command that was run (eg. ll)
# will be repeated by default. # will be repeated by default.
# TODO: maybe redraw/print last REPL output to console # maybe redraw/print last REPL output to console since
# we want to alert the user that more input is expect since
# nothing has been done dur to ignoring sigint.
if ( if (
pdb_obj pdb_obj # only when this actor has a REPL engaged
and sys.version_info <= (3, 10)
): ):
# XXX: yah, mega hack, but how else do we catch this madness XD
if pdb_obj.shname == 'xonsh':
pdb_obj.stdout.write(pdb_obj.prompt)
pdb_obj.stdout.flush()
# TODO: make this work like sticky mode where if there is output # TODO: make this work like sticky mode where if there is output
# detected as written to the tty we redraw this part underneath # detected as written to the tty we redraw this part underneath
# and erase the past draw of this same bit above? # and erase the past draw of this same bit above?
@ -689,14 +746,6 @@ def shield_sigint(
# XXX: lol, see ``pdbpp`` issue: # XXX: lol, see ``pdbpp`` issue:
# https://github.com/pdbpp/pdbpp/issues/496 # https://github.com/pdbpp/pdbpp/issues/496
# TODO: pretty sure this is what we should expect to have to run
# in total but for now we're just going to wait until `pdbpp`
# figures out it's own stuff on 3.10 (and maybe we'll help).
# pdb_obj.do_longlist(None)
# XXX: we were doing this but it shouldn't be required..
print(pdb_obj.prompt, end='', flush=True)
def _set_trace( def _set_trace(
actor: Optional[tractor.Actor] = None, actor: Optional[tractor.Actor] = None,
@ -820,7 +869,10 @@ async def maybe_wait_for_debugger(
) -> None: ) -> None:
if not debug_mode() and not child_in_debug: if (
not debug_mode()
and not child_in_debug
):
return return
if ( if (

View File

@ -1599,7 +1599,10 @@ async def process_messages(
# handshake for them (yet) and instead we simply bail out of # handshake for them (yet) and instead we simply bail out of
# the message loop and expect the teardown sequence to clean # the message loop and expect the teardown sequence to clean
# up. # up.
log.runtime(f'channel from {chan.uid} closed abruptly:\n{chan}') log.runtime(
f'channel from {chan.uid} closed abruptly:\n'
f'-> {chan.raddr}\n'
)
# transport **was** disconnected # transport **was** disconnected
return True return True

View File

@ -457,6 +457,13 @@ async def trio_proc(
await proc.wait() await proc.wait()
if is_root_process(): if is_root_process():
# TODO: solve the following issue where we need
# to do a similar wait like this but in an
# "intermediary" parent actor that itself isn't
# in debug but has a child that is, and we need
# to hold off on relaying SIGINT until that child
# is complete.
# https://github.com/goodboy/tractor/issues/320
await maybe_wait_for_debugger( await maybe_wait_for_debugger(
child_in_debug=_runtime_vars.get( child_in_debug=_runtime_vars.get(
'_debug_mode', False), '_debug_mode', False),