fix stuck screen

This commit is contained in:
Will McGugan
2023-01-23 11:57:48 +01:00
parent cc1f2f61d9
commit 3b9bc0d536
6 changed files with 136 additions and 43 deletions

View File

@@ -1,9 +1,17 @@
from __future__ import annotations
import asyncio
from functools import lru_cache
from inspect import signature, isawaitable
from typing import Any, Callable
from typing import Any, Callable, TYPE_CHECKING
from . import active_app
if TYPE_CHECKING:
from .app import App
# Maximum seconds before warning about a slow callback
INVOKE_TIMEOUT_WARNING = 3
@lru_cache(maxsize=2048)
@@ -12,7 +20,7 @@ def count_parameters(func: Callable) -> int:
return len(signature(func).parameters)
async def invoke(callback: Callable, *params: object) -> Any:
async def _invoke(callback: Callable, *params: object) -> Any:
"""Invoke a callback with an arbitrary number of parameters.
Args:
@@ -27,3 +35,38 @@ async def invoke(callback: Callable, *params: object) -> Any:
if isawaitable(result):
result = await result
return result
async def invoke(callback: Callable, *params: object) -> Any:
"""Invoke a callback with an arbitrary number of parameters.
Args:
callback: The callable to be invoked.
Returns:
The return value of the invoked callable.
"""
app: App | None
try:
app = active_app.get()
except LookupError:
app = None
if app is not None and "debug" in app.features:
# In debug mode we will warn about callbacks that may be stuck
def log_slow() -> None:
"""Log a message regarding a slow callback."""
app.log.warning(
f"Callback {callback} is still pending after {INVOKE_TIMEOUT_WARNING} seconds"
)
call_later_handle = asyncio.get_running_loop().call_later(
INVOKE_TIMEOUT_WARNING, log_slow
)
try:
return await _invoke(callback, *params)
finally:
call_later_handle.cancel()
else:
return await _invoke(callback, *params)

View File

@@ -1191,9 +1191,13 @@ class App(Generic[ReturnType], DOMNode):
_screen = self.get_screen(screen)
if not _screen.is_running:
widgets = self._register(self, _screen)
return (_screen, AwaitMount(_screen, widgets))
await_mount = AwaitMount(_screen, widgets)
self.call_next(await_mount)
return (_screen, await_mount)
else:
return (_screen, AwaitMount(_screen, []))
await_mount = AwaitMount(_screen, [])
self.call_next(await_mount)
return (_screen, await_mount)
def _replace_screen(self, screen: Screen) -> Screen:
"""Handle the replaced screen.
@@ -2013,6 +2017,12 @@ class App(Generic[ReturnType], DOMNode):
await self._close_messages()
async def _on_resize(self, event: events.Resize) -> None:
# print(self.screen, self._screen_stack)
# print(self.screen.size)
# print(self.screen.is_running)
# print((self.screen._message_queue))
# print(self.screen._active_message)
# print(self.screen._task)
event.stop()
await self.screen.post_message(event)

View File

@@ -64,6 +64,7 @@ class MessagePumpMeta(type):
class MessagePump(metaclass=MessagePumpMeta):
def __init__(self, parent: MessagePump | None = None) -> None:
self._message_queue: Queue[Message | None] = Queue()
self._active_nessage: Message | None = None
self._parent = parent
self._running: bool = False
self._closing: bool = False
@@ -75,6 +76,7 @@ class MessagePump(metaclass=MessagePumpMeta):
self._last_idle: float = time()
self._max_idle: float | None = None
self._mounted_event = asyncio.Event()
self._next_callbacks: list[CallbackType] = []
@property
def task(self) -> Task:
@@ -271,10 +273,22 @@ class MessagePump(metaclass=MessagePumpMeta):
Args:
callback: Callable to call next.
*args: Positional arguments to pass to the callable.
**kwargs: Keyword arguments to pass to the callable.
"""
message = events.Callback(self, callback=partial(callback, *args, **kwargs))
self.post_message_no_wait(message)
def call_next(self, callback: Callable, *args, **kwargs) -> None:
"""Schedule a callback to run immediately after processing the current message.
Args:
callback: Callable to run after current event.
*args: Positional arguments to pass to the callable.
**kwargs: Keyword arguments to pass to the callable.
"""
self._next_callbacks.append(partial(callback, *args, **kwargs))
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
# Forward InvokeLater message to the Screen
self.app.screen._invoke_later(message.callback)
@@ -367,35 +381,52 @@ class MessagePump(metaclass=MessagePumpMeta):
except MessagePumpClosed:
break
self._active_message = message
try:
await self._dispatch_message(message)
except CancelledError:
raise
try:
await self._dispatch_message(message)
except CancelledError:
raise
except Exception as error:
self._mounted_event.set()
self.app._handle_exception(error)
break
finally:
self._message_queue.task_done()
current_time = time()
# Insert idle events
if self._message_queue.empty() or (
self._max_idle is not None
and current_time - self._last_idle > self._max_idle
):
self._last_idle = current_time
if not self._closed:
event = events.Idle(self)
for _cls, method in self._get_dispatch_methods(
"on_idle", event
):
try:
await invoke(method, event)
except Exception as error:
self.app._handle_exception(error)
break
finally:
self._active_message = None
async def _flush_next_callbacks(self) -> None:
"""Invoke pending callbacks in next callbacks queue."""
callbacks = self._next_callbacks.copy()
self._next_callbacks.clear()
for callback in callbacks:
try:
await invoke(callback)
except Exception as error:
self._mounted_event.set()
self.app._handle_exception(error)
break
finally:
self._message_queue.task_done()
current_time = time()
# Insert idle events
if self._message_queue.empty() or (
self._max_idle is not None
and current_time - self._last_idle > self._max_idle
):
self._last_idle = current_time
if not self._closed:
event = events.Idle(self)
for _cls, method in self._get_dispatch_methods(
"on_idle", event
):
try:
await invoke(method, event)
except Exception as error:
self.app._handle_exception(error)
break
async def _dispatch_message(self, message: Message) -> None:
"""Dispatch a message received from the message queue.
@@ -412,6 +443,7 @@ class MessagePump(metaclass=MessagePumpMeta):
await self.on_event(message)
else:
await self._on_message(message)
await self._flush_next_callbacks()
def _get_dispatch_methods(
self, method_name: str, message: Message

View File

@@ -367,19 +367,20 @@ class Screen(Widget):
# Check for any widgets marked as 'dirty' (needs a repaint)
event.prevent_default()
async with self.app._dom_lock:
if self.is_current:
if self._layout_required:
self._refresh_layout()
self._layout_required = False
self._dirty_widgets.clear()
if self._repaint_required:
self._dirty_widgets.clear()
self._dirty_widgets.add(self)
self._repaint_required = False
if self.is_current:
async with self.app._dom_lock:
if self.is_current:
if self._layout_required:
self._refresh_layout()
self._layout_required = False
self._dirty_widgets.clear()
if self._repaint_required:
self._dirty_widgets.clear()
self._dirty_widgets.add(self)
self._repaint_required = False
if self._dirty_widgets:
self.update_timer.resume()
if self._dirty_widgets:
self.update_timer.resume()
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
await self._invoke_and_clear_callbacks()
@@ -423,6 +424,7 @@ class Screen(Widget):
def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
"""Refresh the layout (can change size and positions of widgets)."""
print("Screen._refresh_layout", size, full)
size = self.outer_size if size is None else size
if not size:
return

View File

@@ -91,6 +91,9 @@ class AwaitMount:
self._parent = parent
self._widgets = widgets
async def __call__(self) -> None:
await self
def __await__(self) -> Generator[None, None, None]:
async def await_mount() -> None:
if self._widgets:
@@ -606,7 +609,9 @@ class Widget(DOMNode):
parent, *widgets, before=insert_before, after=insert_after
)
return AwaitMount(self, mounted)
await_mount = AwaitMount(self, mounted)
self.call_next(await_mount)
return await_mount
def move_child(
self,

View File

@@ -55,6 +55,7 @@ class Footer(Widget):
async def watch_highlight_key(self, value) -> None:
"""If highlight key changes we need to regenerate the text."""
self._key_text = None
self.refresh()
def on_mount(self) -> None:
watch(self.screen, "focused", self._focus_changed)