From a0b75166c5a924968de1d87448e71d0add13587f Mon Sep 17 00:00:00 2001 From: Random User Date: Fri, 29 Sep 2023 15:26:50 +0200 Subject: [PATCH 1/5] LinuxDriver: Exit if thread dies If `run_input_thread()` dies, the whole application keeps running but becomes unresponsive. It has to be killed by the user, leaving the terminal in an unusable state. This commit terminates the application instead and prints a traceback. --- src/textual/drivers/linux_driver.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index c13512e8ab..8cea10fd4b 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any import rich.repr +import rich.traceback from .. import events, log from .._xterm_parser import XTermParser @@ -265,6 +266,13 @@ def more_data() -> bool: for event in feed(unicode_data): self.process_event(event) except Exception as error: - log(error) + self._app.exit( + return_code=1, + message=rich.traceback.Traceback.from_exception( + exc_type=type(error), + exc_value=error, + traceback=error.__traceback__, + ), + ) finally: selector.close() From 054b9f0f386dacd1e73de1af0c7770216b42a827 Mon Sep 17 00:00:00 2001 From: Random User Date: Fri, 6 Oct 2023 11:39:11 +0200 Subject: [PATCH 2/5] LinuxDriver: Handle any exception from input thread --- src/textual/drivers/linux_driver.py | 45 ++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 8cea10fd4b..0c8dfd4316 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -9,7 +9,7 @@ import tty from codecs import getincrementaldecoder from threading import Event, Thread -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable import rich.repr import rich.traceback @@ -24,6 +24,25 @@ from ..app import App +class ExceptionHandlingThread(Thread): + """ + Thread that passes any exception from `target` to `exception_handler` + + WARNING: `exception_handler` is called inside the thread. You can + schedule calls via `App.call_later`. + """ + + def __init__(self, *args, exception_handler, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._exception_handler: Callable = exception_handler + + def run(self) -> None: + try: + super().run() + except BaseException as error: + self._exception_handler(error) + + @rich.repr.auto(angular=True) class LinuxDriver(Driver): """Powers display and input for Linux / MacOS""" @@ -165,7 +184,10 @@ def on_terminal_resize(signum, stack) -> None: self.write("\x1b[?25l") # Hide cursor self.write("\033[?1003h\n") self.flush() - self._key_thread = Thread(target=self.run_input_thread) + self._key_thread = ExceptionHandlingThread( + target=self.run_input_thread, + exception_handler=self._handle_input_thread_exception, + ) send_size_event() self._key_thread.start() self._request_terminal_sync_mode_support() @@ -265,14 +287,15 @@ def more_data() -> bool: unicode_data = decode(read(fileno, 1024)) for event in feed(unicode_data): self.process_event(event) - except Exception as error: - self._app.exit( - return_code=1, - message=rich.traceback.Traceback.from_exception( - exc_type=type(error), - exc_value=error, - traceback=error.__traceback__, - ), - ) finally: selector.close() + + def _handle_input_thread_exception(self, error): + self._app.call_later( + self._app.panic, + rich.traceback.Traceback.from_exception( + exc_type=type(error), + exc_value=error, + traceback=error.__traceback__, + ), + ) From 646dcad56715aff1f09a5617f57d5efb12bdd0e3 Mon Sep 17 00:00:00 2001 From: Random User Date: Fri, 6 Oct 2023 11:46:20 +0200 Subject: [PATCH 3/5] LinuxDriver: Simpler traceback construction in input thread handler --- src/textual/drivers/linux_driver.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 0c8dfd4316..e7ae8da217 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -293,9 +293,5 @@ def more_data() -> bool: def _handle_input_thread_exception(self, error): self._app.call_later( self._app.panic, - rich.traceback.Traceback.from_exception( - exc_type=type(error), - exc_value=error, - traceback=error.__traceback__, - ), + rich.traceback.Traceback(), ) From dda3bb3972eee4c4b905043ab6f0a1abd205d2c9 Mon Sep 17 00:00:00 2001 From: Random User Date: Tue, 10 Oct 2023 18:47:32 +0200 Subject: [PATCH 4/5] LinuxDriver: Catch exception from run_input_thread() in _run_input_thread() --- src/textual/drivers/linux_driver.py | 43 ++++++++++------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index e7ae8da217..7d597f4a11 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -24,25 +24,6 @@ from ..app import App -class ExceptionHandlingThread(Thread): - """ - Thread that passes any exception from `target` to `exception_handler` - - WARNING: `exception_handler` is called inside the thread. You can - schedule calls via `App.call_later`. - """ - - def __init__(self, *args, exception_handler, **kwargs) -> None: - super().__init__(*args, **kwargs) - self._exception_handler: Callable = exception_handler - - def run(self) -> None: - try: - super().run() - except BaseException as error: - self._exception_handler(error) - - @rich.repr.auto(angular=True) class LinuxDriver(Driver): """Powers display and input for Linux / MacOS""" @@ -184,10 +165,7 @@ def on_terminal_resize(signum, stack) -> None: self.write("\x1b[?25l") # Hide cursor self.write("\033[?1003h\n") self.flush() - self._key_thread = ExceptionHandlingThread( - target=self.run_input_thread, - exception_handler=self._handle_input_thread_exception, - ) + self._key_thread = Thread(target=self._run_input_thread) send_size_event() self._key_thread.start() self._request_terminal_sync_mode_support() @@ -256,6 +234,19 @@ def close(self) -> None: if self._writer_thread is not None: self._writer_thread.stop() + def _run_input_thread(self) -> None: + """ + Key thread target that wraps run_input_thread() to die gracefully if it raises + an exception + """ + try: + self.run_input_thread() + except BaseException as error: + self._app.call_later( + self._app.panic, + rich.traceback.Traceback(), + ) + def run_input_thread(self) -> None: """Wait for input and dispatch events.""" selector = selectors.DefaultSelector() @@ -289,9 +280,3 @@ def more_data() -> bool: self.process_event(event) finally: selector.close() - - def _handle_input_thread_exception(self, error): - self._app.call_later( - self._app.panic, - rich.traceback.Traceback(), - ) From afcd5de7fd4e62fa2819041d6108fb306c95b0da Mon Sep 17 00:00:00 2001 From: Random User Date: Wed, 11 Oct 2023 11:33:08 +0200 Subject: [PATCH 5/5] LinuxDriver: Remove unneeded import: Callable --- src/textual/drivers/linux_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 7d597f4a11..c64f9dcdde 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -9,7 +9,7 @@ import tty from codecs import getincrementaldecoder from threading import Event, Thread -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any import rich.repr import rich.traceback