mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Console to system tray - added max lines
Added missed file Added unique host id for multiple items in submenu
This commit is contained in:
parent
0561940f3d
commit
962d6e9079
2 changed files with 190 additions and 9 deletions
153
openpype/modules/webserver/host_console_listener.py
Normal file
153
openpype/modules/webserver/host_console_listener.py
Normal 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
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue