Add initial `repl_fixture` support B)
It turns out to be fairly useful to allow hooking into a given actor's entry-and-exit around `.devx._debug._pause/._post_mortem()` calls which engage the `pdbp.Pdb` REPL (really our `._debug.PdbREPL` but yeah). Some very handy use cases include, - swapping out-of-band (config) state that may otherwise halt the user's app since the actor normally handles kb&mouse input, in thread, which means that the handler will be blocked while the REPL is in use. - (remotely) reporting actor-runtime state for monitoring purposes around crashes or pauses in normal operation. - allowing for crash-handling to be hard-disabled via `._state._runtime_vars` say for when you never want a debugger to be entered in a production instance where you're not-sure-if/don't-want per-actor `debug_mode: bool` settings to always be unset, say bc you're still debugging some edge cases that ow you'd normally want to REPL up. Impl details, - add a new optional `._state._runtime_vars['repl_fixture']` field which for now can be manually set; i saw no reason for a formal API yet since we want to convert the `dict` to a struct anyway (first). - augment both `.devx._debug._pause()/._post_mortem()` with a new optional `repl_fixture: AbstractContextManager[bool]` kwarg which when provided is `with repl_fixture()` opened around the lowlevel REPL interaction calls; if the enter-result, an expected `bool`, is `False` then the interaction is hard-bypassed. * for the `._pause()` case the `@cm` is opened around the entire body of the embedded `_enter_repl_sync()` closure (for now) though ideally longer term this entire routine is factored to be a lot less "nested" Bp * in `_post_mortem()` the entire previous body is wrapped similarly and also now excepts an optional `boxed_maybe_exc: BoxedMaybeException` only passed in the `open_crash_handler()` caller case. - when the new runtime-var is overridden, (only manually atm) it is used instead but only whenever the above `repl_fixture` kwarg is left null. - add a `BoxedMaybeException.pformat() = __repr__()` which when a `.value: Exception` is set renders a more "objecty" repr of the exc. Obviously tests for all this should be coming soon!repl_fixture
parent
2bb33da9c8
commit
f604c8836d
|
@ -41,16 +41,24 @@ _current_actor: Actor|None = None # type: ignore # noqa
|
||||||
_last_actor_terminated: Actor|None = None
|
_last_actor_terminated: Actor|None = None
|
||||||
|
|
||||||
# TODO: mk this a `msgspec.Struct`!
|
# TODO: mk this a `msgspec.Struct`!
|
||||||
|
# -[ ] type out all fields obvi!
|
||||||
|
# -[ ] (eventually) mk wire-ready for monitoring?
|
||||||
_runtime_vars: dict[str, Any] = {
|
_runtime_vars: dict[str, Any] = {
|
||||||
'_debug_mode': False,
|
# root of actor-process tree info
|
||||||
'_is_root': False,
|
'_is_root': False, # bool
|
||||||
'_root_mailbox': (None, None),
|
'_root_mailbox': (None, None), # tuple[str|None, str|None]
|
||||||
|
|
||||||
|
# registrar info
|
||||||
'_registry_addrs': [],
|
'_registry_addrs': [],
|
||||||
|
|
||||||
'_is_infected_aio': False,
|
# `debug_mode: bool` settings
|
||||||
|
'_debug_mode': False, # bool
|
||||||
|
'repl_fixture': False, # |AbstractContextManager[bool]
|
||||||
# for `tractor.pause_from_sync()` & `breakpoint()` support
|
# for `tractor.pause_from_sync()` & `breakpoint()` support
|
||||||
'use_greenback': False,
|
'use_greenback': False,
|
||||||
|
|
||||||
|
# infected-`asyncio`-mode: `trio` running as guest.
|
||||||
|
'_is_infected_aio': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import bdb
|
import bdb
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
|
AbstractContextManager,
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
contextmanager as cm,
|
contextmanager as cm,
|
||||||
nullcontext,
|
nullcontext,
|
||||||
|
@ -1774,6 +1775,13 @@ async def _pause(
|
||||||
tuple[Task, PdbREPL],
|
tuple[Task, PdbREPL],
|
||||||
trio.Event
|
trio.Event
|
||||||
] = trio.TASK_STATUS_IGNORED,
|
] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
|
# maybe pre/post REPL entry
|
||||||
|
repl_fixture: (
|
||||||
|
AbstractContextManager[bool]
|
||||||
|
|None
|
||||||
|
) = None,
|
||||||
|
|
||||||
**debug_func_kwargs,
|
**debug_func_kwargs,
|
||||||
|
|
||||||
) -> tuple[Task, PdbREPL]|None:
|
) -> tuple[Task, PdbREPL]|None:
|
||||||
|
@ -1836,76 +1844,103 @@ async def _pause(
|
||||||
debug_func: partial[None],
|
debug_func: partial[None],
|
||||||
) -> None:
|
) -> None:
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
debug_func_name: str = (
|
|
||||||
debug_func.func.__name__ if debug_func else 'None'
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: do we want to support using this **just** for the
|
# TODO, support @acm?
|
||||||
# locking / common code (prolly to help address #320)?
|
# -[ ] what about a return-proto for determining
|
||||||
task_status.started((task, repl))
|
# whether the REPL should be allowed to enage?
|
||||||
try:
|
nonlocal repl_fixture
|
||||||
if debug_func:
|
if not (
|
||||||
# block here one (at the appropriate frame *up*) where
|
repl_fixture
|
||||||
# ``breakpoint()`` was awaited and begin handling stdio.
|
or
|
||||||
log.devx(
|
(rt_repl_fixture := _state._runtime_vars.get('repl_fixture'))
|
||||||
'Entering sync world of the `pdb` REPL for task..\n'
|
):
|
||||||
f'{repl}\n'
|
repl_fixture = nullcontext(
|
||||||
f' |_{task}\n'
|
enter_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# set local task on process-global state to avoid
|
_repl_fixture = repl_fixture or rt_repl_fixture
|
||||||
# recurrent entries/requests from the same
|
with _repl_fixture(maybe_bxerr=None) as enter_repl:
|
||||||
# actor-local task.
|
|
||||||
DebugStatus.repl_task = task
|
# XXX when the fixture doesn't allow it, skip
|
||||||
if repl:
|
# the crash-handler REPL and raise now!
|
||||||
DebugStatus.repl = repl
|
if not enter_repl:
|
||||||
else:
|
log.pdb(
|
||||||
log.error(
|
f'pdbp-REPL blocked by a `repl_fixture()` which yielded `False` !\n'
|
||||||
'No REPl instance set before entering `debug_func`?\n'
|
f'repl_fixture: {repl_fixture}\n'
|
||||||
f'{debug_func}\n'
|
f'rt_repl_fixture: {rt_repl_fixture}\n'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
debug_func_name: str = (
|
||||||
|
debug_func.func.__name__ if debug_func else 'None'
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: do we want to support using this **just** for the
|
||||||
|
# locking / common code (prolly to help address #320)?
|
||||||
|
task_status.started((task, repl))
|
||||||
|
try:
|
||||||
|
if debug_func:
|
||||||
|
# block here one (at the appropriate frame *up*) where
|
||||||
|
# ``breakpoint()`` was awaited and begin handling stdio.
|
||||||
|
log.devx(
|
||||||
|
'Entering sync world of the `pdb` REPL for task..\n'
|
||||||
|
f'{repl}\n'
|
||||||
|
f' |_{task}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# set local task on process-global state to avoid
|
||||||
|
# recurrent entries/requests from the same
|
||||||
|
# actor-local task.
|
||||||
|
DebugStatus.repl_task = task
|
||||||
|
if repl:
|
||||||
|
DebugStatus.repl = repl
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
'No REPl instance set before entering `debug_func`?\n'
|
||||||
|
f'{debug_func}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# invoke the low-level REPL activation routine which itself
|
||||||
|
# should call into a `Pdb.set_trace()` of some sort.
|
||||||
|
debug_func(
|
||||||
|
repl=repl,
|
||||||
|
hide_tb=hide_tb,
|
||||||
|
**debug_func_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# invoke the low-level REPL activation routine which itself
|
# TODO: maybe invert this logic and instead
|
||||||
# should call into a `Pdb.set_trace()` of some sort.
|
# do `assert debug_func is None` when
|
||||||
debug_func(
|
# `called_from_sync`?
|
||||||
repl=repl,
|
else:
|
||||||
hide_tb=hide_tb,
|
if (
|
||||||
**debug_func_kwargs,
|
called_from_sync
|
||||||
|
and
|
||||||
|
not DebugStatus.is_main_trio_thread()
|
||||||
|
):
|
||||||
|
assert called_from_bg_thread
|
||||||
|
assert DebugStatus.repl_task is not task
|
||||||
|
|
||||||
|
return (task, repl)
|
||||||
|
|
||||||
|
except trio.Cancelled:
|
||||||
|
log.exception(
|
||||||
|
'Cancelled during invoke of internal\n\n'
|
||||||
|
f'`debug_func = {debug_func_name}`\n'
|
||||||
)
|
)
|
||||||
|
# XXX NOTE: DON'T release lock yet
|
||||||
|
raise
|
||||||
|
|
||||||
# TODO: maybe invert this logic and instead
|
except BaseException:
|
||||||
# do `assert debug_func is None` when
|
__tracebackhide__: bool = False
|
||||||
# `called_from_sync`?
|
log.exception(
|
||||||
else:
|
'Failed to invoke internal\n\n'
|
||||||
if (
|
f'`debug_func = {debug_func_name}`\n'
|
||||||
called_from_sync
|
)
|
||||||
and
|
# NOTE: OW this is ONLY called from the
|
||||||
not DebugStatus.is_main_trio_thread()
|
# `.set_continue/next` hooks!
|
||||||
):
|
DebugStatus.release(cancel_req_task=True)
|
||||||
assert called_from_bg_thread
|
|
||||||
assert DebugStatus.repl_task is not task
|
|
||||||
|
|
||||||
return (task, repl)
|
raise
|
||||||
|
|
||||||
except trio.Cancelled:
|
|
||||||
log.exception(
|
|
||||||
'Cancelled during invoke of internal\n\n'
|
|
||||||
f'`debug_func = {debug_func_name}`\n'
|
|
||||||
)
|
|
||||||
# XXX NOTE: DON'T release lock yet
|
|
||||||
raise
|
|
||||||
|
|
||||||
except BaseException:
|
|
||||||
__tracebackhide__: bool = False
|
|
||||||
log.exception(
|
|
||||||
'Failed to invoke internal\n\n'
|
|
||||||
f'`debug_func = {debug_func_name}`\n'
|
|
||||||
)
|
|
||||||
# NOTE: OW this is ONLY called from the
|
|
||||||
# `.set_continue/next` hooks!
|
|
||||||
DebugStatus.release(cancel_req_task=True)
|
|
||||||
|
|
||||||
raise
|
|
||||||
|
|
||||||
log.devx(
|
log.devx(
|
||||||
'Entering `._pause()` for requesting task\n'
|
'Entering `._pause()` for requesting task\n'
|
||||||
|
@ -2899,6 +2934,14 @@ def _post_mortem(
|
||||||
shield: bool = False,
|
shield: bool = False,
|
||||||
hide_tb: bool = False,
|
hide_tb: bool = False,
|
||||||
|
|
||||||
|
# maybe pre/post REPL entry
|
||||||
|
repl_fixture: (
|
||||||
|
AbstractContextManager[bool]
|
||||||
|
|None
|
||||||
|
) = None,
|
||||||
|
|
||||||
|
boxed_maybe_exc: BoxedMaybeException|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Enter the ``pdbpp`` port mortem entrypoint using our custom
|
Enter the ``pdbpp`` port mortem entrypoint using our custom
|
||||||
|
@ -2906,55 +2949,81 @@ def _post_mortem(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
try:
|
|
||||||
actor: tractor.Actor = current_actor()
|
|
||||||
actor_repr: str = str(actor.uid)
|
|
||||||
# ^TODO, instead a nice runtime-info + maddr + uid?
|
|
||||||
# -[ ] impl a `Actor.__repr()__`??
|
|
||||||
# |_ <task>:<thread> @ <actor>
|
|
||||||
# no_runtime: bool = False
|
|
||||||
|
|
||||||
except NoRuntime:
|
# TODO, support @acm?
|
||||||
actor_repr: str = '<no-actor-runtime?>'
|
# -[ ] what about a return-proto for determining
|
||||||
# no_runtime: bool = True
|
# whether the REPL should be allowed to enage?
|
||||||
|
if not (
|
||||||
|
repl_fixture
|
||||||
|
or
|
||||||
|
(rt_repl_fixture := _state._runtime_vars.get('repl_fixture'))
|
||||||
|
):
|
||||||
|
_repl_fixture = nullcontext(
|
||||||
|
enter_result=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_repl_fixture = (repl_fixture or rt_repl_fixture)(maybe_bxerr=boxed_maybe_exc)
|
||||||
|
|
||||||
try:
|
with _repl_fixture as enter_repl:
|
||||||
task_repr: Task = current_task()
|
|
||||||
except RuntimeError:
|
|
||||||
task_repr: str = '<unknown-Task>'
|
|
||||||
|
|
||||||
# TODO: print the actor supervion tree up to the root
|
# XXX when the fixture doesn't allow it, skip
|
||||||
# here! Bo
|
# the crash-handler REPL and raise now!
|
||||||
log.pdb(
|
if not enter_repl:
|
||||||
f'{_crash_msg}\n'
|
log.pdb(
|
||||||
f'x>(\n'
|
f'pdbp-REPL blocked by a `repl_fixture()` which yielded `False` !\n'
|
||||||
f' |_ {task_repr} @ {actor_repr}\n'
|
f'repl_fixture: {repl_fixture}\n'
|
||||||
|
f'rt_repl_fixture: {rt_repl_fixture}\n'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
)
|
try:
|
||||||
|
actor: tractor.Actor = current_actor()
|
||||||
|
actor_repr: str = str(actor.uid)
|
||||||
|
# ^TODO, instead a nice runtime-info + maddr + uid?
|
||||||
|
# -[ ] impl a `Actor.__repr()__`??
|
||||||
|
# |_ <task>:<thread> @ <actor>
|
||||||
|
# no_runtime: bool = False
|
||||||
|
|
||||||
# NOTE only replacing this from `pdbp.xpm()` to add the
|
except NoRuntime:
|
||||||
# `end=''` to the print XD
|
actor_repr: str = '<no-actor-runtime?>'
|
||||||
print(traceback.format_exc(), end='')
|
# no_runtime: bool = True
|
||||||
|
|
||||||
caller_frame: FrameType = api_frame.f_back
|
try:
|
||||||
|
task_repr: Task = current_task()
|
||||||
|
except RuntimeError:
|
||||||
|
task_repr: str = '<unknown-Task>'
|
||||||
|
|
||||||
# NOTE: see the impl details of followings to understand usage:
|
# TODO: print the actor supervion tree up to the root
|
||||||
# - `pdbp.post_mortem()`
|
# here! Bo
|
||||||
# - `pdbp.xps()`
|
log.pdb(
|
||||||
# - `bdb.interaction()`
|
f'{_crash_msg}\n'
|
||||||
repl.reset()
|
f'x>(\n'
|
||||||
repl.interaction(
|
f' |_ {task_repr} @ {actor_repr}\n'
|
||||||
frame=caller_frame,
|
|
||||||
# frame=None,
|
)
|
||||||
traceback=tb,
|
|
||||||
)
|
# NOTE only replacing this from `pdbp.xpm()` to add the
|
||||||
# XXX NOTE XXX: absolutely required to avoid hangs!
|
# `end=''` to the print XD
|
||||||
# Since we presume the post-mortem was enaged to a task-ending
|
print(traceback.format_exc(), end='')
|
||||||
# error, we MUST release the local REPL request so that not other
|
caller_frame: FrameType = api_frame.f_back
|
||||||
# local task nor the root remains blocked!
|
|
||||||
# if not no_runtime:
|
# NOTE: see the impl details of followings to understand usage:
|
||||||
# DebugStatus.release()
|
# - `pdbp.post_mortem()`
|
||||||
DebugStatus.release()
|
# - `pdbp.xps()`
|
||||||
|
# - `bdb.interaction()`
|
||||||
|
repl.reset()
|
||||||
|
repl.interaction(
|
||||||
|
frame=caller_frame,
|
||||||
|
# frame=None,
|
||||||
|
traceback=tb,
|
||||||
|
)
|
||||||
|
# XXX NOTE XXX: absolutely required to avoid hangs!
|
||||||
|
# Since we presume the post-mortem was enaged to a task-ending
|
||||||
|
# error, we MUST release the local REPL request so that not other
|
||||||
|
# local task nor the root remains blocked!
|
||||||
|
# if not no_runtime:
|
||||||
|
# DebugStatus.release()
|
||||||
|
DebugStatus.release()
|
||||||
|
|
||||||
|
|
||||||
async def post_mortem(
|
async def post_mortem(
|
||||||
|
@ -3210,6 +3279,23 @@ class BoxedMaybeException(Struct):
|
||||||
'''
|
'''
|
||||||
value: BaseException|None = None
|
value: BaseException|None = None
|
||||||
|
|
||||||
|
def pformat(self) -> str:
|
||||||
|
'''
|
||||||
|
Repr the boxed `.value` error in more-than-string
|
||||||
|
repr form.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if not self.value:
|
||||||
|
return f'<{type(self).__name__}( .value=None )>\n'
|
||||||
|
|
||||||
|
return (
|
||||||
|
f'<{type(self.value).__name__}(\n'
|
||||||
|
f' |_.value = {self.value}\n'
|
||||||
|
f')>\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
__repr__ = pformat
|
||||||
|
|
||||||
|
|
||||||
# TODO: better naming and what additionals?
|
# TODO: better naming and what additionals?
|
||||||
# - [ ] optional runtime plugging?
|
# - [ ] optional runtime plugging?
|
||||||
|
@ -3226,7 +3312,12 @@ def open_crash_handler(
|
||||||
KeyboardInterrupt,
|
KeyboardInterrupt,
|
||||||
trio.Cancelled,
|
trio.Cancelled,
|
||||||
},
|
},
|
||||||
tb_hide: bool = True,
|
tb_hide: bool = False,
|
||||||
|
|
||||||
|
repl_fixture: (
|
||||||
|
AbstractContextManager[bool] # pre/post REPL entry
|
||||||
|
|None
|
||||||
|
) = None,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Generic "post mortem" crash handler using `pdbp` REPL debugger.
|
Generic "post mortem" crash handler using `pdbp` REPL debugger.
|
||||||
|
@ -3266,6 +3357,9 @@ def open_crash_handler(
|
||||||
repl=mk_pdb(),
|
repl=mk_pdb(),
|
||||||
tb=sys.exc_info()[2],
|
tb=sys.exc_info()[2],
|
||||||
api_frame=inspect.currentframe().f_back,
|
api_frame=inspect.currentframe().f_back,
|
||||||
|
|
||||||
|
repl_fixture=repl_fixture,
|
||||||
|
boxed_maybe_exc=boxed_maybe_exc,
|
||||||
)
|
)
|
||||||
except bdb.BdbQuit:
|
except bdb.BdbQuit:
|
||||||
__tracebackhide__: bool = False
|
__tracebackhide__: bool = False
|
||||||
|
@ -3281,7 +3375,7 @@ def open_crash_handler(
|
||||||
@cm
|
@cm
|
||||||
def maybe_open_crash_handler(
|
def maybe_open_crash_handler(
|
||||||
pdb: bool|None = None,
|
pdb: bool|None = None,
|
||||||
tb_hide: bool = True,
|
tb_hide: bool = False,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
@ -3293,11 +3387,11 @@ def maybe_open_crash_handler(
|
||||||
flag is passed the pdb REPL is engaed on any crashes B)
|
flag is passed the pdb REPL is engaed on any crashes B)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
__tracebackhide__: bool = tb_hide
|
||||||
|
|
||||||
if pdb is None:
|
if pdb is None:
|
||||||
pdb: bool = _state.is_debug_mode()
|
pdb: bool = _state.is_debug_mode()
|
||||||
|
|
||||||
__tracebackhide__: bool = tb_hide
|
|
||||||
|
|
||||||
rtctx = nullcontext(
|
rtctx = nullcontext(
|
||||||
enter_result=BoxedMaybeException()
|
enter_result=BoxedMaybeException()
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue