First proto: use `greenback` for sync func breakpointing

This works now for supporting a new `tractor.pause_from_sync()`
`tractor`-aware-replacement for `Pdb.set_trace()` from sync functions
which are also scheduled from our runtime. Uses `greenback` to do all
the magic of scheduling the bg `tractor._debug._pause()` task and
engaging the normal TTY locking machinery triggered by `await
tractor.breakpoint()`

Further this starts some public API renaming, making a switch to
`tractor.pause()` from `.breakpoint()` which IMO much better expresses
the semantics of the runtime intervention required to suffice
multi-process "breakpointing"; it also is an alternate name for the same
in computer science more generally: https://en.wikipedia.org/wiki/Breakpoint
It also avoids using the same name as the `breakpoint()` built-in which
is important since there **is alot more going on** when you call our
equivalent API.

Deats of that:
- add deprecation warning for `tractor.breakpoint()`
- add `tractor.pause()` and a shorthand, easier-to-type, alias `.pp()`
  for "pause-point" B)
- add `pause_from_sync()` as the new `breakpoint()`-from-sync-function
  hack which does all the `greenback` stuff for the user.

Still TODO:
- figure out where in the runtime and when to call
  `greenback.ensure_portal()`.
- fix the frame selection issue where
  `trio._core._ki._ki_protection_decorator:wrapper` seems to be always
  shown on REPL start as the selected frame..
asyncio_debugger_support
Tyler Goodlet 2023-06-21 16:08:18 -04:00
parent ee87cf0e29
commit fc56971a2d
3 changed files with 110 additions and 22 deletions

View File

