textual icon indicating copy to clipboard operation
textual copied to clipboard

Signal being ignored / signal handler not being invoked

Open MorningLightMountain713 opened this issue 1 year ago • 5 comments

I'm controlling a textual app via systemd. When I stop the app, I want some cleanup to run. Systemd is sending a SIGTERM. How can I catch that in the textual app? It seems to ignore it. When I hit ctrl +c, the on_unmount action is run (which runs the shutdown method), but not when systemd terminates the app.

I also tried adding a signal handler like so:

    def on_mount(self) -> None:
        loop = asyncio.get_running_loop()
        loop.add_signal_handler(
            signal.SIGTERM, lambda: asyncio.create_task(self.shutdown())
        )

Here is the shutdown method:

    async def shutdown(self, exit: bool = True) -> None:
        if self.webserver_port:
            await self.update_firewall("remove", self.webserver_port)
            self.webserver_port = 0

        if exit:
            self.exit()

I tried testing this manually by sending kill -s SIGTERM pid from the terminal, but that doesn't do anything. At the least, even without a signal handler, I would expect textual to exit.

it seems textual is ignoring signals, and overriding any handlers that get added? Is there a more "textual" way of handling signals?

Thanks.

Textual Diagnostics

Versions

Name Value
Textual 0.82.0
Rich 13.9.2

Python

Name Value
Version 3.12.3
Implementation CPython
Compiler GCC 13.2.0
Executable /usr/lib/flux_config/.venv/bin/python

Operating System

Name Value
System Linux
Release 6.8.0-45-generic
Version #45-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug 30 12:02:04 UTC 2024

Terminal

Name Value
Terminal Application Unknown
TERM xterm-256color
COLORTERM Not set
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=131, height=41
legacy_windows False
min_width 1
max_width 131
is_terminal True
encoding utf-8
max_height 41
justify None
overflow None
no_wrap False
highlight None
markup None
height None

MorningLightMountain713 avatar Oct 17 '24 09:10 MorningLightMountain713

Thank you for your issue. Give us a little time to review it.

PS. You might want to check the FAQ if you haven't done so already.

This is an automated reply, generated by FAQtory

github-actions[bot] avatar Oct 17 '24 09:10 github-actions[bot]

Maybe this helps:

import pathlib
import signal
import time

from textual.app import App, ComposeResult
from textual.widgets import Placeholder


class MinimalApp(App[None]):
    _signal = None

    def on_mount(self) -> None:
        signal.signal(signalnum=signal.SIGHUP, handler=self.catch_signal)
        signal.signal(signalnum=signal.SIGTERM, handler=self.catch_signal)

    def compose(self) -> ComposeResult:
        yield Placeholder("This is a minimal app.")

    def catch_signal(self, signum, frame) -> None:
        self._signal = signum
        self.exit()


app = MinimalApp()
app.run()

pathlib.Path("signal.txt").write_text(
    f"{time.monotonic()}: Received signal: {app._signal}"
)
print("Done")
print(f"Received signal: {app._signal}")

davidfokkema avatar Oct 18 '24 21:10 davidfokkema

Yeah that works, wonder why the add_signal_handler doesn't?

Thanks!

MorningLightMountain713 avatar Oct 21 '24 06:10 MorningLightMountain713

Not sure, but you create a separate task for the shutdown, and that will run besides the Textual event loop? Not sure how Textual schedules events...

davidfokkema avatar Oct 21 '24 10:10 davidfokkema

This works for SIGTERM, but I cannot get it to work for SIGINT or SIGTSTP.

Setting TEXTUAL_ALLOW_SIGNALS=1 works: https://github.com/Textualize/textual/blob/da3f608fd7dab10be67f5a4cdf18c57b37b7ca6f/src/textual/drivers/linux_driver.py#L321-L324

TEXTUAL_ALLOW_SIGNALS seems to be undocumented according to Google and Github searches. I have found this by grepping the codebase for signal.

mxmlnkn avatar Aug 10 '25 14:08 mxmlnkn