Compare commits
5 Commits
5c2e972315
...
b875b35b98
Author | SHA1 | Date |
---|---|---|
Tyler Goodlet | b875b35b98 | |
Tyler Goodlet | 46ddc214cd | |
Tyler Goodlet | b3ee20d3b9 | |
Tyler Goodlet | cf3e6c1218 | |
Tyler Goodlet | 8af9b0201d |
|
@ -1,3 +1,8 @@
|
||||||
|
'''
|
||||||
|
Examples of using the builtin `breakpoint()` from an `asyncio.Task`
|
||||||
|
running in a subactor spawned with `infect_asyncio=True`.
|
||||||
|
|
||||||
|
'''
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
@ -26,15 +31,16 @@ async def bp_then_error(
|
||||||
# NOTE: what happens here inside the hook needs some refinement..
|
# NOTE: what happens here inside the hook needs some refinement..
|
||||||
# => seems like it's still `._debug._set_trace()` but
|
# => seems like it's still `._debug._set_trace()` but
|
||||||
# we set `Lock.local_task_in_debug = 'sync'`, we probably want
|
# we set `Lock.local_task_in_debug = 'sync'`, we probably want
|
||||||
# some further, at least, meta-data about the task/actoq in debug
|
# some further, at least, meta-data about the task/actor in debug
|
||||||
# in terms of making it clear it's asyncio mucking about.
|
# in terms of making it clear it's `asyncio` mucking about.
|
||||||
breakpoint()
|
breakpoint()
|
||||||
|
|
||||||
|
|
||||||
# short checkpoint / delay
|
# short checkpoint / delay
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5) # asyncio-side
|
||||||
|
|
||||||
if raise_after_bp:
|
if raise_after_bp:
|
||||||
raise ValueError('blah')
|
raise ValueError('asyncio side error!')
|
||||||
|
|
||||||
# TODO: test case with this so that it gets cancelled?
|
# TODO: test case with this so that it gets cancelled?
|
||||||
else:
|
else:
|
||||||
|
@ -46,7 +52,7 @@ async def bp_then_error(
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def trio_ctx(
|
async def trio_ctx(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
bp_before_started: bool = True,
|
bp_before_started: bool = False,
|
||||||
):
|
):
|
||||||
|
|
||||||
# this will block until the ``asyncio`` task sends a "first"
|
# this will block until the ``asyncio`` task sends a "first"
|
||||||
|
@ -55,19 +61,19 @@ async def trio_ctx(
|
||||||
|
|
||||||
to_asyncio.open_channel_from(
|
to_asyncio.open_channel_from(
|
||||||
bp_then_error,
|
bp_then_error,
|
||||||
raise_after_bp=not bp_before_started,
|
# raise_after_bp=not bp_before_started,
|
||||||
) as (first, chan),
|
) as (first, chan),
|
||||||
|
|
||||||
trio.open_nursery() as n,
|
trio.open_nursery() as tn,
|
||||||
):
|
):
|
||||||
assert first == 'start'
|
assert first == 'start'
|
||||||
|
|
||||||
if bp_before_started:
|
if bp_before_started:
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
await ctx.started(first)
|
await ctx.started(first) # trio-side
|
||||||
|
|
||||||
n.start_soon(
|
tn.start_soon(
|
||||||
to_asyncio.run_task,
|
to_asyncio.run_task,
|
||||||
aio_sleep_forever,
|
aio_sleep_forever,
|
||||||
)
|
)
|
||||||
|
@ -77,14 +83,18 @@ async def trio_ctx(
|
||||||
async def main(
|
async def main(
|
||||||
bps_all_over: bool = True,
|
bps_all_over: bool = True,
|
||||||
|
|
||||||
|
# TODO, WHICH OF THESE HAZ BUGZ?
|
||||||
|
cancel_from_root: bool = False,
|
||||||
|
err_from_root: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
maybe_enable_greenback=True,
|
maybe_enable_greenback=True,
|
||||||
# loglevel='devx',
|
# loglevel='devx',
|
||||||
) as n:
|
) as an:
|
||||||
ptl: Portal = await n.start_actor(
|
ptl: Portal = await an.start_actor(
|
||||||
'aio_daemon',
|
'aio_daemon',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
infect_asyncio=True,
|
infect_asyncio=True,
|
||||||
|
@ -99,12 +109,18 @@ async def main(
|
||||||
|
|
||||||
assert first == 'start'
|
assert first == 'start'
|
||||||
|
|
||||||
if bps_all_over:
|
# pause in parent to ensure no cross-actor
|
||||||
await tractor.breakpoint()
|
# locking problems exist!
|
||||||
|
await tractor.pause()
|
||||||
|
|
||||||
# await trio.sleep_forever()
|
if cancel_from_root:
|
||||||
await ctx.cancel()
|
await ctx.cancel()
|
||||||
|
|
||||||
|
if err_from_root:
|
||||||
assert 0
|
assert 0
|
||||||
|
else:
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
# TODO: case where we cancel from trio-side while asyncio task
|
# TODO: case where we cancel from trio-side while asyncio task
|
||||||
# has debugger lock?
|
# has debugger lock?
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
'''
|
'''
|
||||||
Fast fail test with a context.
|
Fast fail test with a `Context`.
|
||||||
|
|
||||||
Ensure the partially initialized sub-actor process
|
Ensure the partially initialized sub-actor process
|
||||||
doesn't cause a hang on error/cancel of the parent
|
doesn't cause a hang on error/cancel of the parent
|
||||||
|
|
|
@ -7,7 +7,7 @@ async def breakpoint_forever():
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
yield 'yo'
|
yield 'yo'
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
tractor.log.get_console_log().exception(
|
tractor.log.get_console_log().exception(
|
||||||
'Cancelled while trying to enter pause point!'
|
'Cancelled while trying to enter pause point!'
|
||||||
|
|
|
@ -10,7 +10,7 @@ async def name_error():
|
||||||
async def breakpoint_forever():
|
async def breakpoint_forever():
|
||||||
"Indefinitely re-enter debugger in child actor."
|
"Indefinitely re-enter debugger in child actor."
|
||||||
while True:
|
while True:
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
# NOTE: if the test never sent 'q'/'quit' commands
|
# NOTE: if the test never sent 'q'/'quit' commands
|
||||||
# on the pdb repl, without this checkpoint line the
|
# on the pdb repl, without this checkpoint line the
|
||||||
|
|
|
@ -6,7 +6,7 @@ async def breakpoint_forever():
|
||||||
"Indefinitely re-enter debugger in child actor."
|
"Indefinitely re-enter debugger in child actor."
|
||||||
while True:
|
while True:
|
||||||
await trio.sleep(0.1)
|
await trio.sleep(0.1)
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
|
|
||||||
async def name_error():
|
async def name_error():
|
||||||
|
|
|
@ -6,19 +6,46 @@ import tractor
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with tractor.open_nursery(debug_mode=True) as an:
|
|
||||||
|
|
||||||
assert os.environ['PYTHONBREAKPOINT'] == 'tractor._debug._set_trace'
|
# intially unset, no entry.
|
||||||
|
orig_pybp_var: int = os.environ.get('PYTHONBREAKPOINT')
|
||||||
|
assert orig_pybp_var in {None, "0"}
|
||||||
|
|
||||||
|
async with tractor.open_nursery(
|
||||||
|
debug_mode=True,
|
||||||
|
) as an:
|
||||||
|
assert an
|
||||||
|
assert (
|
||||||
|
(pybp_var := os.environ['PYTHONBREAKPOINT'])
|
||||||
|
==
|
||||||
|
'tractor.devx._debug._sync_pause_from_builtin'
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: an assert that verifies the hook has indeed been, hooked
|
# TODO: an assert that verifies the hook has indeed been, hooked
|
||||||
# XD
|
# XD
|
||||||
assert sys.breakpointhook is not tractor._debug._set_trace
|
assert (
|
||||||
|
(pybp_hook := sys.breakpointhook)
|
||||||
|
is not tractor.devx._debug._set_trace
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f'$PYTHONOBREAKPOINT: {pybp_var!r}\n'
|
||||||
|
f'`sys.breakpointhook`: {pybp_hook!r}\n'
|
||||||
|
)
|
||||||
breakpoint()
|
breakpoint()
|
||||||
|
pass # first bp, tractor hook set.
|
||||||
|
|
||||||
# TODO: an assert that verifies the hook is unhooked..
|
# XXX AFTER EXIT (of actor-runtime) verify the hook is unset..
|
||||||
|
#
|
||||||
|
# YES, this is weird but it's how stdlib docs say to do it..
|
||||||
|
# https://docs.python.org/3/library/sys.html#sys.breakpointhook
|
||||||
|
assert os.environ.get('PYTHONBREAKPOINT') is orig_pybp_var
|
||||||
assert sys.breakpointhook
|
assert sys.breakpointhook
|
||||||
|
|
||||||
|
# now ensure a regular builtin pause still works
|
||||||
breakpoint()
|
breakpoint()
|
||||||
|
pass # last bp, stdlib hook restored
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
|
@ -10,7 +10,7 @@ async def main():
|
||||||
|
|
||||||
await trio.sleep(0.1)
|
await trio.sleep(0.1)
|
||||||
|
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
await trio.sleep(0.1)
|
await trio.sleep(0.1)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ async def main(
|
||||||
# loglevel='runtime',
|
# loglevel='runtime',
|
||||||
):
|
):
|
||||||
while True:
|
while True:
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -4,9 +4,9 @@ import trio
|
||||||
|
|
||||||
async def gen():
|
async def gen():
|
||||||
yield 'yo'
|
yield 'yo'
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
yield 'yo'
|
yield 'yo'
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
|
@ -15,7 +15,7 @@ async def just_bp(
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
await tractor.breakpoint()
|
await tractor.pause()
|
||||||
|
|
||||||
# TODO: bps and errors in this call..
|
# TODO: bps and errors in this call..
|
||||||
async for val in gen():
|
async for val in gen():
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
`tractor.devx.*` tooling sub-pkg test space.
|
`tractor.devx.*` tooling sub-pkg test space.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
import time
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
)
|
)
|
||||||
|
@ -11,9 +12,19 @@ from pexpect.exceptions import (
|
||||||
TIMEOUT,
|
TIMEOUT,
|
||||||
)
|
)
|
||||||
from pexpect.spawnbase import SpawnBase
|
from pexpect.spawnbase import SpawnBase
|
||||||
|
|
||||||
from tractor._testing import (
|
from tractor._testing import (
|
||||||
mk_cmd,
|
mk_cmd,
|
||||||
)
|
)
|
||||||
|
from tractor.devx._debug import (
|
||||||
|
_pause_msg as _pause_msg,
|
||||||
|
_crash_msg as _crash_msg,
|
||||||
|
_repl_fail_msg as _repl_fail_msg,
|
||||||
|
_ctlc_ignore_header as _ctlc_ignore_header,
|
||||||
|
)
|
||||||
|
from conftest import (
|
||||||
|
_ci_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -107,6 +118,9 @@ def expect(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
PROMPT = r"\(Pdb\+\)"
|
||||||
|
|
||||||
|
|
||||||
def in_prompt_msg(
|
def in_prompt_msg(
|
||||||
child: SpawnBase,
|
child: SpawnBase,
|
||||||
parts: list[str],
|
parts: list[str],
|
||||||
|
@ -166,3 +180,40 @@ def assert_before(
|
||||||
err_on_false=True,
|
err_on_false=True,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def do_ctlc(
|
||||||
|
child,
|
||||||
|
count: int = 3,
|
||||||
|
delay: float = 0.1,
|
||||||
|
patt: str|None = None,
|
||||||
|
|
||||||
|
# expect repl UX to reprint the prompt after every
|
||||||
|
# ctrl-c send.
|
||||||
|
# XXX: no idea but, in CI this never seems to work even on 3.10 so
|
||||||
|
# needs some further investigation potentially...
|
||||||
|
expect_prompt: bool = not _ci_env,
|
||||||
|
|
||||||
|
) -> str|None:
|
||||||
|
|
||||||
|
before: str|None = None
|
||||||
|
|
||||||
|
# make sure ctl-c sends don't do anything but repeat output
|
||||||
|
for _ in range(count):
|
||||||
|
time.sleep(delay)
|
||||||
|
child.sendcontrol('c')
|
||||||
|
|
||||||
|
# TODO: figure out why this makes CI fail..
|
||||||
|
# if you run this test manually it works just fine..
|
||||||
|
if expect_prompt:
|
||||||
|
time.sleep(delay)
|
||||||
|
child.expect(PROMPT)
|
||||||
|
before = str(child.before.decode())
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if patt:
|
||||||
|
# should see the last line on console
|
||||||
|
assert patt in before
|
||||||
|
|
||||||
|
# return the console content up to the final prompt
|
||||||
|
return before
|
||||||
|
|
|
@ -21,14 +21,13 @@ from pexpect.exceptions import (
|
||||||
EOF,
|
EOF,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tractor.devx._debug import (
|
from .conftest import (
|
||||||
|
do_ctlc,
|
||||||
|
PROMPT,
|
||||||
_pause_msg,
|
_pause_msg,
|
||||||
_crash_msg,
|
_crash_msg,
|
||||||
_repl_fail_msg,
|
_repl_fail_msg,
|
||||||
)
|
)
|
||||||
from conftest import (
|
|
||||||
_ci_env,
|
|
||||||
)
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
expect,
|
expect,
|
||||||
in_prompt_msg,
|
in_prompt_msg,
|
||||||
|
@ -70,9 +69,6 @@ has_nested_actors = pytest.mark.has_nested_actors
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
PROMPT = r"\(Pdb\+\)"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'user_in_out',
|
'user_in_out',
|
||||||
[
|
[
|
||||||
|
@ -123,8 +119,10 @@ def test_root_actor_error(
|
||||||
ids=lambda item: f'{item[0]} -> {item[1]}',
|
ids=lambda item: f'{item[0]} -> {item[1]}',
|
||||||
)
|
)
|
||||||
def test_root_actor_bp(spawn, user_in_out):
|
def test_root_actor_bp(spawn, user_in_out):
|
||||||
"""Demonstrate breakpoint from in root actor.
|
'''
|
||||||
"""
|
Demonstrate breakpoint from in root actor.
|
||||||
|
|
||||||
|
'''
|
||||||
user_input, expect_err_str = user_in_out
|
user_input, expect_err_str = user_in_out
|
||||||
child = spawn('root_actor_breakpoint')
|
child = spawn('root_actor_breakpoint')
|
||||||
|
|
||||||
|
@ -146,43 +144,6 @@ def test_root_actor_bp(spawn, user_in_out):
|
||||||
assert expect_err_str in str(child.before)
|
assert expect_err_str in str(child.before)
|
||||||
|
|
||||||
|
|
||||||
def do_ctlc(
|
|
||||||
child,
|
|
||||||
count: int = 3,
|
|
||||||
delay: float = 0.1,
|
|
||||||
patt: str|None = None,
|
|
||||||
|
|
||||||
# expect repl UX to reprint the prompt after every
|
|
||||||
# ctrl-c send.
|
|
||||||
# XXX: no idea but, in CI this never seems to work even on 3.10 so
|
|
||||||
# needs some further investigation potentially...
|
|
||||||
expect_prompt: bool = not _ci_env,
|
|
||||||
|
|
||||||
) -> str|None:
|
|
||||||
|
|
||||||
before: str|None = None
|
|
||||||
|
|
||||||
# make sure ctl-c sends don't do anything but repeat output
|
|
||||||
for _ in range(count):
|
|
||||||
time.sleep(delay)
|
|
||||||
child.sendcontrol('c')
|
|
||||||
|
|
||||||
# TODO: figure out why this makes CI fail..
|
|
||||||
# if you run this test manually it works just fine..
|
|
||||||
if expect_prompt:
|
|
||||||
time.sleep(delay)
|
|
||||||
child.expect(PROMPT)
|
|
||||||
before = str(child.before.decode())
|
|
||||||
time.sleep(delay)
|
|
||||||
|
|
||||||
if patt:
|
|
||||||
# should see the last line on console
|
|
||||||
assert patt in before
|
|
||||||
|
|
||||||
# return the console content up to the final prompt
|
|
||||||
return before
|
|
||||||
|
|
||||||
|
|
||||||
def test_root_actor_bp_forever(
|
def test_root_actor_bp_forever(
|
||||||
spawn,
|
spawn,
|
||||||
ctlc: bool,
|
ctlc: bool,
|
||||||
|
@ -919,138 +880,6 @@ def test_different_debug_mode_per_actor(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_pause_from_sync(
|
|
||||||
spawn,
|
|
||||||
ctlc: bool
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Verify we can use the `pdbp` REPL from sync functions AND from
|
|
||||||
any thread spawned with `trio.to_thread.run_sync()`.
|
|
||||||
|
|
||||||
`examples/debugging/sync_bp.py`
|
|
||||||
|
|
||||||
'''
|
|
||||||
child = spawn('sync_bp')
|
|
||||||
|
|
||||||
# first `sync_pause()` after nurseries open
|
|
||||||
child.expect(PROMPT)
|
|
||||||
assert_before(
|
|
||||||
child,
|
|
||||||
[
|
|
||||||
# pre-prompt line
|
|
||||||
_pause_msg,
|
|
||||||
"<Task '__main__.main'",
|
|
||||||
"('root'",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if ctlc:
|
|
||||||
do_ctlc(child)
|
|
||||||
# ^NOTE^ subactor not spawned yet; don't need extra delay.
|
|
||||||
|
|
||||||
child.sendline('c')
|
|
||||||
|
|
||||||
# first `await tractor.pause()` inside `p.open_context()` body
|
|
||||||
child.expect(PROMPT)
|
|
||||||
|
|
||||||
# XXX shouldn't see gb loaded message with PDB loglevel!
|
|
||||||
assert not in_prompt_msg(
|
|
||||||
child,
|
|
||||||
['`greenback` portal opened!'],
|
|
||||||
)
|
|
||||||
# should be same root task
|
|
||||||
assert_before(
|
|
||||||
child,
|
|
||||||
[
|
|
||||||
_pause_msg,
|
|
||||||
"<Task '__main__.main'",
|
|
||||||
"('root'",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if ctlc:
|
|
||||||
do_ctlc(
|
|
||||||
child,
|
|
||||||
# NOTE: setting this to 0 (or some other sufficient
|
|
||||||
# small val) can cause the test to fail since the
|
|
||||||
# `subactor` suffers a race where the root/parent
|
|
||||||
# sends an actor-cancel prior to it hitting its pause
|
|
||||||
# point; by def the value is 0.1
|
|
||||||
delay=0.4,
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX, fwiw without a brief sleep here the SIGINT might actually
|
|
||||||
# trigger "subactor" cancellation by its parent before the
|
|
||||||
# shield-handler is engaged.
|
|
||||||
#
|
|
||||||
# => similar to the `delay` input to `do_ctlc()` below, setting
|
|
||||||
# this too low can cause the test to fail since the `subactor`
|
|
||||||
# suffers a race where the root/parent sends an actor-cancel
|
|
||||||
# prior to the context task hitting its pause point (and thus
|
|
||||||
# engaging the `sigint_shield()` handler in time); this value
|
|
||||||
# seems be good enuf?
|
|
||||||
time.sleep(0.6)
|
|
||||||
|
|
||||||
# one of the bg thread or subactor should have
|
|
||||||
# `Lock.acquire()`-ed
|
|
||||||
# (NOT both, which will result in REPL clobbering!)
|
|
||||||
attach_patts: dict[str, list[str]] = {
|
|
||||||
'subactor': [
|
|
||||||
"'start_n_sync_pause'",
|
|
||||||
"('subactor'",
|
|
||||||
],
|
|
||||||
'inline_root_bg_thread': [
|
|
||||||
"<Thread(inline_root_bg_thread",
|
|
||||||
"('root'",
|
|
||||||
],
|
|
||||||
'start_soon_root_bg_thread': [
|
|
||||||
"<Thread(start_soon_root_bg_thread",
|
|
||||||
"('root'",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
conts: int = 0 # for debugging below matching logic on failure
|
|
||||||
while attach_patts:
|
|
||||||
child.sendline('c')
|
|
||||||
conts += 1
|
|
||||||
child.expect(PROMPT)
|
|
||||||
before = str(child.before.decode())
|
|
||||||
for key in attach_patts:
|
|
||||||
if key in before:
|
|
||||||
attach_key: str = key
|
|
||||||
expected_patts: str = attach_patts.pop(key)
|
|
||||||
assert_before(
|
|
||||||
child,
|
|
||||||
[_pause_msg]
|
|
||||||
+
|
|
||||||
expected_patts
|
|
||||||
)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
pytest.fail(
|
|
||||||
f'No keys found?\n\n'
|
|
||||||
f'{attach_patts.keys()}\n\n'
|
|
||||||
f'{before}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
# ensure no other task/threads engaged a REPL
|
|
||||||
# at the same time as the one that was detected above.
|
|
||||||
for key, other_patts in attach_patts.copy().items():
|
|
||||||
assert not in_prompt_msg(
|
|
||||||
child,
|
|
||||||
other_patts,
|
|
||||||
)
|
|
||||||
|
|
||||||
if ctlc:
|
|
||||||
do_ctlc(
|
|
||||||
child,
|
|
||||||
patt=attach_key,
|
|
||||||
# NOTE same as comment above
|
|
||||||
delay=0.4,
|
|
||||||
)
|
|
||||||
|
|
||||||
child.sendline('c')
|
|
||||||
child.expect(EOF)
|
|
||||||
|
|
||||||
|
|
||||||
def test_post_mortem_api(
|
def test_post_mortem_api(
|
||||||
spawn,
|
spawn,
|
||||||
ctlc: bool,
|
ctlc: bool,
|
||||||
|
|
|
@ -0,0 +1,329 @@
|
||||||
|
'''
|
||||||
|
That "foreign loop/thread" debug REPL support better ALSO WORK!
|
||||||
|
|
||||||
|
Same as `test_native_pause.py`.
|
||||||
|
All these tests can be understood (somewhat) by running the
|
||||||
|
equivalent `examples/debugging/` scripts manually.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# from functools import partial
|
||||||
|
# import itertools
|
||||||
|
import time
|
||||||
|
# from typing import (
|
||||||
|
# Iterator,
|
||||||
|
# )
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pexpect.exceptions import (
|
||||||
|
# TIMEOUT,
|
||||||
|
EOF,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .conftest import (
|
||||||
|
# _ci_env,
|
||||||
|
do_ctlc,
|
||||||
|
PROMPT,
|
||||||
|
# expect,
|
||||||
|
in_prompt_msg,
|
||||||
|
assert_before,
|
||||||
|
_pause_msg,
|
||||||
|
_crash_msg,
|
||||||
|
_ctlc_ignore_header,
|
||||||
|
# _repl_fail_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pause_from_sync(
|
||||||
|
spawn,
|
||||||
|
ctlc: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Verify we can use the `pdbp` REPL from sync functions AND from
|
||||||
|
any thread spawned with `trio.to_thread.run_sync()`.
|
||||||
|
|
||||||
|
`examples/debugging/sync_bp.py`
|
||||||
|
|
||||||
|
'''
|
||||||
|
child = spawn('sync_bp')
|
||||||
|
|
||||||
|
# first `sync_pause()` after nurseries open
|
||||||
|
child.expect(PROMPT)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
# pre-prompt line
|
||||||
|
_pause_msg,
|
||||||
|
"<Task '__main__.main'",
|
||||||
|
"('root'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(child)
|
||||||
|
# ^NOTE^ subactor not spawned yet; don't need extra delay.
|
||||||
|
|
||||||
|
child.sendline('c')
|
||||||
|
|
||||||
|
# first `await tractor.pause()` inside `p.open_context()` body
|
||||||
|
child.expect(PROMPT)
|
||||||
|
|
||||||
|
# XXX shouldn't see gb loaded message with PDB loglevel!
|
||||||
|
assert not in_prompt_msg(
|
||||||
|
child,
|
||||||
|
['`greenback` portal opened!'],
|
||||||
|
)
|
||||||
|
# should be same root task
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_pause_msg,
|
||||||
|
"<Task '__main__.main'",
|
||||||
|
"('root'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(
|
||||||
|
child,
|
||||||
|
# NOTE: setting this to 0 (or some other sufficient
|
||||||
|
# small val) can cause the test to fail since the
|
||||||
|
# `subactor` suffers a race where the root/parent
|
||||||
|
# sends an actor-cancel prior to it hitting its pause
|
||||||
|
# point; by def the value is 0.1
|
||||||
|
delay=0.4,
|
||||||
|
)
|
||||||
|
|
||||||
|
# XXX, fwiw without a brief sleep here the SIGINT might actually
|
||||||
|
# trigger "subactor" cancellation by its parent before the
|
||||||
|
# shield-handler is engaged.
|
||||||
|
#
|
||||||
|
# => similar to the `delay` input to `do_ctlc()` below, setting
|
||||||
|
# this too low can cause the test to fail since the `subactor`
|
||||||
|
# suffers a race where the root/parent sends an actor-cancel
|
||||||
|
# prior to the context task hitting its pause point (and thus
|
||||||
|
# engaging the `sigint_shield()` handler in time); this value
|
||||||
|
# seems be good enuf?
|
||||||
|
time.sleep(0.6)
|
||||||
|
|
||||||
|
# one of the bg thread or subactor should have
|
||||||
|
# `Lock.acquire()`-ed
|
||||||
|
# (NOT both, which will result in REPL clobbering!)
|
||||||
|
attach_patts: dict[str, list[str]] = {
|
||||||
|
'subactor': [
|
||||||
|
"'start_n_sync_pause'",
|
||||||
|
"('subactor'",
|
||||||
|
],
|
||||||
|
'inline_root_bg_thread': [
|
||||||
|
"<Thread(inline_root_bg_thread",
|
||||||
|
"('root'",
|
||||||
|
],
|
||||||
|
'start_soon_root_bg_thread': [
|
||||||
|
"<Thread(start_soon_root_bg_thread",
|
||||||
|
"('root'",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
conts: int = 0 # for debugging below matching logic on failure
|
||||||
|
while attach_patts:
|
||||||
|
child.sendline('c')
|
||||||
|
conts += 1
|
||||||
|
child.expect(PROMPT)
|
||||||
|
before = str(child.before.decode())
|
||||||
|
for key in attach_patts:
|
||||||
|
if key in before:
|
||||||
|
attach_key: str = key
|
||||||
|
expected_patts: str = attach_patts.pop(key)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[_pause_msg]
|
||||||
|
+
|
||||||
|
expected_patts
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pytest.fail(
|
||||||
|
f'No keys found?\n\n'
|
||||||
|
f'{attach_patts.keys()}\n\n'
|
||||||
|
f'{before}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure no other task/threads engaged a REPL
|
||||||
|
# at the same time as the one that was detected above.
|
||||||
|
for key, other_patts in attach_patts.copy().items():
|
||||||
|
assert not in_prompt_msg(
|
||||||
|
child,
|
||||||
|
other_patts,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(
|
||||||
|
child,
|
||||||
|
patt=attach_key,
|
||||||
|
# NOTE same as comment above
|
||||||
|
delay=0.4,
|
||||||
|
)
|
||||||
|
|
||||||
|
child.sendline('c')
|
||||||
|
child.expect(EOF)
|
||||||
|
|
||||||
|
|
||||||
|
def expect_any_of(
|
||||||
|
attach_patts: dict[str, list[str]],
|
||||||
|
child, # what type?
|
||||||
|
ctlc: bool = False,
|
||||||
|
prompt: str = _ctlc_ignore_header,
|
||||||
|
ctlc_delay: float = .4,
|
||||||
|
|
||||||
|
) -> list[str]:
|
||||||
|
'''
|
||||||
|
Receive any of a `list[str]` of patterns provided in
|
||||||
|
`attach_patts`.
|
||||||
|
|
||||||
|
Used to test racing prompts from multiple actors and/or
|
||||||
|
tasks using a common root process' `pdbp` REPL.
|
||||||
|
|
||||||
|
'''
|
||||||
|
assert attach_patts
|
||||||
|
|
||||||
|
child.expect(PROMPT)
|
||||||
|
before = str(child.before.decode())
|
||||||
|
|
||||||
|
for attach_key in attach_patts:
|
||||||
|
if attach_key in before:
|
||||||
|
expected_patts: str = attach_patts.pop(attach_key)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
expected_patts
|
||||||
|
)
|
||||||
|
break # from for
|
||||||
|
else:
|
||||||
|
pytest.fail(
|
||||||
|
f'No keys found?\n\n'
|
||||||
|
f'{attach_patts.keys()}\n\n'
|
||||||
|
f'{before}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure no other task/threads engaged a REPL
|
||||||
|
# at the same time as the one that was detected above.
|
||||||
|
for key, other_patts in attach_patts.copy().items():
|
||||||
|
assert not in_prompt_msg(
|
||||||
|
child,
|
||||||
|
other_patts,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(
|
||||||
|
child,
|
||||||
|
patt=prompt,
|
||||||
|
# NOTE same as comment above
|
||||||
|
delay=ctlc_delay,
|
||||||
|
)
|
||||||
|
|
||||||
|
return expected_patts
|
||||||
|
# yield child
|
||||||
|
|
||||||
|
|
||||||
|
def test_pause_from_asyncio_task(
|
||||||
|
spawn,
|
||||||
|
ctlc: bool
|
||||||
|
# ^TODO, fix for `asyncio`!!
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Verify we can use the `pdbp` REPL from an `asyncio.Task` spawned using
|
||||||
|
APIs in `.to_asyncio`.
|
||||||
|
|
||||||
|
`examples/debugging/asycio_bp.py`
|
||||||
|
|
||||||
|
'''
|
||||||
|
child = spawn('asyncio_bp')
|
||||||
|
|
||||||
|
# RACE on whether trio/asyncio task bps first
|
||||||
|
attach_patts: dict[str, list[str]] = {
|
||||||
|
|
||||||
|
# first pause in guest-mode (aka "infecting")
|
||||||
|
# `trio.Task`.
|
||||||
|
'trio-side': [
|
||||||
|
_pause_msg,
|
||||||
|
"<Task 'trio_ctx'",
|
||||||
|
"('aio_daemon'",
|
||||||
|
],
|
||||||
|
|
||||||
|
# `breakpoint()` from `asyncio.Task`.
|
||||||
|
'asyncio-side': [
|
||||||
|
_pause_msg,
|
||||||
|
"<Task pending name='Task-2' coro=<greenback_shim()",
|
||||||
|
"('aio_daemon'",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
while attach_patts:
|
||||||
|
expect_any_of(
|
||||||
|
attach_patts=attach_patts,
|
||||||
|
child=child,
|
||||||
|
ctlc=ctlc,
|
||||||
|
)
|
||||||
|
child.sendline('c')
|
||||||
|
|
||||||
|
# NOW in race order,
|
||||||
|
# - the asyncio-task will error
|
||||||
|
# - the root-actor parent task will pause
|
||||||
|
#
|
||||||
|
attach_patts: dict[str, list[str]] = {
|
||||||
|
|
||||||
|
# error raised in `asyncio.Task`
|
||||||
|
"raise ValueError('asyncio side error!')": [
|
||||||
|
_crash_msg,
|
||||||
|
'return await chan.receive()', # `.to_asyncio` impl internals in tb
|
||||||
|
"<Task 'trio_ctx'",
|
||||||
|
"@ ('aio_daemon'",
|
||||||
|
"ValueError: asyncio side error!",
|
||||||
|
],
|
||||||
|
|
||||||
|
# parent-side propagation via actor-nursery/portal
|
||||||
|
# "tractor._exceptions.RemoteActorError: remote task raised a 'ValueError'": [
|
||||||
|
"remote task raised a 'ValueError'": [
|
||||||
|
_crash_msg,
|
||||||
|
"src_uid=('aio_daemon'",
|
||||||
|
"('aio_daemon'",
|
||||||
|
],
|
||||||
|
|
||||||
|
# a final pause in root-actor
|
||||||
|
"<Task '__main__.main'": [
|
||||||
|
_pause_msg,
|
||||||
|
"<Task '__main__.main'",
|
||||||
|
"('root'",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
while attach_patts:
|
||||||
|
expect_any_of(
|
||||||
|
attach_patts=attach_patts,
|
||||||
|
child=child,
|
||||||
|
ctlc=ctlc,
|
||||||
|
)
|
||||||
|
child.sendline('c')
|
||||||
|
|
||||||
|
assert not attach_patts
|
||||||
|
|
||||||
|
# final boxed error propagates to root
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_crash_msg,
|
||||||
|
"<Task '__main__.main'",
|
||||||
|
"('root'",
|
||||||
|
"remote task raised a 'ValueError'",
|
||||||
|
"ValueError: asyncio side error!",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(
|
||||||
|
child,
|
||||||
|
# NOTE: setting this to 0 (or some other sufficient
|
||||||
|
# small val) can cause the test to fail since the
|
||||||
|
# `subactor` suffers a race where the root/parent
|
||||||
|
# sends an actor-cancel prior to it hitting its pause
|
||||||
|
# point; by def the value is 0.1
|
||||||
|
delay=0.4,
|
||||||
|
)
|
||||||
|
|
||||||
|
child.sendline('c')
|
||||||
|
child.expect(EOF)
|
|
@ -955,7 +955,7 @@ async def echo_back_sequence(
|
||||||
)
|
)
|
||||||
|
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
# await tractor.breakpoint()
|
# await tractor.pause()
|
||||||
async with ctx.open_stream(
|
async with ctx.open_stream(
|
||||||
msg_buffer_size=msg_buffer_size,
|
msg_buffer_size=msg_buffer_size,
|
||||||
|
|
||||||
|
|
|
@ -271,7 +271,7 @@ def test_faster_task_to_recv_is_cancelled_by_slower(
|
||||||
# the faster subtask was cancelled
|
# the faster subtask was cancelled
|
||||||
break
|
break
|
||||||
|
|
||||||
# await tractor.breakpoint()
|
# await tractor.pause()
|
||||||
# await stream.receive()
|
# await stream.receive()
|
||||||
print(f'final value: {value}')
|
print(f'final value: {value}')
|
||||||
|
|
||||||
|
|
|
@ -730,6 +730,9 @@ class DebugStatus:
|
||||||
# -[ ] see if we can get our proto oco task-mngr to work for
|
# -[ ] see if we can get our proto oco task-mngr to work for
|
||||||
# this?
|
# this?
|
||||||
repl_task: Task|None = None
|
repl_task: Task|None = None
|
||||||
|
# repl_thread: Thread|None = None
|
||||||
|
# ^TODO?
|
||||||
|
|
||||||
repl_release: trio.Event|None = None
|
repl_release: trio.Event|None = None
|
||||||
|
|
||||||
req_task: Task|None = None
|
req_task: Task|None = None
|
||||||
|
@ -839,11 +842,12 @@ class DebugStatus:
|
||||||
if (
|
if (
|
||||||
not cls.is_main_trio_thread()
|
not cls.is_main_trio_thread()
|
||||||
and
|
and
|
||||||
# not _state._runtime_vars.get(
|
not _state._runtime_vars.get(
|
||||||
# '_is_infected_aio',
|
'_is_infected_aio',
|
||||||
# False,
|
False,
|
||||||
# )
|
)
|
||||||
not current_actor().is_infected_aio()
|
# not current_actor().is_infected_aio()
|
||||||
|
# ^XXX, since for bg-thr case will always raise..
|
||||||
):
|
):
|
||||||
trio.from_thread.run_sync(
|
trio.from_thread.run_sync(
|
||||||
signal.signal,
|
signal.signal,
|
||||||
|
@ -928,12 +932,27 @@ class DebugStatus:
|
||||||
try:
|
try:
|
||||||
# sometimes the task might already be terminated in
|
# sometimes the task might already be terminated in
|
||||||
# which case this call will raise an RTE?
|
# which case this call will raise an RTE?
|
||||||
if repl_release is not None:
|
# See below for reporting on that..
|
||||||
|
if (
|
||||||
|
repl_release is not None
|
||||||
|
and
|
||||||
|
not repl_release.is_set()
|
||||||
|
):
|
||||||
if cls.is_main_trio_thread():
|
if cls.is_main_trio_thread():
|
||||||
repl_release.set()
|
repl_release.set()
|
||||||
|
|
||||||
elif current_actor().is_infected_aio():
|
elif (
|
||||||
|
_state._runtime_vars.get(
|
||||||
|
'_is_infected_aio',
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
# ^XXX, again bc we need to not except
|
||||||
|
# but for bg-thread case it will always raise..
|
||||||
|
#
|
||||||
|
# TODO, is there a better api then using
|
||||||
|
# `err_on_no_runtime=False` in the below?
|
||||||
|
# current_actor().is_infected_aio()
|
||||||
|
):
|
||||||
async def _set_repl_release():
|
async def _set_repl_release():
|
||||||
repl_release.set()
|
repl_release.set()
|
||||||
|
|
||||||
|
@ -949,6 +968,15 @@ class DebugStatus:
|
||||||
trio.from_thread.run_sync(
|
trio.from_thread.run_sync(
|
||||||
repl_release.set
|
repl_release.set
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except RuntimeError as rte:
|
||||||
|
log.exception(
|
||||||
|
f'Failed to release debug-request ??\n\n'
|
||||||
|
f'{cls.repr()}\n'
|
||||||
|
)
|
||||||
|
# pdbp.set_trace()
|
||||||
|
raise rte
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# if req_ctx := cls.req_ctx:
|
# if req_ctx := cls.req_ctx:
|
||||||
# req_ctx._scope.cancel()
|
# req_ctx._scope.cancel()
|
||||||
|
@ -976,11 +1004,12 @@ class DebugStatus:
|
||||||
# logging when we don't need to?
|
# logging when we don't need to?
|
||||||
cls.repl = None
|
cls.repl = None
|
||||||
|
|
||||||
# restore original sigint handler
|
# maybe restore original sigint handler
|
||||||
|
# XXX requires runtime check to avoid crash!
|
||||||
|
if current_actor(err_on_no_runtime=False):
|
||||||
cls.unshield_sigint()
|
cls.unshield_sigint()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: use the new `@lowlevel.singleton` for this!
|
# TODO: use the new `@lowlevel.singleton` for this!
|
||||||
def get_debug_req() -> DebugStatus|None:
|
def get_debug_req() -> DebugStatus|None:
|
||||||
return DebugStatus
|
return DebugStatus
|
||||||
|
@ -1066,7 +1095,7 @@ class PdbREPL(pdbp.Pdb):
|
||||||
# Lock.release(raise_on_thread=False)
|
# Lock.release(raise_on_thread=False)
|
||||||
Lock.release()
|
Lock.release()
|
||||||
|
|
||||||
# XXX after `Lock.release()` for root local repl usage
|
# XXX AFTER `Lock.release()` for root local repl usage
|
||||||
DebugStatus.release()
|
DebugStatus.release()
|
||||||
|
|
||||||
def set_quit(self):
|
def set_quit(self):
|
||||||
|
@ -1672,7 +1701,7 @@ class DebugRequestError(RuntimeError):
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
_repl_fail_msg: str = (
|
_repl_fail_msg: str|None = (
|
||||||
'Failed to REPl via `_pause()` '
|
'Failed to REPl via `_pause()` '
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1712,6 +1741,7 @@ async def _pause(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
pause_err: BaseException|None = None
|
||||||
actor: Actor = current_actor()
|
actor: Actor = current_actor()
|
||||||
try:
|
try:
|
||||||
task: Task = current_task()
|
task: Task = current_task()
|
||||||
|
@ -2094,11 +2124,13 @@ async def _pause(
|
||||||
|
|
||||||
# TODO: prolly factor this plus the similar block from
|
# TODO: prolly factor this plus the similar block from
|
||||||
# `_enter_repl_sync()` into a common @cm?
|
# `_enter_repl_sync()` into a common @cm?
|
||||||
except BaseException as pause_err:
|
except BaseException as _pause_err:
|
||||||
|
pause_err: BaseException = _pause_err
|
||||||
if isinstance(pause_err, bdb.BdbQuit):
|
if isinstance(pause_err, bdb.BdbQuit):
|
||||||
log.devx(
|
log.devx(
|
||||||
'REPL for pdb was quit!\n'
|
'REPL for pdb was explicitly quit!\n'
|
||||||
)
|
)
|
||||||
|
_repl_fail_msg = None
|
||||||
|
|
||||||
# when the actor is mid-runtime cancellation the
|
# when the actor is mid-runtime cancellation the
|
||||||
# `Actor._service_n` might get closed before we can spawn
|
# `Actor._service_n` might get closed before we can spawn
|
||||||
|
@ -2117,13 +2149,18 @@ async def _pause(
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
elif isinstance(pause_err, trio.Cancelled):
|
||||||
log.exception(
|
_repl_fail_msg = (
|
||||||
_repl_fail_msg
|
'You called `tractor.pause()` from an already cancelled scope!\n\n'
|
||||||
+
|
'Consider `await tractor.pause(shield=True)` to make it work B)\n'
|
||||||
f'on behalf of {repl_task} ??\n'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
_repl_fail_msg += f'on behalf of {repl_task} ??\n'
|
||||||
|
|
||||||
|
if _repl_fail_msg:
|
||||||
|
log.exception(_repl_fail_msg)
|
||||||
|
|
||||||
if not actor.is_infected_aio():
|
if not actor.is_infected_aio():
|
||||||
DebugStatus.release(cancel_req_task=True)
|
DebugStatus.release(cancel_req_task=True)
|
||||||
|
|
||||||
|
@ -2152,6 +2189,8 @@ async def _pause(
|
||||||
DebugStatus.req_err
|
DebugStatus.req_err
|
||||||
or
|
or
|
||||||
repl_err
|
repl_err
|
||||||
|
or
|
||||||
|
pause_err
|
||||||
):
|
):
|
||||||
__tracebackhide__: bool = False
|
__tracebackhide__: bool = False
|
||||||
|
|
||||||
|
@ -2435,6 +2474,8 @@ def pause_from_sync(
|
||||||
called_from_builtin: bool = False,
|
called_from_builtin: bool = False,
|
||||||
api_frame: FrameType|None = None,
|
api_frame: FrameType|None = None,
|
||||||
|
|
||||||
|
allow_no_runtime: bool = False,
|
||||||
|
|
||||||
# proxy to `._pause()`, for ex:
|
# proxy to `._pause()`, for ex:
|
||||||
# shield: bool = False,
|
# shield: bool = False,
|
||||||
# api_frame: FrameType|None = None,
|
# api_frame: FrameType|None = None,
|
||||||
|
@ -2453,16 +2494,25 @@ def pause_from_sync(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
repl_owner: Task|Thread|None = None
|
||||||
try:
|
try:
|
||||||
actor: tractor.Actor = current_actor(
|
actor: tractor.Actor = current_actor(
|
||||||
err_on_no_runtime=False,
|
err_on_no_runtime=False,
|
||||||
)
|
)
|
||||||
if not actor:
|
if (
|
||||||
raise RuntimeError(
|
not actor
|
||||||
'Not inside the `tractor`-runtime?\n'
|
and
|
||||||
|
not allow_no_runtime
|
||||||
|
):
|
||||||
|
raise NoRuntime(
|
||||||
|
'The actor runtime has not been opened?\n\n'
|
||||||
'`tractor.pause_from_sync()` is not functional without a wrapping\n'
|
'`tractor.pause_from_sync()` is not functional without a wrapping\n'
|
||||||
'- `async with tractor.open_nursery()` or,\n'
|
'- `async with tractor.open_nursery()` or,\n'
|
||||||
'- `async with tractor.open_root_actor()`\n'
|
'- `async with tractor.open_root_actor()`\n\n'
|
||||||
|
|
||||||
|
'If you are getting this from a builtin `breakpoint()` call\n'
|
||||||
|
'it might mean the runtime was started then '
|
||||||
|
'stopped prematurely?\n'
|
||||||
)
|
)
|
||||||
message: str = (
|
message: str = (
|
||||||
f'{actor.uid} task called `tractor.pause_from_sync()`\n'
|
f'{actor.uid} task called `tractor.pause_from_sync()`\n'
|
||||||
|
@ -2485,6 +2535,7 @@ def pause_from_sync(
|
||||||
repl: PdbREPL = mk_pdb()
|
repl: PdbREPL = mk_pdb()
|
||||||
|
|
||||||
# message += f'-> created local REPL {repl}\n'
|
# message += f'-> created local REPL {repl}\n'
|
||||||
|
is_trio_thread: bool = DebugStatus.is_main_trio_thread()
|
||||||
is_root: bool = is_root_process()
|
is_root: bool = is_root_process()
|
||||||
is_aio: bool = actor.is_infected_aio()
|
is_aio: bool = actor.is_infected_aio()
|
||||||
|
|
||||||
|
@ -2500,7 +2551,7 @@ def pause_from_sync(
|
||||||
# thread which will call `._pause()` manually with special
|
# thread which will call `._pause()` manually with special
|
||||||
# handling for root-actor caller usage.
|
# handling for root-actor caller usage.
|
||||||
if (
|
if (
|
||||||
not DebugStatus.is_main_trio_thread()
|
not is_trio_thread
|
||||||
and
|
and
|
||||||
not is_aio # see below for this usage
|
not is_aio # see below for this usage
|
||||||
):
|
):
|
||||||
|
@ -2574,7 +2625,11 @@ def pause_from_sync(
|
||||||
DebugStatus.shield_sigint()
|
DebugStatus.shield_sigint()
|
||||||
assert bg_task is not DebugStatus.repl_task
|
assert bg_task is not DebugStatus.repl_task
|
||||||
|
|
||||||
elif is_aio:
|
elif (
|
||||||
|
not is_trio_thread
|
||||||
|
and
|
||||||
|
is_aio
|
||||||
|
):
|
||||||
greenback: ModuleType = maybe_import_greenback()
|
greenback: ModuleType = maybe_import_greenback()
|
||||||
repl_owner: Task = asyncio.current_task()
|
repl_owner: Task = asyncio.current_task()
|
||||||
DebugStatus.shield_sigint()
|
DebugStatus.shield_sigint()
|
||||||
|
@ -2758,9 +2813,11 @@ def _post_mortem(
|
||||||
# ^TODO, instead a nice runtime-info + maddr + uid?
|
# ^TODO, instead a nice runtime-info + maddr + uid?
|
||||||
# -[ ] impl a `Actor.__repr()__`??
|
# -[ ] impl a `Actor.__repr()__`??
|
||||||
# |_ <task>:<thread> @ <actor>
|
# |_ <task>:<thread> @ <actor>
|
||||||
|
# no_runtime: bool = False
|
||||||
|
|
||||||
except NoRuntime:
|
except NoRuntime:
|
||||||
actor_repr: str = '<no-actor-runtime?>'
|
actor_repr: str = '<no-actor-runtime?>'
|
||||||
|
# no_runtime: bool = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
task_repr: Task = current_task()
|
task_repr: Task = current_task()
|
||||||
|
@ -2796,6 +2853,8 @@ def _post_mortem(
|
||||||
# Since we presume the post-mortem was enaged to a task-ending
|
# Since we presume the post-mortem was enaged to a task-ending
|
||||||
# error, we MUST release the local REPL request so that not other
|
# error, we MUST release the local REPL request so that not other
|
||||||
# local task nor the root remains blocked!
|
# local task nor the root remains blocked!
|
||||||
|
# if not no_runtime:
|
||||||
|
# DebugStatus.release()
|
||||||
DebugStatus.release()
|
DebugStatus.release()
|
||||||
|
|
||||||
|
|
||||||
|
@ -3033,6 +3092,7 @@ async def maybe_wait_for_debugger(
|
||||||
# pass
|
# pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# TODO: better naming and what additionals?
|
# TODO: better naming and what additionals?
|
||||||
# - [ ] optional runtime plugging?
|
# - [ ] optional runtime plugging?
|
||||||
# - [ ] detection for sync vs. async code?
|
# - [ ] detection for sync vs. async code?
|
||||||
|
|
Loading…
Reference in New Issue