From 58cc55cff45b6cd4456ab0c2bb3a83fec0836e7a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Feb 2022 13:01:07 +0100 Subject: [PATCH] OP-2414 - refactor of StdOutBroker Cleaned up unwanted parts (websocket_server). Split into app and window. --- openpype/hosts/harmony/api/lib.py | 7 +- .../webserver/host_console_listener.py | 3 +- openpype/tools/stdout_broker/app.py | 200 +++++------------- openpype/tools/stdout_broker/window.py | 103 +++++++++ 4 files changed, 155 insertions(+), 158 deletions(-) create mode 100644 openpype/tools/stdout_broker/window.py diff --git a/openpype/hosts/harmony/api/lib.py b/openpype/hosts/harmony/api/lib.py index d6fa1f30d8..134f670dc4 100644 --- a/openpype/hosts/harmony/api/lib.py +++ b/openpype/hosts/harmony/api/lib.py @@ -51,8 +51,8 @@ class ProcessContext: callback() if cls.process is not None and cls.process.poll() is not None: log.info("Server is not running, closing") - ProcessContext.stdout_broker.exit() - sys.exit() + ProcessContext.stdout_broker.stop() + QtWidgets.QApplication.quit() def signature(postfix="func") -> str: @@ -88,7 +88,7 @@ def main(*subprocess_args): app.setWindowIcon(icon) ProcessContext.stdout_broker = StdOutBroker('harmony') - + ProcessContext.stdout_broker.start() launch(*subprocess_args) loop_timer = QtCore.QTimer() @@ -620,4 +620,3 @@ def find_node_by_name(name, node_type): return node return None - diff --git a/openpype/modules/webserver/host_console_listener.py b/openpype/modules/webserver/host_console_listener.py index 19428adce3..6138f9f097 100644 --- a/openpype/modules/webserver/host_console_listener.py +++ b/openpype/modules/webserver/host_console_listener.py @@ -34,8 +34,7 @@ class HostListener: webserver.add_route('*', "/ws/host_listener", self.websocket_handler) def _host_is_connecting(self, host_name, label): - from openpype.tools.stdout_broker.app import ConsoleDialog - + from openpype.tools.stdout_broker.window import ConsoleDialog """ Initialize dialog, adds to submenu. """ services_submenu = self.module._services_submenu action = QtWidgets.QAction(label, services_submenu) diff --git a/openpype/tools/stdout_broker/app.py b/openpype/tools/stdout_broker/app.py index f714071b31..a42d93dab4 100644 --- a/openpype/tools/stdout_broker/app.py +++ b/openpype/tools/stdout_broker/app.py @@ -1,18 +1,14 @@ import os import sys -import re +import threading import collections import websocket import json from datetime import datetime -from avalon import style from openpype_modules.webserver.host_console_listener import MsgAction - from openpype.api import Logger -from Qt import QtWidgets, QtCore - log = Logger.get_logger(__name__) @@ -21,53 +17,12 @@ class StdOutBroker: Application showing console in Services tray for non python hosts instead of cmd window. """ - callback_queue = None - process = None - webserver_client = None - _instance = None - MAX_LINES = 10000 - - sdict = { - r">>> ": - ' >>> ', - r"!!!(?!\sCRI|\sERR)": - ' !!! ', - r"\-\-\- ": - ' --- ', - r"\*\*\*(?!\sWRN)": - ' *** ', - r"\*\*\* WRN": - ' *** WRN', - r" \- ": - ' - ', - r"\[ ": - '[', - r"\]": - ']', - r"{": - '{', - r"}": - r"}", - r"\(": - '(', - r"\)": - r")", - r"^\.\.\. ": - ' ... ', - r"!!! ERR: ": - ' !!! ERR: ', - r"!!! CRI: ": - ' !!! CRI: ', - r"(?i)failed": - ' FAILED ', - r"(?i)error": - ' ERROR ' - } + TIMER_TIMEOUT = 0.200 def __init__(self, host_name): self.host_name = host_name - self.websocket_server = None + self.webserver_client = None self.original_stdout_write = None self.original_stderr_write = None @@ -77,25 +32,55 @@ class StdOutBroker: self.host_id = "{}_{}".format(self.host_name, date_str) self._std_available = False + self._is_running = False self._catch_std_outputs() - self._connect_to_tray() - loop_timer = QtCore.QTimer() - loop_timer.setInterval(200) - loop_timer.timeout.connect(self._process_queue) - self.loop_timer = loop_timer - - @property - def websocket_server_is_running(self): - if self.websocket_server is not None: - return self.websocket_server.is_running - return False + self._timer = None @property def send_to_tray(self): """Checks if connected to tray and have access to logs.""" return self.webserver_client and self._std_available + def start(self): + """Start app, create and start timer""" + if not self._std_available or self._is_running: + return + self._is_running = True + self._create_timer() + self._connect_to_tray() + + def stop(self): + """Disconnect from Tray, process last logs""" + if not self._is_running: + return + self._is_running = False + self._process_queue() + self._disconnect_from_tray() + + def host_connected(self): + """Send to Tray console that host is ready - icon change. """ + log.info("Host {} connected".format(self.host_id)) + + payload = { + "host": self.host_id, + "action": MsgAction.INITIALIZED, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + self._send(payload) + + def _create_timer(self): + timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback) + timer.start() + self._timer = timer + + def _timer_callback(self): + if not self._is_running: + return + self._process_queue() + self._create_timer() + def _connect_to_tray(self): """Connect to Tray webserver to pass console output. """ if not self._std_available: # not content to log @@ -135,50 +120,26 @@ class StdOutBroker: self._send(payload) self.webserver_client.close() - def host_connected(self): - """Send to Tray console that host is ready - icon change. """ - log.info("Host {} connected".format(self.host_id)) - - payload = { - "host": self.host_id, - "action": MsgAction.INITIALIZED, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - self._send(payload) - self.loop_timer.start() - - def restart_server(self): - if self.websocket_server: - self.websocket_server.stop_server(restart=True) - - def exit(self): - """Exit whole application. """ - self._disconnect_from_tray() - - if self.websocket_server: - self.websocket_server.stop() - def _catch_std_outputs(self): """Redirects standard out and error to own functions""" if sys.stdout: self.original_stdout_write = sys.stdout.write - sys.stdout.write = self.my_stdout_write + sys.stdout.write = self._my_stdout_write self._std_available = True if sys.stderr: self.original_stderr_write = sys.stderr.write - sys.stderr.write = self.my_stderr_write + sys.stderr.write = self._my_stderr_write self._std_available = True - def my_stdout_write(self, text): + def _my_stdout_write(self, text): """Appends outputted text to queue, keep writing to original stdout""" if self.original_stdout_write is not None: self.original_stdout_write(text) if self.send_to_tray: self.log_queue.append(text) - def my_stderr_write(self, text): + def _my_stderr_write(self, text): """Appends outputted text to queue, keep writing to original stderr""" if self.original_stderr_write is not None: self.original_stderr_write(text) @@ -210,68 +171,3 @@ class StdOutBroker: self.webserver_client.send(json.dumps(payload)) except ConnectionResetError: # Tray closed self._connect_to_tray() - - @staticmethod - def _multiple_replace(text, adict): - """Replace multiple tokens defined in dict. - - Find and replace all occurrences of strings defined in dict is - supplied string. - - Args: - text (str): string to be searched - adict (dict): dictionary with `{'search': 'replace'}` - - Returns: - str: string with replaced tokens - - """ - for r, v in adict.items(): - text = re.sub(r, v, text) - - return text - - @staticmethod - def color(message): - """Color message with html tags. """ - message = StdOutBroker._multiple_replace(message, - StdOutBroker.sdict) - - return message - - -class ConsoleDialog(QtWidgets.QDialog): - """Qt dialog to show stdout instead of unwieldy cmd window""" - WIDTH = 720 - HEIGHT = 450 - MAX_LINES = 10000 - - def __init__(self, text, parent=None): - super(ConsoleDialog, self).__init__(parent) - layout = QtWidgets.QHBoxLayout(parent) - - plain_text = QtWidgets.QPlainTextEdit(self) - plain_text.setReadOnly(True) - plain_text.resize(self.WIDTH, self.HEIGHT) - plain_text.maximumBlockCount = self.MAX_LINES - - while text: - plain_text.appendPlainText(text.popleft().strip()) - - layout.addWidget(plain_text) - - self.setWindowTitle("Console output") - - self.plain_text = plain_text - - self.setStyleSheet(style.load_stylesheet()) - - self.resize(self.WIDTH, self.HEIGHT) - - def append_text(self, new_text): - if isinstance(new_text, str): - new_text = collections.deque(new_text.split("\n")) - while new_text: - text = new_text.popleft() - if text: - self.plain_text.appendHtml(StdOutBroker.color(text)) diff --git a/openpype/tools/stdout_broker/window.py b/openpype/tools/stdout_broker/window.py new file mode 100644 index 0000000000..a2190e0491 --- /dev/null +++ b/openpype/tools/stdout_broker/window.py @@ -0,0 +1,103 @@ +from avalon import style +from Qt import QtWidgets, QtCore +import collections +import re + + +class ConsoleDialog(QtWidgets.QDialog): + """Qt dialog to show stdout instead of unwieldy cmd window""" + WIDTH = 720 + HEIGHT = 450 + MAX_LINES = 10000 + + sdict = { + r">>> ": + ' >>> ', + r"!!!(?!\sCRI|\sERR)": + ' !!! ', + r"\-\-\- ": + ' --- ', + r"\*\*\*(?!\sWRN)": + ' *** ', + r"\*\*\* WRN": + ' *** WRN', + r" \- ": + ' - ', + r"\[ ": + '[', + r"\]": + ']', + r"{": + '{', + r"}": + r"}", + r"\(": + '(', + r"\)": + r")", + r"^\.\.\. ": + ' ... ', + r"!!! ERR: ": + ' !!! ERR: ', + r"!!! CRI: ": + ' !!! CRI: ', + r"(?i)failed": + ' FAILED ', + r"(?i)error": + ' ERROR ' + } + + def __init__(self, text, parent=None): + super(ConsoleDialog, self).__init__(parent) + layout = QtWidgets.QHBoxLayout(parent) + + plain_text = QtWidgets.QPlainTextEdit(self) + plain_text.setReadOnly(True) + plain_text.resize(self.WIDTH, self.HEIGHT) + plain_text.maximumBlockCount = self.MAX_LINES + + while text: + plain_text.appendPlainText(text.popleft().strip()) + + layout.addWidget(plain_text) + + self.setWindowTitle("Console output") + + self.plain_text = plain_text + + self.setStyleSheet(style.load_stylesheet()) + + self.resize(self.WIDTH, self.HEIGHT) + + def append_text(self, new_text): + if isinstance(new_text, str): + new_text = collections.deque(new_text.split("\n")) + while new_text: + text = new_text.popleft() + if text: + self.plain_text.appendHtml(self.color(text)) + + def _multiple_replace(self, text, adict): + """Replace multiple tokens defined in dict. + + Find and replace all occurrences of strings defined in dict is + supplied string. + + Args: + text (str): string to be searched + adict (dict): dictionary with `{'search': 'replace'}` + + Returns: + str: string with replaced tokens + + """ + for r, v in adict.items(): + text = re.sub(r, v, text) + + return text + + def color(self, message): + """Color message with html tags. """ + message = self._multiple_replace(message, self.sdict) + + return message