From 3bc8593a036eb4179f9fcdaa4e941aaa62ba0702 Mon Sep 17 00:00:00 2001 From: Alexander Kozlovsky Date: Tue, 23 Aug 2022 14:02:26 +0200 Subject: [PATCH 1/2] Stop Core on component's startup exception; Include the Core output to the reported GUI error --- src/tribler/core/components/base.py | 4 +++ .../core/sentry_reporter/sentry_reporter.py | 2 +- src/tribler/gui/core_manager.py | 32 ++++++++++++------- src/tribler/gui/error_handler.py | 2 +- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/tribler/core/components/base.py b/src/tribler/core/components/base.py index a9f3704fea1..5c53fff0001 100644 --- a/src/tribler/core/components/base.py +++ b/src/tribler/core/components/base.py @@ -162,6 +162,10 @@ def start(self): def _reraise_startup_exception_in_separate_task(self): async def exception_reraiser(): + e = self._startup_exception + if isinstance(e, ComponentStartupException) and e.component.tribler_should_stop_on_component_error: + self.shutdown_event.set() + # the exception should be intercepted by event loop exception handler raise self._startup_exception diff --git a/src/tribler/core/sentry_reporter/sentry_reporter.py b/src/tribler/core/sentry_reporter/sentry_reporter.py index 1b0158426e1..b5fbc90a48f 100644 --- a/src/tribler/core/sentry_reporter/sentry_reporter.py +++ b/src/tribler/core/sentry_reporter/sentry_reporter.py @@ -280,7 +280,7 @@ def event_from_exception(self, exception) -> Dict: Returns: the event that has been saved in `_before_send` method """ - self._logger.info(f"Event from exception: {exception}") + self._logger.debug(f"Event from exception: {exception}") if not exception: return {} diff --git a/src/tribler/gui/core_manager.py b/src/tribler/gui/core_manager.py index f00aac5eaf9..78a4f8a1841 100644 --- a/src/tribler/gui/core_manager.py +++ b/src/tribler/gui/core_manager.py @@ -2,6 +2,7 @@ import os import re import sys +from collections import deque from pathlib import Path from typing import Optional @@ -16,6 +17,9 @@ from tribler.gui.utilities import connect +CORE_OUTPUT_DEQUE_LENGTH = 10 + + class CoreManager(QObject): """ The CoreManager is responsible for managing the Tribler core (starting/stopping). When we are running the GUI tests, @@ -47,8 +51,8 @@ def __init__(self, root_state_dir: Path, api_port: int, api_key: str, app_manage self.should_quit_app_on_core_finished = False self.use_existing_core = True - self.last_core_stdout_output: str = '' - self.last_core_stderr_output: str = '' + self.last_core_stdout_output: deque = deque(maxlen=CORE_OUTPUT_DEQUE_LENGTH) + self.last_core_stderr_output: deque = deque(maxlen=CORE_OUTPUT_DEQUE_LENGTH) connect(self.events_manager.core_connected, self.on_core_connected) @@ -125,10 +129,11 @@ def on_core_stdout_read_ready(self): return raw_output = bytes(self.core_process.readAllStandardOutput()) - self.last_core_stdout_output = self.decode_raw_core_output(raw_output).strip() + output = self.decode_raw_core_output(raw_output).strip() + self.last_core_stdout_output.append(output) try: - print(self.last_core_stdout_output) # print core output # noqa: T001 + print(output) # print core output # noqa: T001 except OSError: # Possible reason - cannot write to stdout as it was already closed during the application shutdown pass @@ -139,10 +144,11 @@ def on_core_stderr_read_ready(self): return raw_output = bytes(self.core_process.readAllStandardError()) - self.last_core_stderr_output = self.decode_raw_core_output(raw_output).strip() + output = self.decode_raw_core_output(raw_output).strip() + self.last_core_stderr_output.append(output) try: - print(self.last_core_stderr_output, file=sys.stderr) # print core output # noqa: T001 + print(output, file=sys.stderr) # print core output # noqa: T001 except OSError: # Possible reason - cannot write to stdout as it was already closed during the application shutdown pass @@ -196,8 +202,14 @@ def kill_core_process_and_remove_the_lock_file(self): process_checker = ProcessChecker(self.root_state_dir) process_checker.remove_lock() + def get_last_core_output(self, quoted=True): + output = ''.join(self.last_core_stderr_output) or ''.join(self.last_core_stdout_output) + if quoted: + output = re.sub(r'^', '> ', output, flags=re.MULTILINE) + return output + @staticmethod - def format_error_message(exit_code: int, exit_status: int, last_core_output: str) -> str: + def format_error_message(exit_code: int, exit_status: int) -> str: message = f"The Tribler core has unexpectedly finished with exit code {exit_code} and status: {exit_status}." try: string_error = os.strerror(exit_code) @@ -205,9 +217,6 @@ def format_error_message(exit_code: int, exit_status: int, last_core_output: str # On platforms where strerror() returns NULL when given an unknown error number, ValueError is raised. string_error = 'unknown error number' message += f'\n\nError message: {string_error}' - - quoted_output = re.sub(r'^', '> ', last_core_output, flags=re.MULTILINE) - message += f"\n\nLast core output:\n{quoted_output}" return message def on_core_finished(self, exit_code, exit_status): @@ -218,8 +227,7 @@ def on_core_finished(self, exit_code, exit_status): if self.should_quit_app_on_core_finished: self.app_manager.quit_application() else: - output = self.last_core_stderr_output or self.last_core_stdout_output - error_message = self.format_error_message(exit_code, exit_status, output) + error_message = self.format_error_message(exit_code, exit_status) self._logger.warning(error_message) if not self.app_manager.quitting_app: diff --git a/src/tribler/gui/error_handler.py b/src/tribler/gui/error_handler.py index c4f177eca73..020148ec66e 100644 --- a/src/tribler/gui/error_handler.py +++ b/src/tribler/gui/error_handler.py @@ -44,7 +44,7 @@ def gui_error(self, *exc_info): is_core_exception = issubclass(info_type, CoreError) if is_core_exception: - text = text + self.tribler_window.core_manager.last_core_stderr_output + text = f'{text}\n\nLast Core output:\n{self.tribler_window.core_manager.get_last_core_output()}' self._stop_tribler(text) reported_error = ReportedError( From a83547257deedf0d79817c74988e3ed9b4e3c0e7 Mon Sep 17 00:00:00 2001 From: Alexander Kozlovsky Date: Wed, 24 Aug 2022 18:22:15 +0200 Subject: [PATCH 2/2] Return error exit code from the Core process if the Core session fails --- src/tribler/core/components/base.py | 2 ++ src/tribler/core/start_core.py | 23 +++++++++++++++++++---- src/tribler/gui/core_manager.py | 13 ++++++++----- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/tribler/core/components/base.py b/src/tribler/core/components/base.py index 5c53fff0001..1e0a13fdc0b 100644 --- a/src/tribler/core/components/base.py +++ b/src/tribler/core/components/base.py @@ -86,6 +86,7 @@ def __init__(self, config: TriblerConfig = None, components: List[Component] = ( shutdown_event: Event = None, notifier: Notifier = None, failfast: bool = True): # deepcode ignore unguarded~next~call: not necessary to catch StopIteration on infinite iterator self.id = next(Session._next_session_id) + self.exit_code = 0 self.failfast = failfast self.logger = logging.getLogger(self.__class__.__name__) self.config: TriblerConfig = config or TriblerConfig() @@ -164,6 +165,7 @@ def _reraise_startup_exception_in_separate_task(self): async def exception_reraiser(): e = self._startup_exception if isinstance(e, ComponentStartupException) and e.component.tribler_should_stop_on_component_error: + self.exit_code = 1 self.shutdown_event.set() # the exception should be intercepted by event loop exception handler diff --git a/src/tribler/core/start_core.py b/src/tribler/core/start_core.py index c91d67975ad..c22ec0ce79c 100644 --- a/src/tribler/core/start_core.py +++ b/src/tribler/core/start_core.py @@ -4,6 +4,7 @@ import os import signal import sys +from pathlib import Path from typing import List from tribler.core import notifications @@ -92,7 +93,12 @@ def components_gen(config: TriblerConfig): yield GigachannelManagerComponent() -async def core_session(config: TriblerConfig, components: List[Component]): +async def core_session(config: TriblerConfig, components: List[Component]) -> int: + """ + Async task for running a new Tribler session. + + Returns an exit code, which is non-zero if the Tribler session finished with an error. + """ session = Session(config, components, failfast=False) signal.signal(signal.SIGTERM, lambda signum, stack: session.shutdown_event.set) async with session.start() as session: @@ -108,12 +114,16 @@ async def core_session(config: TriblerConfig, components: List[Component]): session.notifier[notifications.tribler_shutdown_state]("Saving configuration...") config.write() + return session.exit_code + -def run_tribler_core_session(api_port, api_key, state_dir, gui_test_mode=False): +def run_tribler_core_session(api_port: str, api_key: str, state_dir: Path, gui_test_mode: bool = False) -> int: """ This method will start a new Tribler session. Note that there is no direct communication between the GUI process and the core: all communication is performed through the HTTP API. + + Returns an exit code value, which is non-zero if the Tribler session finished with an error. """ logger.info(f'Start tribler core. API port: "{api_port}". ' f'API key: "{api_key}". State dir: "{state_dir}". ' @@ -154,7 +164,7 @@ def run_tribler_core_session(api_port, api_key, state_dir, gui_test_mode=False): loop.set_exception_handler(exception_handler.unhandled_error_observer) try: - loop.run_until_complete(core_session(config, components=list(components_gen(config)))) + exit_code = loop.run_until_complete(core_session(config, components=list(components_gen(config)))) finally: if trace_logger: trace_logger.close() @@ -163,6 +173,8 @@ def run_tribler_core_session(api_port, api_key, state_dir, gui_test_mode=False): for handler in logging.getLogger().handlers: handler.flush() + return exit_code + def run_core(api_port, api_key, root_state_dir, parsed_args): logger.info('Running Core' + ' in gui_test_mode' if parsed_args.gui_test_mode else '') @@ -171,4 +183,7 @@ def run_core(api_port, api_key, root_state_dir, parsed_args): with single_tribler_instance(root_state_dir): version_history = VersionHistory(root_state_dir) state_dir = version_history.code_version.directory - run_tribler_core_session(api_port, api_key, state_dir, gui_test_mode=parsed_args.gui_test_mode) + exit_code = run_tribler_core_session(api_port, api_key, state_dir, gui_test_mode=parsed_args.gui_test_mode) + + if exit_code: + sys.exit(exit_code) diff --git a/src/tribler/gui/core_manager.py b/src/tribler/gui/core_manager.py index 78a4f8a1841..0778ac9025a 100644 --- a/src/tribler/gui/core_manager.py +++ b/src/tribler/gui/core_manager.py @@ -211,11 +211,14 @@ def get_last_core_output(self, quoted=True): @staticmethod def format_error_message(exit_code: int, exit_status: int) -> str: message = f"The Tribler core has unexpectedly finished with exit code {exit_code} and status: {exit_status}." - try: - string_error = os.strerror(exit_code) - except ValueError: - # On platforms where strerror() returns NULL when given an unknown error number, ValueError is raised. - string_error = 'unknown error number' + if exit_code == 1: + string_error = "Application error" + else: + try: + string_error = os.strerror(exit_code) + except ValueError: + # On platforms where strerror() returns NULL when given an unknown error number, ValueError is raised. + string_error = 'unknown error number' message += f'\n\nError message: {string_error}' return message