diff --git a/src/run_tribler.py b/src/run_tribler.py index c78e607f6ae..bdb5b321e8d 100644 --- a/src/run_tribler.py +++ b/src/run_tribler.py @@ -10,6 +10,7 @@ from PyQt5.QtCore import QSettings +from tribler_common.logger import load_logger_config from tribler_common.process_checker import ProcessChecker from tribler_common.sentry_reporter.sentry_reporter import SentryReporter, SentryStrategy from tribler_common.sentry_reporter.sentry_scrubber import SentryScrubber @@ -83,12 +84,11 @@ def init_boot_logger(): # Check whether we need to start the core or the user interface if parsed_args.mode in (RunMode.CORE_ONLY, RunMode.GUI_TEST_MODE): - from tribler_core.check_os import should_kill_other_tribler_instances + from tribler_core.check_os import should_kill_other_tribler_instances should_kill_other_tribler_instances(root_state_dir) logger.info('Running in "core" mode') - import tribler_core - tribler_core.load_logger_config(root_state_dir) + load_logger_config('tribler-core', root_state_dir) # Check if we are already running a Tribler instance process_checker = ProcessChecker(root_state_dir) @@ -117,7 +117,7 @@ def init_boot_logger(): os.environ["QT_MAC_WANTS_LAYER"] = "1" # Set up logging - tribler_gui.load_logger_config(root_state_dir) + load_logger_config('tribler-gui', root_state_dir) from tribler_core.check_os import ( check_and_enable_code_tracing, diff --git a/src/tribler-common/tribler_common/logger.py b/src/tribler-common/tribler_common/logger.py new file mode 100644 index 00000000000..3fd86b12a53 --- /dev/null +++ b/src/tribler-common/tribler_common/logger.py @@ -0,0 +1,67 @@ +import logging +import logging.config +import sys +from pathlib import Path + +import yaml + +LOG_CONFIG_FILENAME = 'logger.yaml' + + +logger = logging.getLogger(__name__) + + +# note: this class is used by src/tribler-common/tribler_common/logger.yaml +class StdoutFilter(logging.Filter): + def filter(self, record): + return record.levelno < logging.ERROR + + +def load_logger_config(app_mode, log_dir): + """ + Loads tribler-gui module logger configuration. Note that this function should be called explicitly to + enable GUI logs dump to a file in the log directory (default: inside state directory). + """ + logger_config_path = get_logger_config_path() + setup_logging(app_mode, Path(log_dir), logger_config_path) + + +def get_logger_config_path(): + if not hasattr(sys, '_MEIPASS'): + dirname = Path(__file__).absolute().parent + else: + dirname = Path(getattr(sys, '_MEIPASS'), "tribler_source", "tribler_common") + return dirname / LOG_CONFIG_FILENAME + + +def setup_logging(app_mode, log_dir: Path, config_path: Path): + """ + Setup logging configuration with the given YAML file. + """ + logger.info(f'Load logger config: app_mode={app_mode}, config_path={config_path}, dir={log_dir}') + if not config_path.exists(): + print(f'Logger config not found in {config_path}. Using default configs.', file=sys.stderr) + logging.basicConfig(level=logging.INFO, stream=sys.stdout) + return + + try: + # Update the log file paths in the config + module_info_log_file = log_dir.joinpath(f"{app_mode}-info.log") + module_error_log_file = log_dir.joinpath(f"{app_mode}-error.log") + + with config_path.open() as f: + config_text = f.read() + + config_text = config_text.replace('TRIBLER_INFO_LOG_FILE', str(module_info_log_file)) + config_text = config_text.replace('TRIBLER_ERROR_LOG_FILE', str(module_error_log_file)) + + # Create log directory if it does not exist + if not log_dir.exists(): + log_dir.mkdir(parents=True) + + config = yaml.safe_load(config_text) + logging.config.dictConfig(config) + logger.info(f'Config loaded for app_mode={app_mode}') + except Exception as e: # pylint: disable=broad-except + print('Error in loading logger config. Using default configs. Error:', e, file=sys.stderr) + logging.basicConfig(level=logging.INFO, stream=sys.stdout) diff --git a/src/tribler-gui/tribler_gui/logger.yaml b/src/tribler-common/tribler_common/logger.yaml similarity index 70% rename from src/tribler-gui/tribler_gui/logger.yaml rename to src/tribler-common/tribler_common/logger.yaml index a2f133029af..a6af5b1841b 100644 --- a/src/tribler-gui/tribler_gui/logger.yaml +++ b/src/tribler-common/tribler_common/logger.yaml @@ -1,13 +1,10 @@ -# File: tribler_gui/logger.yaml -# Look to tribler_common/logger/config.yaml for description and example configuration. +# File: tribler_core/logger.yaml version: 1 disable_existing_loggers: false filters: - info_filter: - () : tribler_common.logger.InfoFilter - error_filter: - (): tribler_common.logger.ErrorFilter + stdout_filter: + () : tribler_common.logger.StdoutFilter # Logging formatter formatters: @@ -48,14 +45,14 @@ handlers: target: error_file_handler capacity: 1024 - console: + stdout_handler: class: logging.StreamHandler - level: DEBUG + level: INFO formatter: standard - filters: [info_filter] + filters: [stdout_filter] stream: ext://sys.stdout - error_console: + stderr_handler: class: logging.StreamHandler level: ERROR formatter: error @@ -64,12 +61,11 @@ handlers: # Root Logger Configuration root: level: NOTSET - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: yes + handlers: [stdout_handler, stderr_handler, info_memory_handler, error_memory_handler] -# Module level configuration -loggers: - TriblerGUI: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no +# Module level configuration: +# The following is an example of how you can reduce the verbosity of some specific loggers: +# +# loggers: +# TriblerTunnelCommunity: +# level: WARNING diff --git a/src/tribler-common/tribler_common/logger/__init__.py b/src/tribler-common/tribler_common/logger/__init__.py deleted file mode 100644 index 6ddca47eddf..00000000000 --- a/src/tribler-common/tribler_common/logger/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -import logging.config -import os -import sys -from pathlib import Path - -import yaml - -LOG_CONFIG_FILENAME = 'logger.yaml' - - -# note: this class is used by src/tribler-common/tribler_common/logger/config.yaml -class InfoFilter(logging.Filter): - def filter(self, rec): - return rec.levelno == logging.INFO - - -# note: this class is used by src/tribler-common/tribler_common/logger/config.yaml -class ErrorFilter(logging.Filter): - def filter(self, rec): - return rec.levelno == logging.ERROR - - -def setup_logging(config_path=LOG_CONFIG_FILENAME, module='core', log_dir='LOG_DIR'): - """ - Setup logging configuration with the given YAML file. - """ - if os.path.exists(config_path): - with open(config_path) as f: - try: - # Update the log file paths in the config - config_text = f.read() - module_info_log_file = Path(log_dir).joinpath(f"{module}-info.log") - module_error_log_file = Path(log_dir).joinpath(f"{module}-error.log") - upgrader_log_file = Path(log_dir).joinpath("upgrade.log") - config_text = config_text.replace('TRIBLER_INFO_LOG_FILE', str(module_info_log_file)) - config_text = config_text.replace('TRIBLER_ERROR_LOG_FILE', str(module_error_log_file)) - config_text = config_text.replace('TRIBLER_UPGRADER_LOG_FILE', str(upgrader_log_file)) - - # Create log directory if it does not exist - if not Path(log_dir).exists(): - Path(log_dir).mkdir(parents=True) - - config = yaml.safe_load(config_text) - logging.config.dictConfig(config) - except Exception as e: - print('Error in loading logger config. Using default configs. Error:', e) - logging.basicConfig(level=logging.INFO, stream=sys.stdout) - else: - print(f'Logger config not found in {config_path}. Using default configs.') - logging.basicConfig(level=logging.INFO, stream=sys.stdout) diff --git a/src/tribler-common/tribler_common/logger/config.yaml b/src/tribler-common/tribler_common/logger/config.yaml deleted file mode 100644 index 47a6dffdf26..00000000000 --- a/src/tribler-common/tribler_common/logger/config.yaml +++ /dev/null @@ -1,103 +0,0 @@ -# File: tribler_common/logger/config.yaml - - -# Default Configuration -version: 1 - -# Disable previously created loggers and update the root logger at the instant -#the configuration file is fed. -disable_existing_loggers: true - -# Refer filter block for detailed explanation -filters: - # These are callable modules, where we define class for a filter, upon - #execution an object for the class will be created by log manager - # Format: - # filter_name: - # () : filter class path - info_filter: - () : tribler_common.logger.InfoFilter - error_filter: - (): tribler_common.logger.ErrorFilter - -# Logging formatter definition -# For more details on format types, -# visit - 'https://docs.python.org/3/library/logging.html#logrecord-attributes -formatters: - # Format: - # formatter_name: - # format: "fmt_specified using pre-defined variables" - standard: - format: "%(asctime)s - %(levelname)s - %(name)s(%(lineno)d) - %(message)s" - error: - format: "%(asctime)s - %(levelname)s %(name)s.%(funcName)s(): %(message)s" - -# Logging handlers -# Console and Error Console belongs to StreamHandler whereas info_file_handler belongs to Rotating File Handler -# For a list of pre-defined handlers, visit - 'https://docs.python.org/3/library/logging.handlers.html#module-logging.handlers' -handlers: - # Format: - # handler_name: - # handler_attributes: attribute values - info_file_handler: - # Class Attribute - Define FileHandler, StreamHandler among other handlers - class: logging.handlers.RotatingFileHandler - # Handler Level - level: INFO - # Custom Format defined in formatter block - formatter: standard - # File Name - filename: /tmp/info.log - # Max store value - 10 MB - maxBytes: 10485760 - # Backup count - Rollover attribute - backupCount: 5 - # Log format encoding - encoding: utf8 - - error_file_handler: - # Class Attribute - Define FileHandler, StreamHandler among other handlers - class: logging.handlers.RotatingFileHandler - # Handler Level - level: ERROR - # Custom Format defined in formatter block - formatter: error - # File Name - filename: /tmp/error.log - # Max store value - 10 MB - maxBytes: 10485760 - # Backup count - Rollover attribute - backupCount: 5 - # Log format encoding - encoding: utf8 - - console: - class: logging.StreamHandler - level: DEBUG - formatter: standard - filters: [info_filter] - stream: ext://sys.stdout - - error_console: - class: logging.StreamHandler - level: ERROR - formatter: error - stream: ext://sys.stderr - - -# Root Logger Configuration -root: - # Logger Level - Set to NOTSET if you have child loggers with pre-defined levels - level: NOTSET - # Attach handlers for Root Logger - handlers: [console, error_console, info_file_handler] - # Stop propogation from child to parent in Logging hierarchy - propogate: no - - -# Module level configuration -loggers: - asyncio: - level: WARNING - handlers: [info_file_handler, error_file_handler] - propogate: no diff --git a/src/tribler-common/tribler_common/tests/test_logger.py b/src/tribler-common/tribler_common/tests/test_logger.py new file mode 100644 index 00000000000..c25cc8e8edd --- /dev/null +++ b/src/tribler-common/tribler_common/tests/test_logger.py @@ -0,0 +1,85 @@ +import logging +from unittest.mock import MagicMock, Mock, call, patch + +from tribler_common.logger import get_logger_config_path, setup_logging + + +@patch('tribler_common.logger.__file__', '/a/b/c/logger.py') +def test_get_logger_config_path(): + config_path = get_logger_config_path() + # take the last part of the path to ignore a drive name on Windows + assert config_path.parts[-4:] == ('a', 'b', 'c', 'logger.yaml') + + with patch('sys._MEIPASS', '/x/y/z/', create=True): + config_path = get_logger_config_path() + assert config_path.parts[-6:] == ('x', 'y', 'z', 'tribler_source', 'tribler_common', 'logger.yaml') + + +@patch('tribler_common.logger.logger') +@patch('sys.stdout') +@patch('sys.stderr') +@patch('builtins.print') +@patch('logging.basicConfig') +def test_setup_logging_no_config(basic_config: Mock, print_: Mock, stderr: Mock, stdout: Mock, logger: Mock): + config_path = MagicMock() + config_path.exists.return_value = False + config_path.__str__.return_value = '' + + setup_logging('', '', config_path) + + logger.info.assert_called_once_with("Load logger config: app_mode=, " + "config_path=, dir=") + print_.assert_called_once_with("Logger config not found in . Using default configs.", file=stderr) + basic_config.assert_called_once_with(level=logging.INFO, stream=stdout) + + +@patch('yaml.safe_load') +@patch('logging.config.dictConfig') +@patch('tribler_common.logger.logger') +def test_setup_logging(logger: Mock, dict_config: Mock, yaml_safe_load: Mock): + log_dir = MagicMock() + log_dir.__str__.return_value = '' + log_dir.exists.return_value = False + + config_path = MagicMock() + config_path.__str__.return_value = '' + config_path.exists.return_value = True + config_path.open().__enter__().read().replace().replace.return_value = '' + + yaml_safe_load.return_value = '' + + setup_logging('', log_dir, config_path) + + log_dir.mkdir.assert_called_once_with(parents=True) + + yaml_safe_load.assert_called_once_with('') + dict_config.assert_called_once_with('') + assert logger.info.call_count == 2 + logger.info.assert_has_calls([ + call('Load logger config: app_mode=, config_path=, dir='), + call("Config loaded for app_mode=") + ]) + +@patch('tribler_common.logger.logger') +@patch('sys.stdout') +@patch('sys.stderr') +@patch('builtins.print') +@patch('logging.basicConfig') +def test_setup_logging_exception(basic_config: Mock, print_: Mock, stderr: Mock, stdout: Mock, logger: Mock): + error = ZeroDivisionError() + + log_dir = MagicMock() + log_dir.__str__.return_value = '' + log_dir.exists.return_value = True + log_dir.joinpath.side_effect = error + + config_path = MagicMock() + config_path.__str__.return_value = '' + config_path.exists.return_value = True + + setup_logging('', log_dir, config_path) + + logger.info.assert_called_once_with("Load logger config: app_mode=, " + "config_path=, dir=") + print_.assert_called_once_with('Error in loading logger config. Using default configs. Error:', error, file=stderr) + basic_config.assert_called_once_with(level=logging.INFO, stream=stdout) diff --git a/src/tribler-core/run_tribler_upgrader.py b/src/tribler-core/run_tribler_upgrader.py index 1041694071c..66adef70c7f 100755 --- a/src/tribler-core/run_tribler_upgrader.py +++ b/src/tribler-core/run_tribler_upgrader.py @@ -1,6 +1,7 @@ import signal import sys +from tribler_common.logger import load_logger_config from tribler_common.version_manager import VersionHistory from tribler_core.components.key.key_component import KeyComponent @@ -49,7 +50,5 @@ def upgrade_interrupted(): signal.signal(signal.SIGTERM, interrupt_upgrade) _root_state_dir = Path(sys.argv[1]) - import tribler_core - - tribler_core.load_logger_config(_root_state_dir) + load_logger_config('upgrader', _root_state_dir) upgrade_state_dir(_root_state_dir, interrupt_upgrade_event=upgrade_interrupted) diff --git a/src/tribler-core/tribler_core/__init__.py b/src/tribler-core/tribler_core/__init__.py index cf8804712d3..e36d5716944 100644 --- a/src/tribler-core/tribler_core/__init__.py +++ b/src/tribler-core/tribler_core/__init__.py @@ -5,23 +5,7 @@ import sys from pathlib import Path -from tribler_common.logger import LOG_CONFIG_FILENAME, setup_logging - dir_path = Path(__file__).parent.parent.parent # Make sure IPv8 can be imported sys.path.insert(0, os.path.join(dir_path, "pyipv8")) - - -def load_logger_config(log_dir): - """ - Loads tribler-core module logger configuration. Note that this function should be called explicitly to - enable Core logs dump to a file in the log directory (default: inside state directory). - """ - if hasattr(sys, '_MEIPASS'): - logger_config_path = os.path.join(getattr(sys, '_MEIPASS'), "tribler_source", "tribler_core", - LOG_CONFIG_FILENAME) - else: - logger_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), LOG_CONFIG_FILENAME) - - setup_logging(config_path=logger_config_path, module='tribler-core', log_dir=log_dir) diff --git a/src/tribler-core/tribler_core/components/libtorrent/download_manager/download_manager.py b/src/tribler-core/tribler_core/components/libtorrent/download_manager/download_manager.py index f3da327a782..ce85c1bd002 100644 --- a/src/tribler-core/tribler_core/components/libtorrent/download_manager/download_manager.py +++ b/src/tribler-core/tribler_core/components/libtorrent/download_manager/download_manager.py @@ -574,7 +574,13 @@ def start_download(self, torrent_file=None, tdef=None, config=None, checkpoint_d @task async def start_handle(self, download, atp): - self._logger.info(f"Start handle. Download: {download}. Atp: {atp}") + atp_resume_data_skipped = atp.copy() + resume_data = atp.get('resume_data') + if resume_data: + atp_resume_data_skipped['resume_data'] = '' + self._logger.info(f"Start handle. Download: {download}. Atp: {atp_resume_data_skipped}") + if resume_data: + self._logger.debug(f"Download resume data: {atp['resume_data']}") ltsession = self.get_session(download.config.get_hops()) infohash = download.get_def().get_infohash() diff --git a/src/tribler-core/tribler_core/logger.yaml b/src/tribler-core/tribler_core/logger.yaml deleted file mode 100644 index 6aa79db3221..00000000000 --- a/src/tribler-core/tribler_core/logger.yaml +++ /dev/null @@ -1,164 +0,0 @@ -# File: tribler_core/logger.yaml -# Look to tribler_common/logger/config.yaml for description and example configuration. - -version: 1 -disable_existing_loggers: false -filters: - info_filter: - () : tribler_common.logger.InfoFilter - error_filter: - (): tribler_common.logger.ErrorFilter - -# Logging formatter -formatters: - standard: - format: "[PID:%(process)d] %(asctime)s - %(levelname)s - %(name)s(%(lineno)d) - %(message)s" - error: - format: "[PID:%(process)d] %(asctime)s - %(levelname)s <%(module)s:%(lineno)d> %(name)s.%(funcName)s(): %(message)s" - -# Logging handlers -handlers: - info_file_handler: - class: logging.handlers.RotatingFileHandler - level: INFO - formatter: standard - filename: TRIBLER_INFO_LOG_FILE - maxBytes: 10485760 - backupCount: 5 - encoding: utf8 - - upgrade_file_handler: - class: logging.handlers.RotatingFileHandler - level: INFO - filename: TRIBLER_UPGRADER_LOG_FILE - formatter: standard - maxBytes: 10485760 - - info_memory_handler: - class: logging.handlers.MemoryHandler - level: INFO - target: info_file_handler - capacity: 1024 - - error_file_handler: - class: logging.handlers.RotatingFileHandler - level: ERROR - formatter: error - filename: TRIBLER_ERROR_LOG_FILE - maxBytes: 10485760 - backupCount: 5 - encoding: utf8 - - error_memory_handler: - class: logging.handlers.MemoryHandler - level: ERROR - target: error_file_handler - capacity: 1024 - - console: - class: logging.StreamHandler - level: DEBUG - formatter: standard - filters: [info_filter] - stream: ext://sys.stdout - - error_console: - class: logging.StreamHandler - level: ERROR - formatter: error - stream: ext://sys.stderr - -# Root Logger Configuration -root: - level: NOTSET - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: yes - -# Module level configuration -loggers: - Session: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - asyncio: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - TriblerGUI: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - RequestCache: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - LibtorrentMgr: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - LibtorrentDownloadImpl: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - DiscoveryCommunity: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - HiddenTunnelCommunity: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - TriblerTunnelCommunity: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - PopularityCommunity: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - DHTDiscoveryCommunity: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - TunnelDispatcher: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - XXXFilter: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - ResourceMonitor: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - MetadataStore: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - GigaChannelManager: - level: INFO - handlers: [console, error_console, info_memory_handler, error_memory_handler] - propagate: no - - TriblerUpgrader: - level: INFO - handlers: [console, upgrade_file_handler] - propagate: no - - diff --git a/src/tribler-core/tribler_core/tests/test_start_core.py b/src/tribler-core/tribler_core/tests/test_start_core.py index 82bad72cd8c..d13bfdc666e 100644 --- a/src/tribler-core/tribler_core/tests/test_start_core.py +++ b/src/tribler-core/tribler_core/tests/test_start_core.py @@ -7,7 +7,7 @@ # fmt: off -@patch('tribler_core.load_logger_config', new=MagicMock()) +@patch('tribler_common.logger.load_logger_config', new=MagicMock()) @patch('tribler_core.start_core.set_process_priority', new=MagicMock()) @patch('tribler_core.start_core.check_and_enable_code_tracing', new=MagicMock()) @patch('asyncio.get_event_loop', new=MagicMock()) diff --git a/src/tribler-core/tribler_core/tests/test_utilities.py b/src/tribler-core/tribler_core/tests/test_utilities.py index 443ea76705a..333f81cfbcf 100644 --- a/src/tribler-core/tribler_core/tests/test_utilities.py +++ b/src/tribler-core/tribler_core/tests/test_utilities.py @@ -1,6 +1,7 @@ import logging -from tribler_core import load_logger_config +from tribler_common.logger import load_logger_config + from tribler_core.utilities.tracker_utils import add_url_params from tribler_core.utilities.utilities import parse_magnetlink @@ -42,5 +43,5 @@ def test_load_logger(tmpdir): Test loading the Tribler logger configuration. """ logger_count = len(logging.root.manager.loggerDict) - load_logger_config(tmpdir) + load_logger_config('test', tmpdir) assert len(logging.root.manager.loggerDict) >= logger_count diff --git a/src/tribler-gui/tribler_gui/__init__.py b/src/tribler-gui/tribler_gui/__init__.py index 2743ca1f0eb..2f4c4ab6f5c 100644 --- a/src/tribler-gui/tribler_gui/__init__.py +++ b/src/tribler-gui/tribler_gui/__init__.py @@ -1,26 +1,3 @@ """ This package contains the code for the GUI, written in pyQt. """ -import logging -import os -import sys - -from tribler_common.logger import LOG_CONFIG_FILENAME, setup_logging - -logger = logging.getLogger(__name__) - - -def load_logger_config(log_dir): - """ - Loads tribler-gui module logger configuration. Note that this function should be called explicitly to - enable GUI logs dump to a file in the log directory (default: inside state directory). - """ - logger.info(f'Load logger config: {log_dir}') - if hasattr(sys, '_MEIPASS'): - logger_config_path = os.path.join( - getattr(sys, '_MEIPASS'), "tribler_source", "tribler_gui", LOG_CONFIG_FILENAME - ) - else: - logger_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), LOG_CONFIG_FILENAME) - - setup_logging(config_path=logger_config_path, module='tribler-gui', log_dir=log_dir) diff --git a/src/tribler-gui/tribler_gui/core_manager.py b/src/tribler-gui/tribler_gui/core_manager.py index 796d4ac5758..a0a03214905 100644 --- a/src/tribler-gui/tribler_gui/core_manager.py +++ b/src/tribler-gui/tribler_gui/core_manager.py @@ -34,18 +34,29 @@ def __init__(self, root_state_dir, api_port, api_key, error_handler): self.should_stop_on_shutdown = False self.use_existing_core = True self.is_core_running = False - self.last_core_output: str = '' + self.last_core_stdout_output: str = '' + self.last_core_stderr_output: str = '' connect(self.events_manager.tribler_started, self._set_core_running) def _set_core_running(self, _): self.is_core_running = True - def on_core_read_ready(self): - raw_output = bytes(self.core_process.readAll()) - self.last_core_output = raw_output.decode("utf-8").strip() + def on_core_stdout_read_ready(self): + raw_output = bytes(self.core_process.readAllStandardOutput()) + self.last_core_stdout_output = raw_output.decode("utf-8").strip() try: - print(f'\t{self.last_core_output}') # print core output # noqa: T001 + print(self.last_core_stdout_output) # print core output # noqa: T001 + except OSError: + # Possible reason - cannot write to stdout as it was already closed during the application shutdown + if not self.shutting_down: + raise + + def on_core_stderr_read_ready(self): + raw_output = bytes(self.core_process.readAllStandardError()) + self.last_core_stderr_output = raw_output.decode("utf-8").strip() + try: + print(self.last_core_stderr_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 if not self.shutting_down: @@ -61,7 +72,7 @@ def on_core_finished(self, exit_code, exit_status): exception_message = ( f"The Tribler core has unexpectedly finished with exit code {exit_code} and status: {exit_status}!\n" - f"Last core output: \n {self.last_core_output}" + f"Last core output: \n {self.last_core_stderr_output or self.last_core_stdout_output}" ) raise CoreCrashedError(exception_message) @@ -101,9 +112,9 @@ def start_tribler_core(self, core_args=None, core_env=None): self.core_process = QProcess() self.core_process.setProcessEnvironment(core_env) - self.core_process.setReadChannel(QProcess.StandardOutput) - self.core_process.setProcessChannelMode(QProcess.MergedChannels) - connect(self.core_process.readyRead, self.on_core_read_ready) + self.core_process.setProcessChannelMode(QProcess.SeparateChannels) + connect(self.core_process.readyReadStandardOutput, self.on_core_stdout_read_ready) + connect(self.core_process.readyReadStandardError, self.on_core_stderr_read_ready) connect(self.core_process.finished, self.on_core_finished) self.core_process.start(sys.executable, core_args) diff --git a/src/tribler-gui/tribler_gui/error_handler.py b/src/tribler-gui/tribler_gui/error_handler.py index 5a59c65fcb5..615d64e06c5 100644 --- a/src/tribler-gui/tribler_gui/error_handler.py +++ b/src/tribler-gui/tribler_gui/error_handler.py @@ -37,7 +37,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_output + text = text + self.tribler_window.core_manager.last_core_stderr_output self._stop_tribler(text) self._logger.error(text) diff --git a/src/tribler-gui/tribler_gui/tests/test_core_manager.py b/src/tribler-gui/tribler_gui/tests/test_core_manager.py index 2c245643506..ea3b43d8af0 100644 --- a/src/tribler-gui/tribler_gui/tests/test_core_manager.py +++ b/src/tribler-gui/tribler_gui/tests/test_core_manager.py @@ -34,27 +34,40 @@ async def test_on_core_finished_raises_error(): @patch('tribler_gui.core_manager.print') @patch('tribler_gui.core_manager.EventRequestManager', new=MagicMock()) -async def test_on_core_read_ready(mocked_print: MagicMock): - # test that method `on_core_read_ready` converts byte output to a string and prints it +async def test_on_core_stdout_read_ready(mocked_print: MagicMock): + # test that method `on_core_stdout_read_ready` converts byte output to a string and prints it core_manager = CoreManager(MagicMock(), MagicMock(), MagicMock(), MagicMock()) - core_manager.core_process = MagicMock(readAll=MagicMock(return_value=b'binary string')) + core_manager.core_process = MagicMock(readAllStandardOutput=MagicMock(return_value=b'binary string')) + core_manager.on_core_stdout_read_ready() + mocked_print.assert_called_with('binary string') - core_manager.on_core_read_ready() - mocked_print.assert_called_with('\tbinary string') +@patch('tribler_gui.core_manager.print') +@patch('tribler_gui.core_manager.EventRequestManager', new=MagicMock()) +@patch('sys.stderr') +async def test_on_core_stderr_read_ready(mocked_stderr, mocked_print: MagicMock): + # test that method `on_core_stdout_read_ready` converts byte output to a string and prints it + core_manager = CoreManager(MagicMock(), MagicMock(), MagicMock(), MagicMock()) + core_manager.core_process = MagicMock(readAllStandardError=MagicMock(return_value=b'binary string')) + core_manager.on_core_stderr_read_ready() + mocked_print.assert_called_with('binary string', file=mocked_stderr) @patch('tribler_gui.core_manager.EventRequestManager', new=MagicMock()) @patch('builtins.print', MagicMock(side_effect=OSError())) -def test_on_core_read_ready_os_error(): +def test_on_core_stdout_stderr_read_ready_os_error(): # test that OSError on writing to stdout is suppressed during shutting down core_manager = CoreManager(MagicMock(), MagicMock(), MagicMock(), MagicMock()) core_manager.core_process = MagicMock(read_all=MagicMock(return_value='')) with pytest.raises(OSError): - core_manager.on_core_read_ready() + core_manager.on_core_stdout_read_ready() + + with pytest.raises(OSError): + core_manager.on_core_stderr_read_ready() core_manager.shutting_down = True # no exception during shutting down - core_manager.on_core_read_ready() + core_manager.on_core_stdout_read_ready() + core_manager.on_core_stderr_read_ready() diff --git a/src/tribler-gui/tribler_gui/utilities.py b/src/tribler-gui/tribler_gui/utilities.py index 0e849775bfb..432d73a1941 100644 --- a/src/tribler-gui/tribler_gui/utilities.py +++ b/src/tribler-gui/tribler_gui/utilities.py @@ -17,6 +17,8 @@ import tribler_gui from tribler_gui.defs import HEALTH_DEAD, HEALTH_GOOD, HEALTH_MOOT, HEALTH_UNCHECKED +logger = logging.getLogger(__name__) + NUM_VOTES_BARS = 8 @@ -426,10 +428,8 @@ def get_translator(language=None): # which makes QTranslator use the system language (e.g. the language the OS was installed in), # instead of the user-display language the user installed later. locale = QLocale(language) if language is not None else QLocale(system_locale.uiLanguages()[0]) - tribler_gui.logger.info("Available Tribler translations %s", AVAILABLE_TRANSLATIONS) - tribler_gui.logger.info( - "System language: %s, Tribler language: %s", system_locale.uiLanguages(), locale.uiLanguages() - ) + logger.info("Available Tribler translations %s", AVAILABLE_TRANSLATIONS) + logger.info("System language: %s, Tribler language: %s", system_locale.uiLanguages(), locale.uiLanguages()) translator = QTranslator() filename = "" translator.load(locale, filename, directory=TRANSLATIONS_DIR) diff --git a/tribler.spec b/tribler.spec index 7c71479fbae..d7c483e6a83 100644 --- a/tribler.spec +++ b/tribler.spec @@ -42,6 +42,7 @@ data_to_copy = [ (os.path.join(src_dir, "tribler-gui", "tribler_gui", "qt_resources"), 'qt_resources'), (os.path.join(src_dir, "tribler-gui", "tribler_gui", "images"), 'images'), (os.path.join(src_dir, "tribler-gui", "tribler_gui", "i18n"), 'i18n'), + (os.path.join(src_dir, "tribler-common", "tribler_common"), 'tribler_source/tribler_common'), (os.path.join(src_dir, "tribler-core", "tribler_core"), 'tribler_source/tribler_core'), (os.path.join(src_dir, "tribler-gui", "tribler_gui"), 'tribler_source/tribler_gui'), (os.path.join(root_dir, "build", "win", "resources"), 'tribler_source/resources'),