Console to system tray - added max lines

Added missed file
Added unique host id for multiple items in submenu
This commit is contained in:
Petr Kalis 2021-06-01 13:04:06 +02:00
parent 0561940f3d
commit 962d6e9079
2 changed files with 190 additions and 9 deletions

View file

@ -0,0 +1,153 @@
import aiohttp
from aiohttp import web
import json
import logging
from concurrent.futures import CancelledError
from Qt import QtWidgets, QtCore
from openpype.modules import TrayModulesManager, ITrayService
from openpype.tools.tray_app.app import ConsoleDialog
log = logging.getLogger(__name__)
class IconType:
IDLE = "idle"
RUNNING = "running"
FAILED = "failed"
class MsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"
class HostListener:
def __init__(self, webserver, module):
self._window_per_id = {}
self.module = module
self.webserver = webserver
self._window_per_id = {} # dialogs per host name
self._action_per_id = {} # QAction per host name
webserver.add_route('*', "/ws/host_listener", self.websocket_handler)
def _host_is_connecting(self, host_name, label):
""" Initialize dialog, adds to submenu. """
services_submenu = self.module._services_submenu
action = QtWidgets.QAction(label, services_submenu)
action.triggered.connect(lambda: self.show_widget(host_name))
services_submenu.addAction(action)
self._action_per_id[host_name] = action
self._set_host_icon(host_name, IconType.IDLE)
widget = ConsoleDialog("")
self._window_per_id[host_name] = widget
def _set_host_icon(self, host_name, icon_type):
"""Assigns icon to action for 'host_name' with 'icon_type'.
Action must exist in self._action_per_id
Args:
host_name (str)
icon_type (IconType)
"""
action = self._action_per_id.get(host_name)
if not action:
raise ValueError("Unknown host {}".format(host_name))
icon = None
if icon_type == IconType.IDLE:
icon = ITrayService.get_icon_idle()
elif icon_type == IconType.RUNNING:
icon = ITrayService.get_icon_running()
elif icon_type == IconType.FAILED:
icon = ITrayService.get_icon_failed()
else:
log.info("Unknown icon type {} for {}".format(icon_type,
host_name))
action.setIcon(icon)
def show_widget(self, host_name):
"""Shows prepared widget for 'host_name'.
Dialog get initialized when 'host_name' is connecting.
"""
self.module.execute_in_main_thread(
lambda: self._show_widget(host_name))
def _show_widget(self, host_name):
widget = self._window_per_id[host_name]
widget.show()
widget.raise_()
widget.activateWindow()
async def websocket_handler(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
widget = None
try:
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
host_name, action, text = self._parse_message(msg)
if action == MsgAction.CONNECTING:
self._action_per_id[host_name] = None
# must be sent to main thread, or action wont trigger
self.module.execute_in_main_thread(
lambda: self._host_is_connecting(host_name, text))
elif action == MsgAction.CLOSE:
# clean close
crashed = False
self._close(host_name)
await ws.close()
elif action == MsgAction.INITIALIZED:
initialized = True
self.module.execute_in_main_thread(
# must be queued as _host_is_connecting might not
# be triggered/finished yet
lambda: self._set_host_icon(host_name,
IconType.RUNNING))
elif action == MsgAction.ADD:
self.module.execute_in_main_thread(
lambda: self._add_text(host_name, text))
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' %
ws.exception())
host_name, _, _ = self._parse_message(msg)
self._set_host_icon(host_name, IconType.FAILED)
except CancelledError: # recoverable
pass
except Exception as exc:
error_msg = str(exc)
log.warning("Exception during communication", exc_info=True)
if widget:
widget.append_text(text)
return ws
def _add_text(self, host_name, text):
widget = self._window_per_id[host_name]
widget.append_text(text)
def _close(self, host_name):
""" Clean close - remove from menu, delete widget."""
services_submenu = self.module._services_submenu
action = self._action_per_id.pop(host_name)
services_submenu.removeAction(action)
widget = self._window_per_id.pop(host_name)
if widget.isVisible():
widget.hide()
widget.deleteLater()
def _parse_message(self, msg):
data = json.loads(msg.data)
action = data.get("action")
host_name = data["host"]
value = data.get("text")
return host_name, action, value

View file

@ -1,9 +1,12 @@
import os
import sys
import re
import collections
import queue
import websocket
import json
import itertools
from datetime import datetime
from avalon import style
from openpype.modules.webserver import host_console_listener
@ -11,12 +14,17 @@ from openpype.modules.webserver import host_console_listener
from Qt import QtWidgets, QtGui, QtCore
class ConsoleTrayApp():
"""Application showing console for non python hosts instead of cmd"""
class ConsoleTrayApp:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
callback_queue = None
process = None
webserver_client = None
MAX_LINES = 10000
sdict = {
r">>> ":
'<span style="font-weight: bold;color:#EE5C42"> >>> </span>',
@ -79,15 +87,25 @@ class ConsoleTrayApp():
self.timer = timer
self.catch_std_outputs()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host, date_str)
def _connect(self):
""" Connect to Tray webserver to pass console output. """
ws = websocket.WebSocket()
ws.connect("ws://localhost:8079/ws/host_listener")
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
self.tray_reconnect = False
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
ConsoleTrayApp.webserver_client = ws
payload = {
"host": self.host,
"host": self.host_id,
"action": host_console_listener.MsgAction.CONNECTING,
"text": "Integration with {}".format(str.capitalize(self.host))
}
@ -101,7 +119,7 @@ class ConsoleTrayApp():
return
payload = {
"host": self.host,
"host": self.host_id,
"action": host_console_listener.MsgAction.INITIALIZED,
"text": "Integration with {}".format(str.capitalize(self.host))
}
@ -115,7 +133,7 @@ class ConsoleTrayApp():
return
payload = {
"host": self.host,
"host": self.host_id,
"action": host_console_listener.MsgAction.CLOSE,
"text": "Integration with {}".format(str.capitalize(self.host))
}
@ -133,7 +151,7 @@ class ConsoleTrayApp():
new_text = collections.deque(new_text.split("\n"))
payload = {
"host": self.host,
"host": self.host_id,
"action": host_console_listener.MsgAction.ADD,
"text": "\n".join(new_text)
}
@ -160,6 +178,11 @@ class ConsoleTrayApp():
self._send_text(self.new_text)
self.new_text = collections.deque()
if self.new_text: # no webserver_client, text keeps stashing
start = max(len(self.new_text) - self.MAX_LINES, 0)
self.new_text = itertools.islice(self.new_text,
start, self.MAX_LINES)
if not self.initialized:
if self.initializing:
host_connected = self.is_host_connected()
@ -274,6 +297,7 @@ 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)
@ -282,6 +306,8 @@ class ConsoleDialog(QtWidgets.QDialog):
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())
@ -299,5 +325,7 @@ class ConsoleDialog(QtWidgets.QDialog):
if isinstance(new_text, str):
new_text = collections.deque(new_text.split("\n"))
while new_text:
self.plain_text.appendHtml(
ConsoleTrayApp.color(new_text.popleft()))
text = new_text.popleft()
if text:
self.plain_text.appendHtml(
ConsoleTrayApp.color(text))