221 lines
5.9 KiB
Python
221 lines
5.9 KiB
Python
# 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/>.
|
|
|
|
'''
|
|
Debugger synchronization APIs to ensure orderly access and
|
|
non-TTY-clobbering graceful teardown.
|
|
|
|
|
|
'''
|
|
from __future__ import annotations
|
|
from contextlib import (
|
|
asynccontextmanager as acm,
|
|
)
|
|
from functools import (
|
|
partial,
|
|
)
|
|
from typing import (
|
|
AsyncGenerator,
|
|
Callable,
|
|
)
|
|
|
|
from tractor.log import get_logger
|
|
import trio
|
|
from trio.lowlevel import (
|
|
current_task,
|
|
Task,
|
|
)
|
|
from tractor._context import Context
|
|
from tractor._state import (
|
|
current_actor,
|
|
debug_mode,
|
|
is_root_process,
|
|
)
|
|
from ._repl import (
|
|
TractorConfig as TractorConfig,
|
|
)
|
|
from ._tty_lock import (
|
|
Lock,
|
|
request_root_stdio_lock,
|
|
any_connected_locker_child,
|
|
)
|
|
from ._sigint import (
|
|
sigint_shield as sigint_shield,
|
|
_ctlc_ignore_header as _ctlc_ignore_header
|
|
)
|
|
|
|
log = get_logger(__package__)
|
|
|
|
|
|
async def maybe_wait_for_debugger(
|
|
poll_steps: int = 2,
|
|
poll_delay: float = 0.1,
|
|
child_in_debug: bool = False,
|
|
|
|
header_msg: str = '',
|
|
_ll: str = 'devx',
|
|
|
|
) -> bool: # was locked and we polled?
|
|
|
|
if (
|
|
not debug_mode()
|
|
and
|
|
not child_in_debug
|
|
):
|
|
return False
|
|
|
|
logmeth: Callable = getattr(log, _ll)
|
|
|
|
msg: str = header_msg
|
|
if (
|
|
is_root_process()
|
|
):
|
|
# If we error in the root but the debugger is
|
|
# engaged we don't want to prematurely kill (and
|
|
# thus clobber access to) the local tty since it
|
|
# will make the pdb repl unusable.
|
|
# Instead try to wait for pdb to be released before
|
|
# tearing down.
|
|
ctx_in_debug: Context|None = Lock.ctx_in_debug
|
|
in_debug: tuple[str, str]|None = (
|
|
ctx_in_debug.chan.uid
|
|
if ctx_in_debug
|
|
else None
|
|
)
|
|
if in_debug == current_actor().uid:
|
|
log.debug(
|
|
msg
|
|
+
|
|
'Root already owns the TTY LOCK'
|
|
)
|
|
return True
|
|
|
|
elif in_debug:
|
|
msg += (
|
|
f'Debug `Lock` in use by subactor\n|\n|_{in_debug}\n'
|
|
)
|
|
# TODO: could this make things more deterministic?
|
|
# wait to see if a sub-actor task will be
|
|
# scheduled and grab the tty lock on the next
|
|
# tick?
|
|
# XXX => but it doesn't seem to work..
|
|
# await trio.testing.wait_all_tasks_blocked(cushion=0)
|
|
else:
|
|
logmeth(
|
|
msg
|
|
+
|
|
'Root immediately acquired debug TTY LOCK'
|
|
)
|
|
return False
|
|
|
|
for istep in range(poll_steps):
|
|
if (
|
|
Lock.req_handler_finished is not None
|
|
and not Lock.req_handler_finished.is_set()
|
|
and in_debug is not None
|
|
):
|
|
# caller_frame_info: str = pformat_caller_frame()
|
|
logmeth(
|
|
msg
|
|
+
|
|
'\n^^ Root is waiting on tty lock release.. ^^\n'
|
|
# f'{caller_frame_info}\n'
|
|
)
|
|
|
|
if not any_connected_locker_child():
|
|
Lock.get_locking_task_cs().cancel()
|
|
|
|
with trio.CancelScope(shield=True):
|
|
await Lock.req_handler_finished.wait()
|
|
|
|
log.devx(
|
|
f'Subactor released debug lock\n'
|
|
f'|_{in_debug}\n'
|
|
)
|
|
break
|
|
|
|
# is no subactor locking debugger currently?
|
|
if (
|
|
in_debug is None
|
|
and (
|
|
Lock.req_handler_finished is None
|
|
or Lock.req_handler_finished.is_set()
|
|
)
|
|
):
|
|
logmeth(
|
|
msg
|
|
+
|
|
'Root acquired tty lock!'
|
|
)
|
|
break
|
|
|
|
else:
|
|
logmeth(
|
|
'Root polling for debug:\n'
|
|
f'poll step: {istep}\n'
|
|
f'poll delya: {poll_delay}\n\n'
|
|
f'{Lock.repr()}\n'
|
|
)
|
|
with trio.CancelScope(shield=True):
|
|
await trio.sleep(poll_delay)
|
|
continue
|
|
|
|
return True
|
|
|
|
# else:
|
|
# # TODO: non-root call for #320?
|
|
# this_uid: tuple[str, str] = current_actor().uid
|
|
# async with acquire_debug_lock(
|
|
# subactor_uid=this_uid,
|
|
# ):
|
|
# pass
|
|
return False
|
|
|
|
|
|
@acm
|
|
async def acquire_debug_lock(
|
|
subactor_uid: tuple[str, str],
|
|
) -> AsyncGenerator[
|
|
trio.CancelScope|None,
|
|
tuple,
|
|
]:
|
|
'''
|
|
Request to acquire the TTY `Lock` in the root actor, release on
|
|
exit.
|
|
|
|
This helper is for actor's who don't actually need to acquired
|
|
the debugger but want to wait until the lock is free in the
|
|
process-tree root such that they don't clobber an ongoing pdb
|
|
REPL session in some peer or child!
|
|
|
|
'''
|
|
if not debug_mode():
|
|
yield None
|
|
return
|
|
|
|
task: Task = current_task()
|
|
async with trio.open_nursery() as n:
|
|
ctx: Context = await n.start(
|
|
partial(
|
|
request_root_stdio_lock,
|
|
actor_uid=subactor_uid,
|
|
task_uid=(task.name, id(task)),
|
|
)
|
|
)
|
|
yield ctx
|
|
ctx.cancel()
|