First draft workin minus non-main-thread usage!
parent
8e66f45e23
commit
c04d77a3c9
|
@ -0,0 +1,69 @@
|
|||
import trio
|
||||
import tractor
|
||||
|
||||
|
||||
def sync_pause():
|
||||
tractor.pause_from_sync()
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def start_n_sync_pause(
|
||||
ctx: tractor.Context,
|
||||
):
|
||||
# sync to requesting peer
|
||||
await ctx.started()
|
||||
|
||||
actor: tractor.Actor = tractor.current_actor()
|
||||
print(f'entering SYNC PAUSE in {actor.uid}')
|
||||
sync_pause()
|
||||
print(f'back from SYNC PAUSE in {actor.uid}')
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
|
||||
from tractor._rpc import maybe_import_gb
|
||||
|
||||
async with tractor.open_nursery(
|
||||
debug_mode=True,
|
||||
) as an:
|
||||
|
||||
# TODO: where to put this?
|
||||
# => just inside `open_root_actor()` yah?
|
||||
await maybe_import_gb()
|
||||
|
||||
p: tractor.Portal = await an.start_actor(
|
||||
'subactor',
|
||||
enable_modules=[__name__],
|
||||
# infect_asyncio=True,
|
||||
debug_mode=True,
|
||||
loglevel='cancel',
|
||||
)
|
||||
|
||||
# TODO: 3 sub-actor usage cases:
|
||||
# -[ ] via a `.run_in_actor()` call
|
||||
# -[ ] via a `.run()`
|
||||
# -[ ] via a `.open_context()`
|
||||
#
|
||||
async with p.open_context(
|
||||
start_n_sync_pause,
|
||||
) as (ctx, first):
|
||||
assert first is None
|
||||
|
||||
await tractor.pause()
|
||||
sync_pause()
|
||||
|
||||
# TODO: make this work!!
|
||||
await trio.to_thread.run_sync(
|
||||
sync_pause,
|
||||
abandon_on_cancel=False,
|
||||
)
|
||||
|
||||
await ctx.cancel()
|
||||
|
||||
# TODO: case where we cancel from trio-side while asyncio task
|
||||
# has debugger lock?
|
||||
await p.cancel_actor()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
trio.run(main)
|
|
@ -46,7 +46,7 @@ import pdbp
|
|||
import tractor
|
||||
import trio
|
||||
from trio.lowlevel import current_task
|
||||
from trio_typing import (
|
||||
from trio import (
|
||||
TaskStatus,
|
||||
# Task,
|
||||
)
|
||||
|
@ -400,7 +400,6 @@ async def wait_for_parent_stdin_hijack(
|
|||
|
||||
# this syncs to child's ``Context.started()`` call.
|
||||
async with portal.open_context(
|
||||
|
||||
lock_tty_for_child,
|
||||
subactor_uid=actor_uid,
|
||||
|
||||
|
@ -682,7 +681,10 @@ def _set_trace(
|
|||
async def _pause(
|
||||
|
||||
debug_func: Callable = _set_trace,
|
||||
release_lock_signal: trio.Event | None = None,
|
||||
|
||||
# NOTE: must be passed in the `.pause_from_sync()` case!
|
||||
pdb: MultiActorPdb|None = None,
|
||||
undo_sigint: Callable|None = None,
|
||||
|
||||
# TODO: allow caller to pause despite task cancellation,
|
||||
# exactly the same as wrapping with:
|
||||
|
@ -691,8 +693,7 @@ async def _pause(
|
|||
# => the REMAINING ISSUE is that the scope's .__exit__() frame
|
||||
# is always show in the debugger on entry.. and there seems to
|
||||
# be no way to override it?..
|
||||
# shield: bool = False,
|
||||
|
||||
#
|
||||
shield: bool = False,
|
||||
task_status: TaskStatus[trio.Event] = trio.TASK_STATUS_IGNORED
|
||||
|
||||
|
@ -707,7 +708,6 @@ async def _pause(
|
|||
'''
|
||||
__tracebackhide__: bool = True
|
||||
actor = current_actor()
|
||||
pdb, undo_sigint = mk_mpdb()
|
||||
task_name: str = trio.lowlevel.current_task().name
|
||||
|
||||
if (
|
||||
|
@ -716,9 +716,14 @@ async def _pause(
|
|||
):
|
||||
Lock.local_pdb_complete = trio.Event()
|
||||
|
||||
debug_func = partial(
|
||||
debug_func,
|
||||
)
|
||||
if debug_func is not None:
|
||||
debug_func = partial(
|
||||
debug_func,
|
||||
)
|
||||
|
||||
if pdb is None:
|
||||
assert undo_sigint is None, 'You must pass both!?!'
|
||||
pdb, undo_sigint = mk_mpdb()
|
||||
|
||||
# TODO: need a more robust check for the "root" actor
|
||||
if (
|
||||
|
@ -761,12 +766,14 @@ async def _pause(
|
|||
# ```
|
||||
# but not entirely sure if that's a sane way to implement it?
|
||||
try:
|
||||
print("ACQUIRING TTY LOCK from CHILD")
|
||||
with trio.CancelScope(shield=True):
|
||||
await actor._service_n.start(
|
||||
wait_for_parent_stdin_hijack,
|
||||
actor.uid,
|
||||
)
|
||||
Lock.repl = pdb
|
||||
|
||||
except RuntimeError:
|
||||
Lock.release()
|
||||
|
||||
|
@ -779,11 +786,13 @@ async def _pause(
|
|||
raise
|
||||
|
||||
elif is_root_process():
|
||||
print("ROOT TTY LOCK BRANCH")
|
||||
|
||||
# we also wait in the root-parent for any child that
|
||||
# may have the tty locked prior
|
||||
# TODO: wait, what about multiple root tasks acquiring it though?
|
||||
if Lock.global_actor_in_debug == actor.uid:
|
||||
print("ROOT ALREADY HAS TTY?")
|
||||
# re-entrant root process already has it: noop.
|
||||
return
|
||||
|
||||
|
@ -797,11 +806,14 @@ async def _pause(
|
|||
|
||||
# must shield here to avoid hitting a ``Cancelled`` and
|
||||
# a child getting stuck bc we clobbered the tty
|
||||
print("ACQUIRING TTY LOCK from ROOT")
|
||||
with trio.CancelScope(shield=True):
|
||||
await Lock._debug_lock.acquire()
|
||||
else:
|
||||
# may be cancelled
|
||||
print("ROOT TRYING LOCK ACQUIRE")
|
||||
await Lock._debug_lock.acquire()
|
||||
print("ROOT LOCKED TTY")
|
||||
|
||||
Lock.global_actor_in_debug = actor.uid
|
||||
Lock.local_task_in_debug = task_name
|
||||
|
@ -811,32 +823,27 @@ async def _pause(
|
|||
# TODO: do we want to support using this **just** for the
|
||||
# locking / common code (prolly to help address #320)?
|
||||
#
|
||||
# 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")
|
||||
# with trio.CancelScope(shield=True):
|
||||
# await release_lock_signal.wait()
|
||||
if debug_func is None:
|
||||
task_status.started(Lock)
|
||||
print("ROOT .started(Lock) now!")
|
||||
|
||||
# else:
|
||||
else:
|
||||
# block here one (at the appropriate frame *up*) where
|
||||
# ``breakpoint()`` was awaited and begin handling stdio.
|
||||
log.debug('Entering sync world of the `pdb` REPL..')
|
||||
try:
|
||||
debug_func(
|
||||
actor,
|
||||
pdb,
|
||||
extra_frames_up_when_async=2,
|
||||
shield=shield,
|
||||
)
|
||||
except BaseException:
|
||||
log.exception(
|
||||
'Failed to invoke internal `debug_func = '
|
||||
f'{debug_func.func.__name__}`\n'
|
||||
)
|
||||
raise
|
||||
log.debug('Entering sync world of the `pdb` REPL..')
|
||||
try:
|
||||
debug_func(
|
||||
actor,
|
||||
pdb,
|
||||
extra_frames_up_when_async=2,
|
||||
shield=shield,
|
||||
)
|
||||
except BaseException:
|
||||
log.exception(
|
||||
'Failed to invoke internal `debug_func = '
|
||||
f'{debug_func.func.__name__}`\n'
|
||||
)
|
||||
raise
|
||||
|
||||
except bdb.BdbQuit:
|
||||
Lock.release()
|
||||
|
@ -862,8 +869,7 @@ async def _pause(
|
|||
|
||||
async def pause(
|
||||
|
||||
debug_func: Callable = _set_trace,
|
||||
release_lock_signal: trio.Event | None = None,
|
||||
debug_func: Callable|None = _set_trace,
|
||||
|
||||
# TODO: allow caller to pause despite task cancellation,
|
||||
# exactly the same as wrapping with:
|
||||
|
@ -872,10 +878,11 @@ async def pause(
|
|||
# => the REMAINING ISSUE is that the scope's .__exit__() frame
|
||||
# is always show in the debugger on entry.. and there seems to
|
||||
# be no way to override it?..
|
||||
# shield: bool = False,
|
||||
|
||||
#
|
||||
shield: bool = False,
|
||||
task_status: TaskStatus[trio.Event] = trio.TASK_STATUS_IGNORED
|
||||
task_status: TaskStatus[trio.Event] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
**_pause_kwargs,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -920,16 +927,16 @@ async def pause(
|
|||
task_status.started(cs)
|
||||
return await _pause(
|
||||
debug_func=debug_func,
|
||||
release_lock_signal=release_lock_signal,
|
||||
shield=True,
|
||||
task_status=task_status,
|
||||
**_pause_kwargs
|
||||
)
|
||||
else:
|
||||
return await _pause(
|
||||
debug_func=debug_func,
|
||||
release_lock_signal=release_lock_signal,
|
||||
shield=False,
|
||||
task_status=task_status,
|
||||
**_pause_kwargs
|
||||
)
|
||||
|
||||
|
||||
|
@ -938,46 +945,64 @@ async def pause(
|
|||
# TODO: allow pausing from sync code.
|
||||
# normally by remapping python's builtin breakpoint() hook to this
|
||||
# runtime aware version which takes care of all .
|
||||
def pause_from_sync() -> None:
|
||||
print("ENTER SYNC PAUSE")
|
||||
def pause_from_sync(
|
||||
hide_tb: bool = True
|
||||
) -> None:
|
||||
|
||||
__tracebackhide__: bool = hide_tb
|
||||
actor: tractor.Actor = current_actor(
|
||||
err_on_no_runtime=False,
|
||||
)
|
||||
if actor:
|
||||
try:
|
||||
import greenback
|
||||
# __tracebackhide__ = True
|
||||
print(
|
||||
f'{actor.uid}: JUST ENTERED `tractor.pause_from_sync()`'
|
||||
f'|_{actor}\n'
|
||||
)
|
||||
if not actor:
|
||||
raise RuntimeError(
|
||||
'Not inside the `tractor`-runtime?\n'
|
||||
'`tractor.pause_from_sync()` is not functional without a wrapping\n'
|
||||
'- `async with tractor.open_nursery()` or,\n'
|
||||
'- `async with tractor.open_root_actor()`\n'
|
||||
)
|
||||
|
||||
try:
|
||||
import greenback
|
||||
except ModuleNotFoundError:
|
||||
raise RuntimeError(
|
||||
'The `greenback` lib is required to use `tractor.pause_from_sync()`!\n'
|
||||
'https://github.com/oremanj/greenback\n'
|
||||
)
|
||||
|
||||
# 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,
|
||||
))
|
||||
)
|
||||
|
||||
except ModuleNotFoundError:
|
||||
log.warning('NO GREENBACK FOUND')
|
||||
else:
|
||||
log.warning('Not inside actor-runtime')
|
||||
# out = greenback.await_(
|
||||
# actor._service_n.start(partial(
|
||||
# pause,
|
||||
# debug_func=None,
|
||||
# release_lock_signal=task_can_release_tty_lock,
|
||||
# ))
|
||||
# )
|
||||
|
||||
# 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 ;)
|
||||
db, undo_sigint = mk_mpdb()
|
||||
Lock.local_task_in_debug = 'sync'
|
||||
# db.config.enable_hidden_frames = True
|
||||
greenback.await_(
|
||||
pause(
|
||||
debug_func=None,
|
||||
pdb=db,
|
||||
undo_sigint=undo_sigint,
|
||||
)
|
||||
)
|
||||
|
||||
# we entered the global ``breakpoint()`` built-in from sync
|
||||
Lock.local_task_in_debug = 'sync'
|
||||
|
||||
# TODO: ensure we aggressively make the user aware about
|
||||
# entering the global ``breakpoint()`` built-in from sync
|
||||
# code?
|
||||
frame: FrameType | None = sys._getframe()
|
||||
# print(f'FRAME: {str(frame)}')
|
||||
# assert not db._is_hidden(frame)
|
||||
|
||||
frame: FrameType = frame.f_back # type: ignore
|
||||
|
||||
# db.config.enable_hidden_frames = True
|
||||
# assert not db._is_hidden(frame)
|
||||
# print(f'FRAME: {str(frame)}')
|
||||
# if not db._is_hidden(frame):
|
||||
# pdbp.set_trace()
|
||||
|
@ -985,17 +1010,21 @@ def pause_from_sync() -> None:
|
|||
# (frame, frame.f_lineno)
|
||||
# )
|
||||
db.set_trace(frame=frame)
|
||||
# NOTE XXX: see the `@pdbp.hideframe` decoration
|
||||
# on `Lock.unshield_sigint()`.. I have NO CLUE why
|
||||
|
||||
# XXX NOTE XXX no other LOC can be here without it
|
||||
# showing up in the REPL's last stack frame !?!
|
||||
# -[ ] tried to use `@pdbp.hideframe` decoration but
|
||||
# still doesn't work
|
||||
#
|
||||
# FROM BEFORE: on `Lock.unshield_sigint()`.. I have NO CLUE why
|
||||
# the next instruction's def frame is being shown
|
||||
# in the tb but it seems to be something wonky with
|
||||
# the way `pdb` core works?
|
||||
#
|
||||
# NOTE: not needed any more anyway since it's all in
|
||||
# `Lock.release()` now!
|
||||
# undo_sigint()
|
||||
|
||||
# Lock.global_actor_in_debug = actor.uid
|
||||
# Lock.release()
|
||||
# task_can_release_tty_lock.set()
|
||||
|
||||
|
||||
# using the "pause" semantics instead since
|
||||
# that better covers actually somewhat "pausing the runtime"
|
||||
|
|
Loading…
Reference in New Issue