@ -48,6 +48,9 @@ from ._exceptions import (
)
from ._debug import (
breakpoint,
pause,
pp,
pause_from_sync,
post_mortem,
)
from . import msg
@ -61,12 +64,12 @@ from ._runtime import Actor
__all__ = [
'Actor',
'BaseExceptionGroup',
'Channel',
'Context',
'ContextCancelled',
'ModuleNotExposed',
'MsgStream',
'BaseExceptionGroup',
'Portal',
'RemoteActorError',
'breakpoint',
@ -79,7 +82,10 @@ __all__ = [
'open_actor_cluster',
'open_nursery',
'open_root_actor',
'pause',
'post_mortem',
'pp',
'pause_from_sync'
'query_actor',
'run_daemon',
'stream',

View File

@ -374,7 +374,7 @@ async def wait_for_parent_stdin_hijack(
This function is used by any sub-actor to acquire mutex access to
the ``pdb`` REPL and thus the root's TTY for interactive debugging
(see below inside ``_breakpoint()``). It can be used to ensure that
(see below inside ``_pause()``). It can be used to ensure that
an intermediate nursery-owning actor does not clobber its children
if they are in debug (see below inside
``maybe_wait_for_debugger()``).
@ -440,17 +440,29 @@ def mk_mpdb() -> tuple[MultiActorPdb, Callable]:
return pdb, Lock.unshield_sigint
async def _breakpoint(
async def _pause(
debug_func,
debug_func: Callable | None = None,
release_lock_signal: trio.Event | None = None,
# TODO:
# shield: bool = False
task_status: TaskStatus[trio.Event] = trio.TASK_STATUS_IGNORED
) -> None:
'''
Breakpoint entry for engaging debugger instance sync-interaction,
from async code, executing in actor runtime (task).
A pause point (more commonly known as a "breakpoint") interrupt
instruction for engaging a blocking debugger instance to
conduct manual console-based-REPL-interaction from within
`tractor`'s async runtime, normally from some single-threaded
and currently executing actor-hosted-`trio`-task in some
(remote) process.
NOTE: we use the semantics "pause" since it better encompasses
the entirety of the necessary global-runtime-state-mutation any
actor-task must access and lock in order to get full isolated
control over the process tree's root TTY:
https://en.wikipedia.org/wiki/Breakpoint
'''
__tracebackhide__ = True
@ -559,10 +571,21 @@ async def _breakpoint(
Lock.repl = pdb
try:
# block here one (at the appropriate frame *up*) where
# ``breakpoint()`` was awaited and begin handling stdio.
log.debug("Entering the synchronous world of pdb")
debug_func(actor, pdb)
# breakpoint()
if debug_func is None:
assert release_lock_signal, (
'Must pass `release_lock_signal: trio.Event` if no '
'trace func provided!'
)
print(f"{actor.uid} ENTERING WAIT")
task_status.started()
await release_lock_signal.wait()
else:
# block here one (at the appropriate frame *up*) where
# ``breakpoint()`` was awaited and begin handling stdio.
log.debug("Entering the synchronous world of pdb")
debug_func(actor, pdb)
except bdb.BdbQuit:
Lock.release()
@ -708,8 +731,8 @@ def shield_sigint_handler(
# elif debug_mode():
else: # XXX: shouldn't ever get here?
print("WTFWTFWTF")
raise KeyboardInterrupt
raise RuntimeError("WTFWTFWTF")
# raise KeyboardInterrupt("WTFWTFWTF")
# NOTE: currently (at least on ``fancycompleter`` 0.9.2)
# it looks to be that the last command that was run (eg. ll)
@ -737,21 +760,18 @@ def shield_sigint_handler(
# https://github.com/goodboy/tractor/issues/130#issuecomment-663752040
# https://github.com/prompt-toolkit/python-prompt-toolkit/blob/c2c6af8a0308f9e5d7c0e28cb8a02963fe0ce07a/prompt_toolkit/patch_stdout.py
# XXX LEGACY: lol, see ``pdbpp`` issue:
# https://github.com/pdbpp/pdbpp/issues/496
def _set_trace(
actor: tractor.Actor | None = None,
pdb: MultiActorPdb | None = None,
):
__tracebackhide__ = True
actor = actor or tractor.current_actor()
actor: tractor.Actor = actor or tractor.current_actor()
# start 2 levels up in user code
frame: Optional[FrameType] = sys._getframe()
frame: FrameType | None = sys._getframe()
if frame:
frame = frame.f_back # type: ignore
frame: FrameType = frame.f_back # type: ignore
if (
frame
@ -773,10 +793,66 @@ def _set_trace(
pdb.set_trace(frame=frame)
breakpoint = partial(
_breakpoint,
# TODO: allow pausing from sync code, normally by remapping
# python's builtin breakpoint() hook to this runtime aware version.
def pause_from_sync() -> None:
import greenback
actor: tractor.Actor = tractor.current_actor()
task_can_release_tty_lock = trio.Event()
# spawn bg task which will lock out the TTY, we poll
# just below until the release event is reporting that task as
# waiting.. not the most ideal but works for now ;)
greenback.await_(
actor._service_n.start(partial(
_pause,
debug_func=None,
release_lock_signal=task_can_release_tty_lock,
))
)
print("ENTER SYNC PAUSE")
pdb, undo_sigint = mk_mpdb()
try:
print("ENTER SYNC PAUSE")
# _set_trace(actor=actor)
# we entered the global ``breakpoint()`` built-in from sync
# code?
Lock.local_task_in_debug = 'sync'
frame: FrameType | None = sys._getframe()
print(f'FRAME: {str(frame)}')
frame: FrameType = frame.f_back # type: ignore
print(f'FRAME: {str(frame)}')
frame: FrameType = frame.f_back # type: ignore
print(f'FRAME: {str(frame)}')
pdb.set_trace(frame=frame)
# pdb.do_frame(
# pdb.curindex
finally:
task_can_release_tty_lock.set()
undo_sigint()
# using the "pause" semantics instead since
# that better covers actually somewhat "pausing the runtime"
# for this particular paralell task to do debugging B)
pause = partial(
_pause,
_set_trace,
)
pp = pause # short-hand for "pause point"
async def breakpoint(**kwargs):
log.warning(
'`tractor.breakpoint()` is deprecated!\n'
'Please use `tractor.pause()` instead!\n'
)
await pause(**kwargs)
def _post_mortem(
@ -801,7 +877,7 @@ def _post_mortem(
post_mortem = partial(
_breakpoint,
_pause,
_post_mortem,
)

View File

@ -95,6 +95,10 @@ async def _invoke(
treat_as_gen: bool = False
failed_resp: bool = False
if _state.debug_mode():
import greenback
await greenback.ensure_portal()
# possibly a traceback (not sure what typing is for this..)
tb = None
@ -1862,4 +1866,6 @@ class Arbiter(Actor):
) -> None:
uid = (str(uid[0]), str(uid[1]))
self._registry.pop(uid)
entry: tuple = self._registry.pop(uid, None)
if entry is None:
log.warning(f'Request to de-register {uid} failed?')