diff --git a/tractor/devx/debug/_post_mortem.py b/tractor/devx/debug/_post_mortem.py index db2d67fe..2ab5dd7f 100644 --- a/tractor/devx/debug/_post_mortem.py +++ b/tractor/devx/debug/_post_mortem.py @@ -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()__`?? + # |_ : @ - try: - actor: Actor = current_actor() - actor_repr: str = str(actor.uid) - # ^TODO, instead a nice runtime-info + maddr + uid? - # -[ ] impl a `Actor.__repr()__`?? - # |_ : @ + except NoRuntime: + actor_repr: str = '' - except NoRuntime: - actor_repr: str = '' + try: + task_repr: Task = trio.lowlevel.current_task() + except RuntimeError: + task_repr: str = '' - try: - task_repr: Task = trio.lowlevel.current_task() - except RuntimeError: - task_repr: str = '' + # 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( diff --git a/tractor/devx/debug/_trace.py b/tractor/devx/debug/_trace.py index 1c02d379..70d39325 100644 --- a/tractor/devx/debug/_trace.py +++ b/tractor/devx/debug/_trace.py @@ -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()\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( diff --git a/tractor/devx/debug/_tty_lock.py b/tractor/devx/debug/_tty_lock.py index 08f0daf7..78f5d16a 100644 --- a/tractor/devx/debug/_tty_lock.py +++ b/tractor/devx/debug/_tty_lock.py @@ -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: