Textualize / textual

The lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.
https://textual.textualize.io/
MIT License
25.17k stars 772 forks source link

Running self.run_worker crashes the app with no error message #5064

Open mzebrak opened 2 days ago

mzebrak commented 2 days ago

This was observed in: https://github.com/Textualize/textual/issues/5008 but I think it's a separate issue.

MRE:

 from __future__ import annotations

from typing import cast

from textual import on
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label

class FirstScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Header()
        yield Label("this is the first screen")
        yield Footer()

class SecondScreen(Screen):
    BINDINGS = [
        Binding("a", "finish", "Here it works"),  # key binding works just fine
    ]
    def compose(self) -> ComposeResult:
        yield Header()
        yield Label("This is the second screen")
        yield Button("Here it doesn't work")
        yield Footer()

    async def action_finish(self) -> None:
        await self._finish()

    @on(Button.Pressed)
    async def finish(self) -> None:
        # Running in worker causes app closure with no error
        # can't run directly also because of a deadlock  https://github.com/Textualize/textual/issues/5008
        self.run_worker(self._finish())

    async def _finish(self) -> None:
        app = cast(MyApp, self.app)
        await app.action_pop_until_first_screen()
        self.app.mount(Label("done"))  # assume something needs to be done after awaiting previous action

class MyApp(App):
    BINDINGS = [
        Binding("d", "push_second_screen", "Push second screen"),
    ]

    async def on_mount(self) -> None:
        await self.push_screen(FirstScreen())

    async def action_pop_until_first_screen(self) -> None:
        while not isinstance(self.screen, FirstScreen):
            await self.pop_screen()

    async def action_push_second_screen(self) -> None:
        await self.push_screen(SecondScreen())

MyApp().run()

I investigated a bit and found out that when the app closure happens, there is asyncio.CancelledError being raised from this line: https://github.com/Textualize/textual/blob/main/src/textual/worker.py#L339

After adding a wrapper to log cancellation inspired by https://stackoverflow.com/a/71356489 and some more logging looks like it comes from await_prune of AwaitRemove (log from mentioned issue and its MRE, but I hope helps a bit):

 272 │ 2024-09-26 10:35:07.036 | ❌ ERROR    | textual.worker:_run:380 - In worker run of: action_pop_current_screen
 273 │ 2024-09-26 10:35:07.037 | ❌ ERROR    | textual.worker:_run_async:338 - In _run_async of: action_pop_current_screen
 274 │ 2024-09-26 10:35:07.037 | ❌ ERROR    | textual.worker:_run_async:339 - self._work is <coroutine object MyApp.action_pop_current_screen at 0x7f13aa1a78b0>
 275 │ 2024-09-26 10:35:07.037 | ❌ ERROR    | textual.worker:_run_async:348 - awaiting self._work
 276 │ 2024-09-26 10:35:07.064 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function sleep at 0x7f13aefa4e50>
 277 │ 2024-09-26 10:35:07.064 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function Timer._run at 0x7f13aee51fc0>
 278 │ 2024-09-26 10:35:07.064 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function sleep at 0x7f13aefa4e50>
 279 │ 2024-09-26 10:35:07.065 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function Timer._run at 0x7f13aee51fc0>
 280 │ 2024-09-26 10:35:07.067 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function sleep at 0x7f13aefa4e50>
 281 │ 2024-09-26 10:35:07.068 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function Timer._run at 0x7f13aee51fc0>
 282 │ 2024-09-26 10:35:07.069 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function sleep at 0x7f13aefa4e50>
 283 │ 2024-09-26 10:35:07.069 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function Timer._run at 0x7f13aee51fc0>
 284 │ 2024-09-26 10:35:07.070 | ❌ ERROR    | textual.utils:wrapper:27 - Cancelled <function AwaitRemove.__await__.<locals>.await_prune at 0x7f13a9fe1360>
 285 │ 2024-09-26 10:35:07.070 | ❌ ERROR    | textual.worker:_run_async:352 - CancelledError: CancelledError()

further narrowing shows it comes from await gather(*tasks) and we can see there is <Task cancelled name='message pump SecondScreen()', ...> there

github-actions[bot] commented 2 days ago

We found the following entries in the FAQ which you may find helpful:

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by FAQtory

mzebrak commented 2 days ago

Found the casue of this issue. Replacing self.run_worker with self.app.run_worker seems to work. Still it is weird it just crashed with NO message in the first case.