diff --git a/src/tribler-gui/tribler_gui/code_executor.py b/src/tribler-gui/tribler_gui/code_executor.py index 7bbe93b973e..a3afc771b94 100644 --- a/src/tribler-gui/tribler_gui/code_executor.py +++ b/src/tribler-gui/tribler_gui/code_executor.py @@ -1,13 +1,15 @@ import binascii -import code -import io import logging +import os import sys +import traceback from base64 import b64decode, b64encode +from code import InteractiveConsole +from pathlib import Path from PyQt5.QtNetwork import QTcpServer -from tribler_gui.utilities import connect +from tribler_gui.utilities import connect, take_screenshot class CodeExecutor: @@ -23,7 +25,7 @@ class CodeExecutor: Note that the socket uses the newline as separator. """ - def __init__(self, port, shell_variables={}): + def __init__(self, port, shell_variables=None): self.logger = logging.getLogger(self.__class__.__name__) self.tcp_server = QTcpServer() self.sockets = [] @@ -33,13 +35,15 @@ def __init__(self, port, shell_variables={}): else: connect(self.tcp_server.newConnection, self._on_new_connection) - self.shell = Console(locals=shell_variables) + self.shell = Console(locals=shell_variables or {}, logger=self.logger) def _on_new_connection(self): + self.logger.info("CodeExecutor has new connection") + while self.tcp_server.hasPendingConnections(): socket = self.tcp_server.nextPendingConnection() connect(socket.readyRead, self._on_socket_read_ready) - connect(socket.disconnected, lambda dc_socket=socket: self._on_socket_disconnect(dc_socket)) + connect(socket.disconnected, self._on_socket_disconnect(socket)) self.sockets.append(socket) # If Tribler has crashed, notify the other side immediately @@ -47,25 +51,27 @@ def _on_new_connection(self): self.on_crash(self.stack_trace) def run_code(self, code, task_id): - self.shell.runcode(code) - stdout = self.shell.stdout.read() - stderr = self.shell.stderr.read() + self.logger.info(f"Run code for task {task_id}") + self.logger.debug(f"Code for execution:\n{code}") - self.logger.info("Code execution with task %s finished:", task_id) - self.logger.info("Stdout of task %s: %s", task_id, stdout) - if 'Traceback' in stderr and 'SystemExit' not in stderr: - self.logger.error("Executed code with failure: %s", b64encode(code)) + try: + self.shell.runcode(code) + except SystemExit: + pass - # Determine the return value - if 'return_value' not in self.shell.console.locals: - return_value = b64encode(b'') - else: - return_value = b64encode(self.shell.console.locals['return_value'].encode('utf-8')) + if self.shell.last_traceback: + self.on_crash(self.shell.last_traceback) + return + + self.logger.info("Code execution with task %s finished:", task_id) + return_value = b64encode(self.shell.locals.get('return_value', '').encode('utf-8')) for socket in self.sockets: socket.write(b"result %s %s\n" % (return_value, task_id)) def on_crash(self, exception_text): + self.logger.error(f"Crash in CodeExecutor:\n{exception_text}") + self.stack_trace = exception_text for socket in self.sockets: socket.write(b"crash %s\n" % b64encode(exception_text.encode('utf-8'))) @@ -77,63 +83,40 @@ def _on_socket_read_ready(self): return try: - code = b64decode(parts[0]) + code = b64decode(parts[0]).decode('utf8') task_id = parts[1].replace(b'\n', b'') self.run_code(code, task_id) except binascii.Error: self.logger.error("Invalid base64 code string received!") def _on_socket_disconnect(self, socket): - self.sockets.remove(socket) - - -class Stream: - def __init__(self): - self.stream = io.StringIO() + def on_socket_disconnect_handler(): + self.sockets.remove(socket) + return on_socket_disconnect_handler - def read(self, *args, **kwargs): - result = self.stream.read(*args, **kwargs) - self.stream = io.StringIO(self.stream.read()) - return result +class Console(InteractiveConsole): + last_traceback = None - def write(self, *args, **kwargs): - p = self.stream.tell() - self.stream.seek(0, io.SEEK_END) - result = self.stream.write(*args, **kwargs) - self.stream.seek(p) + def __init__(self, locals, logger): # pylint: disable=redefined-builtin + super().__init__(locals=locals) + self.logger = logger - return result - - -class Console: - def __init__(self, locals=None): - self.console = code.InteractiveConsole(locals=locals) - - self.stdout = Stream() - self.stderr = Stream() - - def runcode(self, *args, **kwargs): - stdout = sys.stdout - sys.stdout = self.stdout - - stderr = sys.stderr - sys.stderr = self.stderr - - result = None + def showtraceback(self) -> None: + last_type, last_value, last_tb = sys.exc_info() try: - result = self.console.runcode(*args, **kwargs) - except SyntaxError: - self.console.showsyntaxerror() - except SystemExit: - pass - except: - self.console.showtraceback() - - sys.stdout = stdout - sys.stderr = stderr - - return result - - def execute(self, command): - return self.runcode(code.compile_command(command)) + self.last_traceback = ''.join(traceback.format_exception(last_type, last_value, last_tb)) + self.take_screenshot() + super().showtraceback() # report the error to Sentry + finally: + del last_tb + + def take_screenshot(self): + window = self.locals.get('window') + if not window: + self.logger.warning("Cannot take screenshot, window is not found in locals") + else: + app_tester_dir = self.locals.get('app_tester_dir') + screenshots_dir = Path(app_tester_dir or os.getcwd()) / "screenshots" + self.logger.info(f"Creating screenshot in {screenshots_dir}") + take_screenshot(window, screenshots_dir) diff --git a/src/tribler-gui/tribler_gui/error_handler.py b/src/tribler-gui/tribler_gui/error_handler.py index dbbabd227a6..22bc143e706 100644 --- a/src/tribler-gui/tribler_gui/error_handler.py +++ b/src/tribler-gui/tribler_gui/error_handler.py @@ -22,10 +22,14 @@ def __init__(self, tribler_window): self._tribler_stopped = False def gui_error(self, *exc_info): + if exc_info and len(exc_info) == 3: + info_type, info_error, tb = exc_info + text = "".join(traceback.format_exception(info_type, info_error, tb)) + self._logger.error(text) + if self._tribler_stopped: return - info_type, info_error, tb = exc_info if SentryReporter.global_strategy == SentryStrategy.SEND_SUPPRESSED: self._logger.info(f'GUI error was suppressed and not sent to Sentry: {info_type.__name__}: {info_error}') return @@ -34,14 +38,11 @@ def gui_error(self, *exc_info): return self._handled_exceptions.add(info_type) - text = "".join(traceback.format_exception(info_type, info_error, tb)) - is_core_exception = issubclass(info_type, CoreError) if is_core_exception: text = text + self.tribler_window.core_manager.core_traceback self._stop_tribler(text) - self._logger.error(text) reported_error = ReportedError( type=type(info_type).__name__, text=text, diff --git a/src/tribler-gui/tribler_gui/tribler_app.py b/src/tribler-gui/tribler_gui/tribler_app.py index 444e84d3e62..916f016b866 100644 --- a/src/tribler-gui/tribler_gui/tribler_app.py +++ b/src/tribler-gui/tribler_gui/tribler_app.py @@ -1,4 +1,5 @@ import os +import os.path import sys from PyQt5.QtCore import QCoreApplication, QEvent, Qt diff --git a/src/tribler-gui/tribler_gui/utilities.py b/src/tribler-gui/tribler_gui/utilities.py index 0e849775bfb..f82ad1c3c7d 100644 --- a/src/tribler-gui/tribler_gui/utilities.py +++ b/src/tribler-gui/tribler_gui/utilities.py @@ -3,6 +3,7 @@ import math import os import sys +import time import traceback import types from datetime import datetime, timedelta @@ -11,7 +12,8 @@ from urllib.parse import quote_plus from uuid import uuid4 -from PyQt5.QtCore import QCoreApplication, QLocale, QTranslator, pyqtSignal +from PyQt5.QtCore import QCoreApplication, QLocale, QPoint, QTranslator, pyqtSignal +from PyQt5.QtGui import QPixmap, QRegion from PyQt5.QtWidgets import QApplication import tribler_gui @@ -434,3 +436,12 @@ def get_translator(language=None): filename = "" translator.load(locale, filename, directory=TRANSLATIONS_DIR) return translator + + +def take_screenshot(window, screenshots_dir): + timestamp = int(time.time()) + pixmap = QPixmap(window.rect().size()) + window.render(pixmap, QPoint(), QRegion(window.rect())) + screenshots_dir.mkdir(exist_ok=True) + img_name = 'exception_screenshot_%d.jpg' % timestamp + pixmap.save(str(screenshots_dir / img_name))