Skip to content

Commit

Permalink
Merge pull request #6663 from kozlovsky/fix/apptester
Browse files Browse the repository at this point in the history
Fix/apptester
  • Loading branch information
kozlovsky authored Dec 17, 2021
2 parents f413cf8 + e17b009 commit 6bca340
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 72 deletions.
117 changes: 50 additions & 67 deletions src/tribler-gui/tribler_gui/code_executor.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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 = []
Expand All @@ -33,39 +35,43 @@ 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
if self.stack_trace:
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')))
Expand All @@ -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)
9 changes: 5 additions & 4 deletions src/tribler-gui/tribler_gui/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/tribler-gui/tribler_gui/tribler_app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import os.path
import sys

from PyQt5.QtCore import QCoreApplication, QEvent, Qt
Expand Down
13 changes: 12 additions & 1 deletion src/tribler-gui/tribler_gui/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import math
import os
import sys
import time
import traceback
import types
from datetime import datetime, timedelta
Expand All @@ -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
Expand Down Expand Up @@ -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))

0 comments on commit 6bca340

Please sign in to comment.