forked from goodboy/tractor
Merge pull request #349 from goodboy/prompt_on_ctrlc
Re-draw `pdbpp` prompt on `SIGINT`deprecate_arbiter_addr
commit
de93c8257c
|
@ -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.
|
7
setup.py
7
setup.py
|
@ -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'],
|
||||||
|
|
|
@ -165,17 +165,16 @@ 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 {node} has nested actors and fails with Ctrl-C.\n'
|
||||||
f'Test for {node} uses nested actors and fails in CI\n'
|
f'The test can sometimes run fine locally but until'
|
||||||
f'The test seems to run fine locally but until we solve'
|
' we solve' 'this issue this CI test will be xfail:\n'
|
||||||
'this issue this CI test will be xfail:\n'
|
'https://github.com/goodboy/tractor/issues/320'
|
||||||
'https://github.com/goodboy/tractor/issues/320'
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if use_ctlc:
|
if use_ctlc:
|
||||||
# XXX: disable pygments highlighting for auto-tests
|
# XXX: disable pygments highlighting for auto-tests
|
||||||
|
|
|
@ -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,32 +657,34 @@ 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:
|
||||||
name = uid_in_debug[0]
|
|
||||||
if name != 'root':
|
|
||||||
log.pdb(
|
|
||||||
f"Ignoring SIGINT while child in debug mode: `{uid_in_debug}`"
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
if pdb_obj:
|
||||||
log.pdb(
|
name = uid_in_debug[0]
|
||||||
"Ignoring SIGINT while in debug mode"
|
if name != 'root':
|
||||||
)
|
log.pdb(
|
||||||
|
f"Ignoring SIGINT, child in debug mode: `{uid_in_debug}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.pdb(
|
||||||
|
"Ignoring SIGINT while in debug mode"
|
||||||
|
)
|
||||||
elif (
|
elif (
|
||||||
is_root_process()
|
is_root_process()
|
||||||
):
|
):
|
||||||
log.pdb(
|
if pdb_obj:
|
||||||
"Ignoring SIGINT since debug mode is enabled"
|
log.pdb(
|
||||||
)
|
"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 (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue