Re-impl as `DebugStatus.maybe_enter_repl_fixture()`
Dropping the `_maybe_open_repl_fixture()` approach and instead using a `DebugStatus._fixture_stack = ExitStack()` which provides for much simpler support around both sync and async pausing APIs thanks to only invoking `repl_fixture.__exit__()` on actual `PdbREPL` interaction being complete! Deats, - all `repl_fixture` detection logic still happens in one place (the new method) but we aren't limited to closing it via an immediate post REPL `.__exit__()` call which instead is triggered by, - `DebugStatus.release()` which now calls `._fixture_stack.close()` and thus only invokes `repl_fixture.__exit__()` when user REPL-ing is **actually complete** an arbitrary amount of debugging time later. - include the notes for `@acm` support above the new method, though not sure if they're as relevant any more? Benefits, - we can drop the previously added indent levels from `_enter_repl_sync()` and `_post_mortem()`. - now we automatically have support for the `.pause_from_sync()` API since `_enter_repl_sync()` doesn't close the prior `_maybe_open_repl_fixture()` immediately when `debug_func=None`; the user's `__exit__()` is only ever called once `.release()` is. Other, - add big 'CASE' comments around the various blocks in `.pause_from_sync()`, i was having trouble figuring out which i was using from a `breakpoint()` in a dependent app..repl_fixture
parent
d716c57234
commit
2a37f6abdd
|
@ -64,7 +64,6 @@ from tractor._exceptions import (
|
|||
)
|
||||
from ._trace import (
|
||||
_pause,
|
||||
_maybe_open_repl_fixture,
|
||||
)
|
||||
from ._tty_lock import (
|
||||
DebugStatus,
|
||||
|
@ -143,65 +142,65 @@ def _post_mortem(
|
|||
'''
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
with _maybe_open_repl_fixture(
|
||||
# maybe enter any user fixture
|
||||
enter_repl: bool = DebugStatus.maybe_enter_repl_fixture(
|
||||
repl=repl,
|
||||
repl_fixture=repl_fixture,
|
||||
boxed_maybe_exc=boxed_maybe_exc,
|
||||
) as enter_repl:
|
||||
if not enter_repl:
|
||||
return
|
||||
)
|
||||
if not enter_repl:
|
||||
return
|
||||
try:
|
||||
actor: Actor = current_actor()
|
||||
actor_repr: str = str(actor.uid)
|
||||
# ^TODO, instead a nice runtime-info + maddr + uid?
|
||||
# -[ ] impl a `Actor.__repr()__`??
|
||||
# |_ <task>:<thread> @ <actor>
|
||||
|
||||
try:
|
||||
actor: Actor = current_actor()
|
||||
actor_repr: str = str(actor.uid)
|
||||
# ^TODO, instead a nice runtime-info + maddr + uid?
|
||||
# -[ ] impl a `Actor.__repr()__`??
|
||||
# |_ <task>:<thread> @ <actor>
|
||||
except NoRuntime:
|
||||
actor_repr: str = '<no-actor-runtime?>'
|
||||
|
||||
except NoRuntime:
|
||||
actor_repr: str = '<no-actor-runtime?>'
|
||||
try:
|
||||
task_repr: Task = trio.lowlevel.current_task()
|
||||
except RuntimeError:
|
||||
task_repr: str = '<unknown-Task>'
|
||||
|
||||
try:
|
||||
task_repr: Task = trio.lowlevel.current_task()
|
||||
except RuntimeError:
|
||||
task_repr: str = '<unknown-Task>'
|
||||
# TODO: print the actor supervion tree up to the root
|
||||
# here! Bo
|
||||
log.pdb(
|
||||
f'{_crash_msg}\n'
|
||||
f'x>(\n'
|
||||
f' |_ {task_repr} @ {actor_repr}\n'
|
||||
|
||||
# TODO: print the actor supervion tree up to the root
|
||||
# here! Bo
|
||||
log.pdb(
|
||||
f'{_crash_msg}\n'
|
||||
f'x>(\n'
|
||||
f' |_ {task_repr} @ {actor_repr}\n'
|
||||
)
|
||||
|
||||
)
|
||||
# XXX NOTE(s) on `pdbp.xpm()` version..
|
||||
#
|
||||
# - seems to lose the up-stack tb-info?
|
||||
# - currently we're (only) replacing this from `pdbp.xpm()`
|
||||
# to add the `end=''` to the print XD
|
||||
#
|
||||
print(traceback.format_exc(), end='')
|
||||
caller_frame: FrameType = api_frame.f_back
|
||||
|
||||
# XXX NOTE(s) on `pdbp.xpm()` version..
|
||||
#
|
||||
# - seems to lose the up-stack tb-info?
|
||||
# - currently we're (only) replacing this from `pdbp.xpm()`
|
||||
# to add the `end=''` to the print XD
|
||||
#
|
||||
print(traceback.format_exc(), end='')
|
||||
caller_frame: FrameType = api_frame.f_back
|
||||
# NOTE, see the impl details of these in the lib to
|
||||
# understand usage:
|
||||
# - `pdbp.post_mortem()`
|
||||
# - `pdbp.xps()`
|
||||
# - `bdb.interaction()`
|
||||
repl.reset()
|
||||
repl.interaction(
|
||||
frame=caller_frame,
|
||||
# frame=None,
|
||||
traceback=tb,
|
||||
)
|
||||
|
||||
# NOTE, see the impl details of these in the lib to
|
||||
# understand usage:
|
||||
# - `pdbp.post_mortem()`
|
||||
# - `pdbp.xps()`
|
||||
# - `bdb.interaction()`
|
||||
repl.reset()
|
||||
repl.interaction(
|
||||
frame=caller_frame,
|
||||
# frame=None,
|
||||
traceback=tb,
|
||||
)
|
||||
|
||||
# XXX NOTE XXX: this is abs 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!
|
||||
DebugStatus.release()
|
||||
# XXX NOTE XXX: this is abs 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!
|
||||
DebugStatus.release()
|
||||
|
||||
|
||||
async def post_mortem(
|
||||
|
|
|
@ -28,8 +28,6 @@ import asyncio
|
|||
import bdb
|
||||
from contextlib import (
|
||||
AbstractContextManager,
|
||||
contextmanager as cm,
|
||||
nullcontext,
|
||||
)
|
||||
from functools import (
|
||||
partial,
|
||||
|
@ -37,7 +35,6 @@ from functools import (
|
|||
import inspect
|
||||
import threading
|
||||
from typing import (
|
||||
Iterator,
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
@ -89,7 +86,7 @@ if TYPE_CHECKING:
|
|||
from tractor._runtime import (
|
||||
Actor,
|
||||
)
|
||||
from ._post_mortem import BoxedMaybeException
|
||||
# from ._post_mortem import BoxedMaybeException
|
||||
from ._repl import PdbREPL
|
||||
|
||||
log = get_logger(__package__)
|
||||
|
@ -99,69 +96,6 @@ _repl_fail_msg: str|None = (
|
|||
'Failed to REPl via `_pause()` '
|
||||
)
|
||||
|
||||
# TODO, support @acm?
|
||||
# -[ ] what about a return-proto for determining
|
||||
# whether the REPL should be allowed to enage?
|
||||
# -[ ] consider factoring this `_repl_fixture` block into
|
||||
# a common @cm somehow so it can be repurposed both here and
|
||||
# in `._pause()`??
|
||||
# -[ ] we could also use the `ContextDecorator`-type in that
|
||||
# case to simply decorate the `_enter_repl_sync()` closure?
|
||||
# |_https://docs.python.org/3/library/contextlib.html#using-a-context-manager-as-a-function-decorator
|
||||
@cm
|
||||
def _maybe_open_repl_fixture(
|
||||
repl: PdbREPL,
|
||||
# ^XXX **always provided** by the low-level REPL-invoker,
|
||||
# - _post_mortem()
|
||||
# - _pause()
|
||||
|
||||
repl_fixture: (
|
||||
AbstractContextManager[bool]
|
||||
|None
|
||||
) = None,
|
||||
boxed_maybe_exc: BoxedMaybeException|None = None,
|
||||
) -> Iterator[bool]:
|
||||
'''
|
||||
Maybe open a pre/post REPL entry "fixture" `@cm` provided by the
|
||||
user, the caller should use the delivered `bool` to determine
|
||||
whether to engage the `PdbREPL`.
|
||||
|
||||
'''
|
||||
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
|
||||
)(
|
||||
repl=repl,
|
||||
maybe_bxerr=boxed_maybe_exc
|
||||
)
|
||||
|
||||
with _repl_fixture as enter_repl:
|
||||
|
||||
# XXX when the fixture doesn't allow it, skip
|
||||
# the crash-handler REPL and raise now!
|
||||
if not enter_repl:
|
||||
log.pdb(
|
||||
f'pdbp-REPL blocked by a `repl_fixture()` which yielded `False` !\n'
|
||||
f'repl_fixture: {repl_fixture}\n'
|
||||
f'rt_repl_fixture: {rt_repl_fixture}\n'
|
||||
)
|
||||
yield False # no don't enter REPL
|
||||
return
|
||||
|
||||
yield True # yes enter REPL
|
||||
|
||||
|
||||
|
||||
async def _pause(
|
||||
|
||||
debug_func: Callable|partial|None,
|
||||
|
@ -255,90 +189,86 @@ async def _pause(
|
|||
) -> None:
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
# TODO, support @acm?
|
||||
# -[ ] what about a return-proto for determining
|
||||
# whether the REPL should be allowed to enage?
|
||||
# nonlocal repl_fixture
|
||||
|
||||
with _maybe_open_repl_fixture(
|
||||
# maybe enter any user fixture
|
||||
enter_repl: bool = DebugStatus.maybe_enter_repl_fixture(
|
||||
repl=repl,
|
||||
repl_fixture=repl_fixture,
|
||||
) as enter_repl:
|
||||
if not enter_repl:
|
||||
return
|
||||
)
|
||||
if not enter_repl:
|
||||
return
|
||||
|
||||
debug_func_name: str = (
|
||||
debug_func.func.__name__ if debug_func else 'None'
|
||||
)
|
||||
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'
|
||||
)
|
||||
# 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,
|
||||
# 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'
|
||||
)
|
||||
|
||||
# TODO: maybe invert this logic and instead
|
||||
# do `assert debug_func is None` when
|
||||
# `called_from_sync`?
|
||||
else:
|
||||
if (
|
||||
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'
|
||||
# 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,
|
||||
)
|
||||
# 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)
|
||||
# TODO: maybe invert this logic and instead
|
||||
# do `assert debug_func is None` when
|
||||
# `called_from_sync`?
|
||||
else:
|
||||
if (
|
||||
called_from_sync
|
||||
and
|
||||
not DebugStatus.is_main_trio_thread()
|
||||
):
|
||||
assert called_from_bg_thread
|
||||
assert DebugStatus.repl_task is not task
|
||||
|
||||
raise
|
||||
return (task, repl)
|
||||
|
||||
log.devx(
|
||||
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.debug(
|
||||
'Entering `._pause()` for requesting task\n'
|
||||
f'|_{task}\n'
|
||||
)
|
||||
|
@ -352,7 +282,7 @@ async def _pause(
|
|||
or
|
||||
DebugStatus.repl_release.is_set()
|
||||
):
|
||||
log.devx(
|
||||
log.debug(
|
||||
'Setting new `DebugStatus.repl_release: trio.Event` for requesting task\n'
|
||||
f'|_{task}\n'
|
||||
)
|
||||
|
@ -431,7 +361,7 @@ async def _pause(
|
|||
'with no request ctx !?!?'
|
||||
)
|
||||
|
||||
log.devx(
|
||||
log.debug(
|
||||
f'attempting to {acq_prefix}acquire '
|
||||
f'{ctx_line}'
|
||||
)
|
||||
|
@ -1022,6 +952,8 @@ def pause_from_sync(
|
|||
# noop: non-cancelled `.to_thread`
|
||||
# `trio.Cancelled`: cancelled `.to_thread`
|
||||
|
||||
# CASE: bg-thread spawned via `trio.to_thread`
|
||||
# -----
|
||||
# when called from a (bg) thread, run an async task in a new
|
||||
# thread which will call `._pause()` manually with special
|
||||
# handling for root-actor caller usage.
|
||||
|
@ -1107,6 +1039,9 @@ def pause_from_sync(
|
|||
# '`tractor.pause[_from_sync]()` not yet supported '
|
||||
# 'for infected `asyncio` mode!'
|
||||
# )
|
||||
#
|
||||
# CASE: bg-thread running `asyncio.Task`
|
||||
# -----
|
||||
elif (
|
||||
not is_trio_thread
|
||||
and
|
||||
|
@ -1184,7 +1119,9 @@ def pause_from_sync(
|
|||
f'- greenback.bestow_portal(<task>)\n'
|
||||
)
|
||||
|
||||
else: # we are presumably the `trio.run()` + main thread
|
||||
# CASE: `trio.run()` + "main thread"
|
||||
# -----
|
||||
else:
|
||||
# raises on not-found by default
|
||||
greenback: ModuleType = maybe_import_greenback()
|
||||
|
||||
|
@ -1286,7 +1223,9 @@ def _sync_pause_from_builtin(
|
|||
Proxy call `.pause_from_sync()` but indicate the caller is the
|
||||
`breakpoint()` built-in.
|
||||
|
||||
Note: this assigned to `os.environ['PYTHONBREAKPOINT']` inside `._root`
|
||||
Note: this always assigned to `os.environ['PYTHONBREAKPOINT']`
|
||||
inside `._root.open_root_actor()` whenever `debug_mode=True` is
|
||||
set.
|
||||
|
||||
'''
|
||||
pause_from_sync(
|
||||
|
|
|
@ -22,7 +22,9 @@ Root-actor TTY mutex-locking machinery.
|
|||
from __future__ import annotations
|
||||
import asyncio
|
||||
from contextlib import (
|
||||
AbstractContextManager,
|
||||
asynccontextmanager as acm,
|
||||
ExitStack,
|
||||
)
|
||||
import textwrap
|
||||
import threading
|
||||
|
@ -75,6 +77,9 @@ if TYPE_CHECKING:
|
|||
from ._repl import (
|
||||
PdbREPL,
|
||||
)
|
||||
from ._post_mortem import (
|
||||
BoxedMaybeException,
|
||||
)
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
@ -601,6 +606,10 @@ class DebugStatus:
|
|||
# request.
|
||||
repl: PdbREPL|None = None
|
||||
|
||||
# any `repl_fixture` provided by user are entered and
|
||||
# latered closed on `.release()`
|
||||
_fixture_stack = ExitStack()
|
||||
|
||||
# TODO: yet again this looks like a task outcome where we need
|
||||
# to sync to the completion of one task (and get its result)
|
||||
# being used everywhere for syncing..
|
||||
|
@ -803,6 +812,70 @@ class DebugStatus:
|
|||
|
||||
return False
|
||||
|
||||
# TODO, support @acm?
|
||||
# -[ ] what about a return-proto for determining
|
||||
# whether the REPL should be allowed to enage?
|
||||
# -[x] consider factoring this `_repl_fixture` block into
|
||||
# a common @cm somehow so it can be repurposed both here and
|
||||
# in `._pause()`??
|
||||
# -[ ] we could also use the `ContextDecorator`-type in that
|
||||
# case to simply decorate the `_enter_repl_sync()` closure?
|
||||
# |_https://docs.python.org/3/library/contextlib.html#using-a-context-manager-as-a-function-decorator
|
||||
@classmethod
|
||||
def maybe_enter_repl_fixture(
|
||||
cls,
|
||||
# ^XXX **always provided** by the low-level REPL-invoker,
|
||||
# - _post_mortem()
|
||||
# - _pause()
|
||||
repl: PdbREPL,
|
||||
|
||||
# maybe pre/post REPL entry
|
||||
repl_fixture: (
|
||||
AbstractContextManager[bool]
|
||||
|None
|
||||
) = None,
|
||||
|
||||
# if called from crashed context, provided by
|
||||
# `open_crash_handler()`
|
||||
boxed_maybe_exc: BoxedMaybeException|None = None,
|
||||
) -> bool:
|
||||
'''
|
||||
Maybe open a pre/post REPL entry "fixture" `@cm` provided by the
|
||||
user, the caller should use the delivered `bool` to determine
|
||||
whether to engage the `PdbREPL`.
|
||||
|
||||
'''
|
||||
if not (
|
||||
repl_fixture
|
||||
or
|
||||
(rt_repl_fixture := _state._runtime_vars.get('repl_fixture'))
|
||||
):
|
||||
return True # YES always enter
|
||||
|
||||
_repl_fixture = (
|
||||
repl_fixture
|
||||
or
|
||||
rt_repl_fixture
|
||||
)
|
||||
enter_repl: bool = DebugStatus._fixture_stack.enter_context(
|
||||
_repl_fixture(
|
||||
repl=repl,
|
||||
maybe_bxerr=boxed_maybe_exc,
|
||||
)
|
||||
)
|
||||
if not enter_repl:
|
||||
log.pdb(
|
||||
f'pdbp-REPL blocked by a `repl_fixture()` which yielded `False` !\n'
|
||||
f'repl_fixture: {repl_fixture}\n'
|
||||
f'rt_repl_fixture: {rt_repl_fixture}\n'
|
||||
)
|
||||
|
||||
log.devx(
|
||||
f'User provided `repl_fixture` entered with,\n'
|
||||
f'{repl_fixture!r} -> {enter_repl!r}\n'
|
||||
)
|
||||
return enter_repl
|
||||
|
||||
@classmethod
|
||||
# @pdbp.hideframe
|
||||
def release(
|
||||
|
@ -890,6 +963,8 @@ class DebugStatus:
|
|||
if current_actor(err_on_no_runtime=False):
|
||||
cls.unshield_sigint()
|
||||
|
||||
cls._fixture_stack.close()
|
||||
|
||||
|
||||
# TODO: use the new `@lowlevel.singleton` for this!
|
||||
def get_debug_req() -> DebugStatus|None:
|
||||
|
|
Loading…
Reference in New Issue