Start splitting into `devx.debug.` sub-mods
From what was originall the `.devx._debug` monolith module, since that file was way out of ctl in terms of LoC! New modules so far include, - ._repl: our `pdb[p]` ext type/lowlevel-APIs and `mk_pdb()` factory. - ._sigint: just our REPL-interaction shield-handler. - ._tty_lock: containing all the root-actor TTY mutex machinery including the `Lock`/`DebugStatus` primitives/APIs as well as the inter-tree IPC context eps: * the server-side `lock_stdio_for_peer()` which pairs with the, * client-(subactor)-side `request_root_stdio_lock()` via the, * pld-msg-spec of `LockStatus/LockRelease`. AND the `any_connected_locker_child()` predicate.repl_fixture
							parent
							
								
									4f6a9c62c6
								
							
						
					
					
						commit
						f6513bb2cf
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -0,0 +1,207 @@
 | 
				
			||||||
 | 
					# tractor: structured concurrent "actors".
 | 
				
			||||||
 | 
					# Copyright 2018-eternity Tyler Goodlet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This program is free software: you can redistribute it and/or
 | 
				
			||||||
 | 
					# modify it under the terms of the GNU Affero General Public License
 | 
				
			||||||
 | 
					# as published by the Free Software Foundation, either version 3 of
 | 
				
			||||||
 | 
					# the License, or (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This program is distributed in the hope that it will be useful, but
 | 
				
			||||||
 | 
					# WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
				
			||||||
 | 
					# Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# You should have received a copy of the GNU Affero General Public
 | 
				
			||||||
 | 
					# License along with this program.  If not, see
 | 
				
			||||||
 | 
					# <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'''
 | 
				
			||||||
 | 
					`pdpp.Pdb` extentions/customization and other delegate usage.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'''
 | 
				
			||||||
 | 
					from functools import (
 | 
				
			||||||
 | 
					    cached_property,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pdbp
 | 
				
			||||||
 | 
					from tractor._state import (
 | 
				
			||||||
 | 
					    is_root_process,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._tty_lock import (
 | 
				
			||||||
 | 
					    Lock,
 | 
				
			||||||
 | 
					    DebugStatus,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TractorConfig(pdbp.DefaultConfig):
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Custom `pdbp` config which tries to use the best tradeoff
 | 
				
			||||||
 | 
					    between pretty and minimal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    use_pygments: bool = True
 | 
				
			||||||
 | 
					    sticky_by_default: bool = False
 | 
				
			||||||
 | 
					    enable_hidden_frames: bool = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # much thanks @mdmintz for the hot tip!
 | 
				
			||||||
 | 
					    # fixes line spacing issue when resizing terminal B)
 | 
				
			||||||
 | 
					    truncate_long_lines: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ------ - ------
 | 
				
			||||||
 | 
					    # our own custom config vars mostly
 | 
				
			||||||
 | 
					    # for syncing with the actor tree's singleton
 | 
				
			||||||
 | 
					    # TTY `Lock`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PdbREPL(pdbp.Pdb):
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Add teardown hooks and local state describing any
 | 
				
			||||||
 | 
					    ongoing TTY `Lock` request dialog.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    # override the pdbp config with our coolio one
 | 
				
			||||||
 | 
					    # NOTE: this is only loaded when no `~/.pdbrc` exists
 | 
				
			||||||
 | 
					    # so we should prolly pass it into the .__init__() instead?
 | 
				
			||||||
 | 
					    # i dunno, see the `DefaultFactory` and `pdb.Pdb` impls.
 | 
				
			||||||
 | 
					    DefaultConfig = TractorConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    status = DebugStatus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE: see details in stdlib's `bdb.py`
 | 
				
			||||||
 | 
					    # def user_exception(self, frame, exc_info):
 | 
				
			||||||
 | 
					    #     '''
 | 
				
			||||||
 | 
					    #     Called when we stop on an exception.
 | 
				
			||||||
 | 
					    #     '''
 | 
				
			||||||
 | 
					    #     log.warning(
 | 
				
			||||||
 | 
					    #         'Exception during REPL sesh\n\n'
 | 
				
			||||||
 | 
					    #         f'{frame}\n\n'
 | 
				
			||||||
 | 
					    #         f'{exc_info}\n\n'
 | 
				
			||||||
 | 
					    #     )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE: this actually hooks but i don't see anyway to detect
 | 
				
			||||||
 | 
					    # if an error was caught.. this is why currently we just always
 | 
				
			||||||
 | 
					    # call `DebugStatus.release` inside `_post_mortem()`.
 | 
				
			||||||
 | 
					    # def preloop(self):
 | 
				
			||||||
 | 
					    #     print('IN PRELOOP')
 | 
				
			||||||
 | 
					    #     super().preloop()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: cleaner re-wrapping of all this?
 | 
				
			||||||
 | 
					    # -[ ] figure out how to disallow recursive .set_trace() entry
 | 
				
			||||||
 | 
					    #     since that'll cause deadlock for us.
 | 
				
			||||||
 | 
					    # -[ ] maybe a `@cm` to call `super().<same_meth_name>()`?
 | 
				
			||||||
 | 
					    # -[ ] look at hooking into the `pp` hook specially with our
 | 
				
			||||||
 | 
					    #     own set of pretty-printers?
 | 
				
			||||||
 | 
					    #    * `.pretty_struct.Struct.pformat()`
 | 
				
			||||||
 | 
					    #    * `.pformat(MsgType.pld)`
 | 
				
			||||||
 | 
					    #    * `.pformat(Error.tb_str)`?
 | 
				
			||||||
 | 
					    #    * .. maybe more?
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    def set_continue(self):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            super().set_continue()
 | 
				
			||||||
 | 
					        finally:
 | 
				
			||||||
 | 
					            # NOTE: for subactors the stdio lock is released via the
 | 
				
			||||||
 | 
					            # allocated RPC locker task, so for root we have to do it
 | 
				
			||||||
 | 
					            # manually.
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                is_root_process()
 | 
				
			||||||
 | 
					                and
 | 
				
			||||||
 | 
					                Lock._debug_lock.locked()
 | 
				
			||||||
 | 
					                and
 | 
				
			||||||
 | 
					                DebugStatus.is_main_trio_thread()
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                # Lock.release(raise_on_thread=False)
 | 
				
			||||||
 | 
					                Lock.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # XXX AFTER `Lock.release()` for root local repl usage
 | 
				
			||||||
 | 
					            DebugStatus.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_quit(self):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            super().set_quit()
 | 
				
			||||||
 | 
					        finally:
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                is_root_process()
 | 
				
			||||||
 | 
					                and
 | 
				
			||||||
 | 
					                Lock._debug_lock.locked()
 | 
				
			||||||
 | 
					                and
 | 
				
			||||||
 | 
					                DebugStatus.is_main_trio_thread()
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                # Lock.release(raise_on_thread=False)
 | 
				
			||||||
 | 
					                Lock.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # XXX after `Lock.release()` for root local repl usage
 | 
				
			||||||
 | 
					            DebugStatus.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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def mk_pdb() -> PdbREPL:
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Deliver a new `PdbREPL`: a multi-process safe `pdbp.Pdb`-variant
 | 
				
			||||||
 | 
					    using the magic of `tractor`'s SC-safe IPC.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    B)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Our `pdb.Pdb` subtype accomplishes multi-process safe debugging
 | 
				
			||||||
 | 
					    by:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - mutexing access to the root process' std-streams (& thus parent
 | 
				
			||||||
 | 
					      process TTY) via an IPC managed `Lock` singleton per
 | 
				
			||||||
 | 
					      actor-process tree.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - temporarily overriding any subactor's SIGINT handler to shield
 | 
				
			||||||
 | 
					      during live REPL sessions in sub-actors such that cancellation
 | 
				
			||||||
 | 
					      is never (mistakenly) triggered by a ctrl-c and instead only by
 | 
				
			||||||
 | 
					      explicit runtime API requests or after the
 | 
				
			||||||
 | 
					      `pdb.Pdb.interaction()` call has returned.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FURTHER, the `pdbp.Pdb` instance is configured to be `trio`
 | 
				
			||||||
 | 
					    "compatible" from a SIGINT handling perspective; we mask out
 | 
				
			||||||
 | 
					    the default `pdb` handler and instead apply `trio`s default
 | 
				
			||||||
 | 
					    which mostly addresses all issues described in:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     - https://github.com/python-trio/trio/issues/1155
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The instance returned from this factory should always be
 | 
				
			||||||
 | 
					    preferred over the default `pdb[p].set_trace()` whenever using
 | 
				
			||||||
 | 
					    a `pdb` REPL inside a `trio` based runtime.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pdb = PdbREPL()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # XXX: These are the important flags mentioned in
 | 
				
			||||||
 | 
					    # https://github.com/python-trio/trio/issues/1155
 | 
				
			||||||
 | 
					    # which resolve the traceback spews to console.
 | 
				
			||||||
 | 
					    pdb.allow_kbdint = True
 | 
				
			||||||
 | 
					    pdb.nosigint = True
 | 
				
			||||||
 | 
					    return pdb
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,333 @@
 | 
				
			||||||
 | 
					# tractor: structured concurrent "actors".
 | 
				
			||||||
 | 
					# Copyright 2018-eternity Tyler Goodlet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This program is free software: you can redistribute it and/or
 | 
				
			||||||
 | 
					# modify it under the terms of the GNU Affero General Public License
 | 
				
			||||||
 | 
					# as published by the Free Software Foundation, either version 3 of
 | 
				
			||||||
 | 
					# the License, or (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This program is distributed in the hope that it will be useful, but
 | 
				
			||||||
 | 
					# WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
				
			||||||
 | 
					# Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# You should have received a copy of the GNU Affero General Public
 | 
				
			||||||
 | 
					# License along with this program.  If not, see
 | 
				
			||||||
 | 
					# <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'''
 | 
				
			||||||
 | 
					A custom SIGINT handler which mainly shields actor (task)
 | 
				
			||||||
 | 
					cancellation during REPL interaction.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'''
 | 
				
			||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					from typing import (
 | 
				
			||||||
 | 
					    TYPE_CHECKING,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					import trio
 | 
				
			||||||
 | 
					from tractor.log import get_logger
 | 
				
			||||||
 | 
					from tractor._state import (
 | 
				
			||||||
 | 
					    current_actor,
 | 
				
			||||||
 | 
					    is_root_process,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from ._repl import (
 | 
				
			||||||
 | 
					    PdbREPL,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from ._tty_lock import (
 | 
				
			||||||
 | 
					    any_connected_locker_child,
 | 
				
			||||||
 | 
					    DebugStatus,
 | 
				
			||||||
 | 
					    Lock,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
 | 
					    from tractor.ipc import (
 | 
				
			||||||
 | 
					        Channel,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    from tractor._runtime import (
 | 
				
			||||||
 | 
					        Actor,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log = get_logger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_ctlc_ignore_header: str = (
 | 
				
			||||||
 | 
					    'Ignoring SIGINT while debug REPL in use'
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def sigint_shield(
 | 
				
			||||||
 | 
					    signum: int,
 | 
				
			||||||
 | 
					    frame: 'frame',  # type: ignore # noqa
 | 
				
			||||||
 | 
					    *args,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					) -> None:
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Specialized, debugger-aware SIGINT handler.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    In childred we always ignore/shield for SIGINT to avoid
 | 
				
			||||||
 | 
					    deadlocks since cancellation should always be managed by the
 | 
				
			||||||
 | 
					    supervising parent actor. The root actor-proces is always
 | 
				
			||||||
 | 
					    cancelled on ctrl-c.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    __tracebackhide__: bool = True
 | 
				
			||||||
 | 
					    actor: Actor = current_actor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def do_cancel():
 | 
				
			||||||
 | 
					        # If we haven't tried to cancel the runtime then do that instead
 | 
				
			||||||
 | 
					        # of raising a KBI (which may non-gracefully destroy
 | 
				
			||||||
 | 
					        # a ``trio.run()``).
 | 
				
			||||||
 | 
					        if not actor._cancel_called:
 | 
				
			||||||
 | 
					            actor.cancel_soon()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If the runtime is already cancelled it likely means the user
 | 
				
			||||||
 | 
					        # hit ctrl-c again because teardown didn't fully take place in
 | 
				
			||||||
 | 
					        # which case we do the "hard" raising of a local KBI.
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            raise KeyboardInterrupt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # only set in the actor actually running the REPL
 | 
				
			||||||
 | 
					    repl: PdbREPL|None = DebugStatus.repl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: maybe we should flatten out all these cases using
 | 
				
			||||||
 | 
					    # a match/case?
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # root actor branch that reports whether or not a child
 | 
				
			||||||
 | 
					    # has locked debugger.
 | 
				
			||||||
 | 
					    if is_root_process():
 | 
				
			||||||
 | 
					        # log.warning(
 | 
				
			||||||
 | 
					        log.devx(
 | 
				
			||||||
 | 
					            'Handling SIGINT in root actor\n'
 | 
				
			||||||
 | 
					            f'{Lock.repr()}'
 | 
				
			||||||
 | 
					            f'{DebugStatus.repr()}\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        # try to see if the supposed (sub)actor in debug still
 | 
				
			||||||
 | 
					        # has an active connection to *this* actor, and if not
 | 
				
			||||||
 | 
					        # it's likely they aren't using the TTY lock / debugger
 | 
				
			||||||
 | 
					        # and we should propagate SIGINT normally.
 | 
				
			||||||
 | 
					        any_connected: bool = any_connected_locker_child()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        problem = (
 | 
				
			||||||
 | 
					            f'root {actor.uid} handling SIGINT\n'
 | 
				
			||||||
 | 
					            f'any_connected: {any_connected}\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            f'{Lock.repr()}\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            (ctx := Lock.ctx_in_debug)
 | 
				
			||||||
 | 
					            and
 | 
				
			||||||
 | 
					            (uid_in_debug := ctx.chan.uid) # "someone" is (ostensibly) using debug `Lock`
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            name_in_debug: str = uid_in_debug[0]
 | 
				
			||||||
 | 
					            assert not repl
 | 
				
			||||||
 | 
					            # if not repl:  # but it's NOT us, the root actor.
 | 
				
			||||||
 | 
					            # sanity: since no repl ref is set, we def shouldn't
 | 
				
			||||||
 | 
					            # be the lock owner!
 | 
				
			||||||
 | 
					            assert name_in_debug != 'root'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # IDEAL CASE: child has REPL as expected
 | 
				
			||||||
 | 
					            if any_connected:  # there are subactors we can contact
 | 
				
			||||||
 | 
					                # XXX: only if there is an existing connection to the
 | 
				
			||||||
 | 
					                # (sub-)actor in debug do we ignore SIGINT in this
 | 
				
			||||||
 | 
					                # parent! Otherwise we may hang waiting for an actor
 | 
				
			||||||
 | 
					                # which has already terminated to unlock.
 | 
				
			||||||
 | 
					                #
 | 
				
			||||||
 | 
					                # NOTE: don't emit this with `.pdb()` level in
 | 
				
			||||||
 | 
					                # root without a higher level.
 | 
				
			||||||
 | 
					                log.runtime(
 | 
				
			||||||
 | 
					                    _ctlc_ignore_header
 | 
				
			||||||
 | 
					                    +
 | 
				
			||||||
 | 
					                    f' by child '
 | 
				
			||||||
 | 
					                    f'{uid_in_debug}\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                problem = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                problem += (
 | 
				
			||||||
 | 
					                    '\n'
 | 
				
			||||||
 | 
					                    f'A `pdb` REPL is SUPPOSEDLY in use by child {uid_in_debug}\n'
 | 
				
			||||||
 | 
					                    f'BUT, no child actors are IPC contactable!?!?\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # IDEAL CASE: root has REPL as expected
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # root actor still has this SIGINT handler active without
 | 
				
			||||||
 | 
					            # an actor using the `Lock` (a bug state) ??
 | 
				
			||||||
 | 
					            # => so immediately cancel any stale lock cs and revert
 | 
				
			||||||
 | 
					            # the handler!
 | 
				
			||||||
 | 
					            if not DebugStatus.repl:
 | 
				
			||||||
 | 
					                # TODO: WHEN should we revert back to ``trio``
 | 
				
			||||||
 | 
					                # handler if this one is stale?
 | 
				
			||||||
 | 
					                # -[ ] maybe after a counts work of ctl-c mashes?
 | 
				
			||||||
 | 
					                # -[ ] use a state var like `stale_handler: bool`?
 | 
				
			||||||
 | 
					                problem += (
 | 
				
			||||||
 | 
					                    'No subactor is using a `pdb` REPL according `Lock.ctx_in_debug`?\n'
 | 
				
			||||||
 | 
					                    'BUT, the root should be using it, WHY this handler ??\n\n'
 | 
				
			||||||
 | 
					                    'So either..\n'
 | 
				
			||||||
 | 
					                    '- some root-thread is using it but has no `.repl` set?, OR\n'
 | 
				
			||||||
 | 
					                    '- something else weird is going on outside the runtime!?\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # NOTE: since we emit this msg on ctl-c, we should
 | 
				
			||||||
 | 
					                # also always re-print the prompt the tail block!
 | 
				
			||||||
 | 
					                log.pdb(
 | 
				
			||||||
 | 
					                    _ctlc_ignore_header
 | 
				
			||||||
 | 
					                    +
 | 
				
			||||||
 | 
					                    f' by root actor..\n'
 | 
				
			||||||
 | 
					                    f'{DebugStatus.repl_task}\n'
 | 
				
			||||||
 | 
					                    f' |_{repl}\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                problem = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # XXX if one is set it means we ARE NOT operating an ideal
 | 
				
			||||||
 | 
					        # case where a child subactor or us (the root) has the
 | 
				
			||||||
 | 
					        # lock without any other detected problems.
 | 
				
			||||||
 | 
					        if problem:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # detect, report and maybe clear a stale lock request
 | 
				
			||||||
 | 
					            # cancel scope.
 | 
				
			||||||
 | 
					            lock_cs: trio.CancelScope = Lock.get_locking_task_cs()
 | 
				
			||||||
 | 
					            maybe_stale_lock_cs: bool = (
 | 
				
			||||||
 | 
					                lock_cs is not None
 | 
				
			||||||
 | 
					                and not lock_cs.cancel_called
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if maybe_stale_lock_cs:
 | 
				
			||||||
 | 
					                problem += (
 | 
				
			||||||
 | 
					                    '\n'
 | 
				
			||||||
 | 
					                    'Stale `Lock.ctx_in_debug._scope: CancelScope` detected?\n'
 | 
				
			||||||
 | 
					                    f'{Lock.ctx_in_debug}\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    '-> Calling ctx._scope.cancel()!\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                lock_cs.cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # TODO: wen do we actually want/need this, see above.
 | 
				
			||||||
 | 
					            # DebugStatus.unshield_sigint()
 | 
				
			||||||
 | 
					            log.warning(problem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # child actor that has locked the debugger
 | 
				
			||||||
 | 
					    elif not is_root_process():
 | 
				
			||||||
 | 
					        log.debug(
 | 
				
			||||||
 | 
					            f'Subactor {actor.uid} handling SIGINT\n\n'
 | 
				
			||||||
 | 
					            f'{Lock.repr()}\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rent_chan: Channel = actor._parent_chan
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            rent_chan is None
 | 
				
			||||||
 | 
					            or
 | 
				
			||||||
 | 
					            not rent_chan.connected()
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            log.warning(
 | 
				
			||||||
 | 
					                'This sub-actor thinks it is debugging '
 | 
				
			||||||
 | 
					                'but it has no connection to its parent ??\n'
 | 
				
			||||||
 | 
					                f'{actor.uid}\n'
 | 
				
			||||||
 | 
					                'Allowing SIGINT propagation..'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            DebugStatus.unshield_sigint()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        repl_task: str|None = DebugStatus.repl_task
 | 
				
			||||||
 | 
					        req_task: str|None = DebugStatus.req_task
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            repl_task
 | 
				
			||||||
 | 
					            and
 | 
				
			||||||
 | 
					            repl
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            log.pdb(
 | 
				
			||||||
 | 
					                _ctlc_ignore_header
 | 
				
			||||||
 | 
					                +
 | 
				
			||||||
 | 
					                f' by local task\n\n'
 | 
				
			||||||
 | 
					                f'{repl_task}\n'
 | 
				
			||||||
 | 
					                f' |_{repl}\n'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        elif req_task:
 | 
				
			||||||
 | 
					            log.debug(
 | 
				
			||||||
 | 
					                _ctlc_ignore_header
 | 
				
			||||||
 | 
					                +
 | 
				
			||||||
 | 
					                f' by local request-task and either,\n'
 | 
				
			||||||
 | 
					                f'- someone else is already REPL-in and has the `Lock`, or\n'
 | 
				
			||||||
 | 
					                f'- some other local task already is replin?\n\n'
 | 
				
			||||||
 | 
					                f'{req_task}\n'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO can we remove this now?
 | 
				
			||||||
 | 
					        # -[ ] does this path ever get hit any more?
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            msg: str = (
 | 
				
			||||||
 | 
					                'SIGINT shield handler still active BUT, \n\n'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if repl_task is None:
 | 
				
			||||||
 | 
					                msg += (
 | 
				
			||||||
 | 
					                    '- No local task claims to be in debug?\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if repl is None:
 | 
				
			||||||
 | 
					                msg += (
 | 
				
			||||||
 | 
					                    '- No local REPL is currently active?\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if req_task is None:
 | 
				
			||||||
 | 
					                msg += (
 | 
				
			||||||
 | 
					                    '- No debug request task is active?\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            log.warning(
 | 
				
			||||||
 | 
					                msg
 | 
				
			||||||
 | 
					                +
 | 
				
			||||||
 | 
					                'Reverting handler to `trio` default!\n'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            DebugStatus.unshield_sigint()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # XXX ensure that the reverted-to-handler actually is
 | 
				
			||||||
 | 
					            # able to rx what should have been **this** KBI ;)
 | 
				
			||||||
 | 
					            do_cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: how to handle the case of an intermediary-child actor
 | 
				
			||||||
 | 
					        # that **is not** marked in debug mode? See oustanding issue:
 | 
				
			||||||
 | 
					        # https://github.com/goodboy/tractor/issues/320
 | 
				
			||||||
 | 
					        # elif debug_mode():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # 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 (
 | 
				
			||||||
 | 
					        DebugStatus.repl  # only when current actor has a REPL engaged
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        flush_status: str = (
 | 
				
			||||||
 | 
					            'Flushing stdout to ensure new prompt line!\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # XXX: yah, mega hack, but how else do we catch this madness XD
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            repl.shname == 'xonsh'
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            flush_status += (
 | 
				
			||||||
 | 
					                '-> ALSO re-flushing due to `xonsh`..\n'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            repl.stdout.write(repl.prompt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # log.warning(
 | 
				
			||||||
 | 
					        log.devx(
 | 
				
			||||||
 | 
					            flush_status
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        repl.stdout.flush()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: better console UX to match the current "mode":
 | 
				
			||||||
 | 
					        # -[ ] for example if in sticky mode where if there is output
 | 
				
			||||||
 | 
					        #   detected as written to the tty we redraw this part underneath
 | 
				
			||||||
 | 
					        #   and erase the past draw of this same bit above?
 | 
				
			||||||
 | 
					        # repl.sticky = True
 | 
				
			||||||
 | 
					        # repl._print_if_sticky()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # also see these links for an approach from `ptk`:
 | 
				
			||||||
 | 
					        # https://github.com/goodboy/tractor/issues/130#issuecomment-663752040
 | 
				
			||||||
 | 
					        # https://github.com/prompt-toolkit/python-prompt-toolkit/blob/c2c6af8a0308f9e5d7c0e28cb8a02963fe0ce07a/prompt_toolkit/patch_stdout.py
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        log.devx(
 | 
				
			||||||
 | 
					        # log.warning(
 | 
				
			||||||
 | 
					            'Not flushing stdout since not needed?\n'
 | 
				
			||||||
 | 
					            f'|_{repl}\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # XXX only for tracing this handler
 | 
				
			||||||
 | 
					    log.devx('exiting SIGINT')
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue