Compare commits

..

5 Commits

Author SHA1 Message Date
Tyler Goodlet b875b35b98 Change `tractor.breakpoint()` to new `.pause()` in test suite 2024-12-09 16:08:55 -05:00
Tyler Goodlet 46ddc214cd Wrap `asyncio_bp.py` ex into test suite
Ensuring we can at least use `breakpoint()` from an infected actor's
`asyncio.Task` spawned via a `.to_asyncio` API.

Also includes a little `tests/devx/` reorging,
- start splitting out non-`tractor.pause()` tests into a new
  `test_pause_from_non_trio.py` for all the `.pause_from_sync()`
  use in bg-threaded or `asyncio` applications.
- factor harness commonalities to the `devx/conftest` (namely
  the `do_ctlc()` masher).
- mv `test_pause_from_sync` to the new non`-trio` mod.

NOTE, the `ctlc=True` is still failing for
`test_pause_from_asyncio_task` which is a user-happiness bug but not
anything fundamentally broken - just need to handle the `asyncio` case
in `.devx._debug.sigint_shield()`!
2024-12-09 15:38:28 -05:00
Tyler Goodlet b3ee20d3b9 Add `breakpoint()` hook restoration example + test 2024-12-05 20:56:39 -05:00
Tyler Goodlet cf3e6c1218 Rename `n: trio.Nursery` -> `tn` (task nursery) 2024-12-04 14:01:38 -05:00
Tyler Goodlet 8af9b0201d Messy-teardown `DebugStatus` related fixes
Mostly fixing edge cases with `asyncio` and/or bg threads where the
`.repl_release: trio.Event` needs to be used from the main `trio`
thread OW confusing-but-valid teardown tracebacks can show under various
races.

Also improve,
- log reporting for such internal bugs to make them more obvious on
  console via `log.exception()`.
- only restore the SIGINT handler when runtime is (still) active.
- reporting when `tractor.pause(shield=True)` should be used and
  unhiding the internal frames from the tb in that case.
- for `pause_from_sync()` some deep fixes..
 |_add a `allow_no_runtime: bool = False` flag to allow
   **not** requiring the actor runtime to be active.
 |_fix the `greenback` case-branch to only trigger on `not
   is_trio_thread`.
 |_add a scope-global `repl_owner: Task|Thread|None = None` to
   avoid ref errors..
2024-12-03 15:26:25 -05:00
15 changed files with 548 additions and 236 deletions

View File

@ -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?

View File

@ -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

View File

@ -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!'

View File

@ -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

View File

@ -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():

View File

@ -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)

View File

@ -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)

View File

@ -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__':

View File

@ -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():

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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}')

View File

@ -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?