diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py
index c16a72c5e5..393a878f76 100644
--- a/openpype/hooks/pre_non_python_host_launch.py
+++ b/openpype/hooks/pre_non_python_host_launch.py
@@ -1,4 +1,5 @@
import os
+import subprocess
from openpype.lib import (
PreLaunchHook,
@@ -17,6 +18,8 @@ class NonPythonHostHook(PreLaunchHook):
"""
app_groups = ["harmony", "photoshop", "aftereffects"]
+ order = 20
+
def execute(self):
# Pop executable
executable_path = self.launch_context.launch_args.pop(0)
@@ -45,3 +48,6 @@ class NonPythonHostHook(PreLaunchHook):
if remainders:
self.launch_context.launch_args.extend(remainders)
+
+ self.launch_context.kwargs["stdout"] = subprocess.DEVNULL
+ self.launch_context.kwargs["stderr"] = subprocess.STDOUT
diff --git a/openpype/hooks/pre_with_windows_shell.py b/openpype/hooks/pre_with_windows_shell.py
index 0c10583b99..441ab1a675 100644
--- a/openpype/hooks/pre_with_windows_shell.py
+++ b/openpype/hooks/pre_with_windows_shell.py
@@ -11,12 +11,14 @@ class LaunchWithWindowsShell(PreLaunchHook):
instead.
"""
- # Should be as last hook becuase must change launch arguments to string
+ # Should be as last hook because must change launch arguments to string
order = 1000
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
platforms = ["windows"]
def execute(self):
+ launch_args = self.launch_context.clear_launch_args(
+ self.launch_context.launch_args)
new_args = [
# Get comspec which is cmd.exe in most cases.
os.environ.get("COMSPEC", "cmd.exe"),
@@ -24,7 +26,7 @@ class LaunchWithWindowsShell(PreLaunchHook):
"/c",
# Convert arguments to command line arguments (as string)
"\"{}\"".format(
- subprocess.list2cmdline(self.launch_context.launch_args)
+ subprocess.list2cmdline(launch_args)
)
]
# Convert list to string
diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py
index b7f8e0e252..943cd9fcaf 100644
--- a/openpype/lib/delivery.py
+++ b/openpype/lib/delivery.py
@@ -43,7 +43,7 @@ def sizeof_fmt(num, suffix='B'):
return "%.1f%s%s" % (num, 'Yi', suffix)
-def path_from_represenation(representation, anatomy):
+def path_from_representation(representation, anatomy):
from avalon import pipeline # safer importing
try:
@@ -126,18 +126,22 @@ def check_destination_path(repre_id,
anatomy_filled = anatomy.format_all(anatomy_data)
dest_path = anatomy_filled["delivery"][template_name]
report_items = collections.defaultdict(list)
- sub_msg = None
+
if not dest_path.solved:
msg = (
"Missing keys in Representation's context"
" for anatomy template \"{}\"."
).format(template_name)
+ sub_msg = (
+ "Representation: {}
"
+ ).format(repre_id)
+
if dest_path.missing_keys:
keys = ", ".join(dest_path.missing_keys)
- sub_msg = (
- "Representation: {}
- Missing keys: \"{}\"
"
- ).format(repre_id, keys)
+ sub_msg += (
+ "- Missing keys: \"{}\"
"
+ ).format(keys)
if dest_path.invalid_types:
items = []
@@ -145,10 +149,9 @@ def check_destination_path(repre_id,
items.append("\"{}\" {}".format(key, str(value)))
keys = ", ".join(items)
- sub_msg = (
- "Representation: {}
"
+ sub_msg += (
"- Invalid value DataType: \"{}\"
"
- ).format(repre_id, keys)
+ ).format(keys)
report_items[msg].append(sub_msg)
diff --git a/openpype/modules/base.py b/openpype/modules/base.py
index 50bd6411ce..441a9731b7 100644
--- a/openpype/modules/base.py
+++ b/openpype/modules/base.py
@@ -139,6 +139,25 @@ class ITrayModule:
"""
pass
+ def execute_in_main_thread(self, callback):
+ """ Pushes callback to the queue or process 'callback' on a main thread
+
+ Some callbacks need to be processed on main thread (menu actions
+ must be added on main thread or they won't get triggered etc.)
+ """
+ # called without initialized tray, still main thread needed
+ if not self.tray_initialized:
+ try:
+ callback = self._main_thread_callbacks.popleft()
+ callback()
+ except:
+ self.log.warning(
+ "Failed to execute {} in main thread".format(callback),
+ exc_info=True)
+
+ return
+ self.manager.tray_manager.execute_in_main_thread(callback)
+
def show_tray_message(self, title, message, icon=None, msecs=None):
"""Show tray message.
diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py
index f8553b2eac..2e7599647a 100644
--- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py
+++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py
@@ -9,7 +9,7 @@ from openpype.api import Anatomy, config
from openpype.modules.ftrack.lib import BaseAction, statics_icon
from openpype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY
from openpype.lib.delivery import (
- path_from_represenation,
+ path_from_representation,
get_format_dict,
check_destination_path,
process_single_file,
@@ -74,7 +74,7 @@ class Delivery(BaseAction):
"value": project_name
})
- # Prpeare anatomy data
+ # Prepare anatomy data
anatomy = Anatomy(project_name)
new_anatomies = []
first = None
@@ -368,12 +368,18 @@ class Delivery(BaseAction):
def launch(self, session, entities, event):
if "values" not in event["data"]:
- return
+ return {
+ "success": True,
+ "message": "Nothing to do"
+ }
values = event["data"]["values"]
skipped = values.pop("__skipped__")
if skipped:
- return None
+ return {
+ "success": False,
+ "message": "Action skipped"
+ }
user_id = event["source"]["user"]["id"]
user_entity = session.query(
@@ -391,27 +397,45 @@ class Delivery(BaseAction):
try:
self.db_con.install()
- self.real_launch(session, entities, event)
- job["status"] = "done"
+ report = self.real_launch(session, entities, event)
- except Exception:
+ except Exception as exc:
+ report = {
+ "success": False,
+ "title": "Delivery failed",
+ "items": [{
+ "type": "label",
+ "value": (
+ "Error during delivery action process:
{}"
+ "
Check logs for more information."
+ ).format(str(exc))
+ }]
+ }
self.log.warning(
"Failed during processing delivery action.",
exc_info=True
)
finally:
- if job["status"] != "done":
+ if report["success"]:
+ job["status"] = "done"
+ else:
job["status"] = "failed"
session.commit()
self.db_con.uninstall()
- if job["status"] == "failed":
+ if not report["success"]:
+ self.show_interface(
+ items=report["items"],
+ title=report["title"],
+ event=event
+ )
return {
"success": False,
- "message": "Delivery failed. Check logs for more information."
+ "message": "Errors during delivery process. See report."
}
- return True
+
+ return report
def real_launch(self, session, entities, event):
self.log.info("Delivery action just started.")
@@ -431,7 +455,7 @@ class Delivery(BaseAction):
if not repre_names:
return {
"success": True,
- "message": "Not selected components to deliver."
+ "message": "No selected components to deliver."
}
location_path = location_path.strip()
@@ -479,7 +503,7 @@ class Delivery(BaseAction):
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
- repre_path = path_from_represenation(repre, anatomy)
+ repre_path = path_from_representation(repre, anatomy)
# TODO add backup solution where root of path from component
# is replaced with root
args = (
@@ -502,7 +526,7 @@ class Delivery(BaseAction):
def report(self, report_items):
"""Returns dict with final status of delivery (succes, fail etc.)."""
items = []
- title = "Delivery report"
+
for msg, _items in report_items.items():
if not _items:
continue
@@ -533,9 +557,8 @@ class Delivery(BaseAction):
return {
"items": items,
- "title": title,
- "success": False,
- "message": "Delivery Finished"
+ "title": "Delivery report",
+ "success": False
}
diff --git a/openpype/modules/webserver/host_console_listener.py b/openpype/modules/webserver/host_console_listener.py
new file mode 100644
index 0000000000..9dd7dcc9b6
--- /dev/null
+++ b/openpype/modules/webserver/host_console_listener.py
@@ -0,0 +1,151 @@
+import aiohttp
+from aiohttp import web
+import json
+import logging
+from concurrent.futures import CancelledError
+from Qt import QtWidgets
+
+from openpype.modules import 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
+ self._close(host_name)
+ await ws.close()
+ elif action == MsgAction.INITIALIZED:
+ 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:
+ log.warning("Exception during communication", exc_info=True)
+ if widget:
+ error_msg = str(exc)
+ widget.append_text(error_msg)
+
+ 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
diff --git a/openpype/modules/webserver/webserver_module.py b/openpype/modules/webserver/webserver_module.py
index 59a0a08427..2d88aff40d 100644
--- a/openpype/modules/webserver/webserver_module.py
+++ b/openpype/modules/webserver/webserver_module.py
@@ -7,6 +7,8 @@ import six
from openpype import resources
from .. import PypeModule, ITrayService
+from openpype.modules.webserver.host_console_listener import HostListener
+
@six.add_metaclass(ABCMeta)
class IWebServerRoutes:
@@ -23,6 +25,7 @@ class WebServerModule(PypeModule, ITrayService):
def initialize(self, _module_settings):
self.enabled = True
self.server_manager = None
+ self._host_listener = None
self.port = self.find_free_port()
@@ -37,6 +40,7 @@ class WebServerModule(PypeModule, ITrayService):
def tray_init(self):
self.create_server_manager()
self._add_resources_statics()
+ self._add_listeners()
def tray_start(self):
self.start_server()
@@ -54,6 +58,9 @@ class WebServerModule(PypeModule, ITrayService):
webserver_url, static_prefix
)
+ def _add_listeners(self):
+ self._host_listener = HostListener(self.server_manager, self)
+
def start_server(self):
if self.server_manager:
self.server_manager.start_server()
diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py
index 68b1f9a52a..3753f1bfc9 100644
--- a/openpype/plugins/load/delivery.py
+++ b/openpype/plugins/load/delivery.py
@@ -11,7 +11,7 @@ from openpype import resources
from openpype.lib.delivery import (
sizeof_fmt,
- path_from_represenation,
+ path_from_representation,
get_format_dict,
check_destination_path,
process_single_file,
@@ -170,7 +170,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
if repre["name"] not in selected_repres:
continue
- repre_path = path_from_represenation(repre, self.anatomy)
+ repre_path = path_from_representation(repre, self.anatomy)
anatomy_data = copy.deepcopy(repre["context"])
new_report_items = check_destination_path(str(repre["_id"]),
diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py
index 506105d2ce..32c4b23f4f 100644
--- a/openpype/scripts/non_python_host_launch.py
+++ b/openpype/scripts/non_python_host_launch.py
@@ -81,11 +81,11 @@ def main(argv):
host_name = os.environ["AVALON_APP"].lower()
if host_name == "photoshop":
- from avalon.photoshop.lib import launch
+ from avalon.photoshop.lib import main
elif host_name == "aftereffects":
- from avalon.aftereffects.lib import launch
+ from avalon.aftereffects.lib import main
elif host_name == "harmony":
- from avalon.harmony.lib import launch
+ from avalon.harmony.lib import main
else:
title = "Unknown host name"
message = (
@@ -97,7 +97,7 @@ def main(argv):
if launch_args:
# Launch host implementation
- launch(*launch_args)
+ main(*launch_args)
else:
# Show message box
on_invalid_args(after_script_idx is None)
diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py
index a5c61a9dda..05f4ea64f8 100644
--- a/openpype/settings/entities/lib.py
+++ b/openpype/settings/entities/lib.py
@@ -76,27 +76,33 @@ def _fill_schema_template_data(
if key not in template_data:
template_data[key] = value
- # Store paths by first part if path
- # - None value says that whole key should be skipped
- skip_paths_by_first_key = {}
- for path in skip_paths:
- parts = path.split("/")
- key = parts.pop(0)
- if key not in skip_paths_by_first_key:
- skip_paths_by_first_key[key] = []
-
- value = "/".join(parts)
- skip_paths_by_first_key[key].append(value or None)
-
if not template:
output = template
elif isinstance(template, list):
+ # Store paths by first part if path
+ # - None value says that whole key should be skipped
+ skip_paths_by_first_key = {}
+ for path in skip_paths:
+ parts = path.split("/")
+ key = parts.pop(0)
+ if key not in skip_paths_by_first_key:
+ skip_paths_by_first_key[key] = []
+
+ value = "/".join(parts)
+ skip_paths_by_first_key[key].append(value or None)
+
output = []
for item in template:
# Get skip paths for children item
_skip_paths = []
- if skip_paths_by_first_key and isinstance(item, dict):
+ if not isinstance(item, dict):
+ pass
+
+ elif item.get("type") in WRAPPER_TYPES:
+ _skip_paths = copy.deepcopy(skip_paths)
+
+ elif skip_paths_by_first_key:
# Check if this item should be skipped
key = item.get("key")
if key and key in skip_paths_by_first_key:
@@ -108,7 +114,8 @@ def _fill_schema_template_data(
output_item = _fill_schema_template_data(
item, template_data, _skip_paths, required_keys, missing_keys
)
- output.append(output_item)
+ if output_item:
+ output.append(output_item)
elif isinstance(template, dict):
output = {}
@@ -116,6 +123,8 @@ def _fill_schema_template_data(
output[key] = _fill_schema_template_data(
value, template_data, skip_paths, required_keys, missing_keys
)
+ if output.get("type") in WRAPPER_TYPES and not output.get("children"):
+ return {}
elif isinstance(template, STRING_TYPE):
# TODO find much better way how to handle filling template data
diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py
index fa16dbf855..d9806a96fd 100644
--- a/openpype/tools/tray/pype_tray.py
+++ b/openpype/tools/tray/pype_tray.py
@@ -1,3 +1,4 @@
+import collections
import os
import sys
import atexit
@@ -23,7 +24,6 @@ class TrayManager:
def __init__(self, tray_widget, main_window):
self.tray_widget = tray_widget
self.main_window = main_window
-
self.pype_info_widget = None
self.log = Logger.get_logger(self.__class__.__name__)
@@ -34,6 +34,28 @@ class TrayManager:
self.errors = []
+ self.main_thread_timer = None
+ self._main_thread_callbacks = collections.deque()
+ self._execution_in_progress = None
+
+ def execute_in_main_thread(self, callback):
+ self._main_thread_callbacks.append(callback)
+
+ def _main_thread_execution(self):
+ if self._execution_in_progress:
+ return
+ self._execution_in_progress = True
+ while self._main_thread_callbacks:
+ try:
+ callback = self._main_thread_callbacks.popleft()
+ callback()
+ except:
+ self.log.warning(
+ "Failed to execute {} in main thread".format(callback),
+ exc_info=True)
+
+ self._execution_in_progress = False
+
def initialize_modules(self):
"""Add modules to tray."""
@@ -59,6 +81,14 @@ class TrayManager:
# Print time report
self.modules_manager.print_report()
+ # create timer loop to check callback functions
+ main_thread_timer = QtCore.QTimer()
+ main_thread_timer.setInterval(300)
+ main_thread_timer.timeout.connect(self._main_thread_execution)
+ main_thread_timer.start()
+
+ self.main_thread_timer = main_thread_timer
+
def show_tray_message(self, title, message, icon=None, msecs=None):
"""Show tray message.
diff --git a/openpype/tools/tray_app/__init__.py b/openpype/tools/tray_app/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openpype/tools/tray_app/app.py b/openpype/tools/tray_app/app.py
new file mode 100644
index 0000000000..339e6343f8
--- /dev/null
+++ b/openpype/tools/tray_app/app.py
@@ -0,0 +1,331 @@
+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
+
+from Qt import QtWidgets, QtCore
+
+
+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">>> ":
+ ' >>> ',
+ 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, host, launch_method, subprocess_args, is_host_connected,
+ parent=None):
+ self.host = host
+
+ self.initialized = False
+ self.websocket_server = None
+ self.initializing = False
+ self.tray = False
+ self.launch_method = launch_method
+ self.subprocess_args = subprocess_args
+ self.is_host_connected = is_host_connected
+ self.tray_reconnect = True
+
+ self.original_stdout_write = None
+ self.original_stderr_write = None
+ self.new_text = collections.deque()
+
+ timer = QtCore.QTimer()
+ timer.timeout.connect(self.on_timer)
+ timer.setInterval(200)
+ timer.start()
+
+ 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()
+ 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_id,
+ "action": host_console_listener.MsgAction.CONNECTING,
+ "text": "Integration with {}".format(str.capitalize(self.host))
+ }
+ self.tray_reconnect = False
+ self._send(payload)
+
+ def _connected(self):
+ """ Send to Tray console that host is ready - icon change. """
+ print("Host {} connected".format(self.host))
+ if not ConsoleTrayApp.webserver_client:
+ return
+
+ payload = {
+ "host": self.host_id,
+ "action": host_console_listener.MsgAction.INITIALIZED,
+ "text": "Integration with {}".format(str.capitalize(self.host))
+ }
+ self.tray_reconnect = False
+ self._send(payload)
+
+ def _close(self):
+ """ Send to Tray that host is closing - remove from Services. """
+ print("Host {} closing".format(self.host))
+ if not ConsoleTrayApp.webserver_client:
+ return
+
+ payload = {
+ "host": self.host_id,
+ "action": host_console_listener.MsgAction.CLOSE,
+ "text": "Integration with {}".format(str.capitalize(self.host))
+ }
+
+ self._send(payload)
+ self.tray_reconnect = False
+ ConsoleTrayApp.webserver_client.close()
+
+ def _send_text(self, new_text):
+ """ Send console content. """
+ if not ConsoleTrayApp.webserver_client:
+ return
+
+ if isinstance(new_text, str):
+ new_text = collections.deque(new_text.split("\n"))
+
+ payload = {
+ "host": self.host_id,
+ "action": host_console_listener.MsgAction.ADD,
+ "text": "\n".join(new_text)
+ }
+
+ self._send(payload)
+
+ def _send(self, payload):
+ """ Worker method to send to existing websocket connection. """
+ if not ConsoleTrayApp.webserver_client:
+ return
+
+ try:
+ ConsoleTrayApp.webserver_client.send(json.dumps(payload))
+ except ConnectionResetError: # Tray closed
+ ConsoleTrayApp.webserver_client = None
+ self.tray_reconnect = True
+
+ def on_timer(self):
+ """Called periodically to initialize and run function on main thread"""
+ if self.tray_reconnect:
+ self._connect() # reconnect
+
+ if ConsoleTrayApp.webserver_client and self.new_text:
+ 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()
+ if host_connected is None: # keep trying
+ return
+ elif not host_connected:
+ text = "{} process is not alive. Exiting".format(self.host)
+ print(text)
+ self._send_text([text])
+ ConsoleTrayApp.websocket_server.stop()
+ sys.exit(1)
+ elif host_connected:
+ self.initialized = True
+ self.initializing = False
+ self._connected()
+
+ return
+
+ ConsoleTrayApp.callback_queue = queue.Queue()
+ self.initializing = True
+
+ self.launch_method(*self.subprocess_args)
+ elif ConsoleTrayApp.process.poll() is not None:
+ self.exit()
+ elif ConsoleTrayApp.callback_queue:
+ try:
+ callback = ConsoleTrayApp.callback_queue.get(block=False)
+ callback()
+ except queue.Empty:
+ pass
+
+ @classmethod
+ def execute_in_main_thread(cls, func_to_call_from_main_thread):
+ """Put function to the queue to be picked by 'on_timer'"""
+ if not cls.callback_queue:
+ cls.callback_queue = queue.Queue()
+ cls.callback_queue.put(func_to_call_from_main_thread)
+
+ @classmethod
+ def restart_server(cls):
+ if ConsoleTrayApp.websocket_server:
+ ConsoleTrayApp.websocket_server.stop_server(restart=True)
+
+ # obsolete
+ def exit(self):
+ """ Exit whole application. """
+ self._close()
+ if ConsoleTrayApp.websocket_server:
+ ConsoleTrayApp.websocket_server.stop()
+ ConsoleTrayApp.process.kill()
+ ConsoleTrayApp.process.wait()
+ if self.timer:
+ self.timer.stop()
+ QtCore.QCoreApplication.exit()
+
+ def catch_std_outputs(self):
+ """Redirects standard out and error to own functions"""
+ if not sys.stdout:
+ self.dialog.append_text("Cannot read from stdout!")
+ else:
+ self.original_stdout_write = sys.stdout.write
+ sys.stdout.write = self.my_stdout_write
+
+ if not sys.stderr:
+ self.dialog.append_text("Cannot read from stderr!")
+ else:
+ self.original_stderr_write = sys.stderr.write
+ sys.stderr.write = self.my_stderr_write
+
+ 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)
+ self.new_text.append(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)
+ self.new_text.append(text)
+
+ @staticmethod
+ def _multiple_replace(text, adict):
+ """Replace multiple tokens defined in dict.
+
+ Find and replace all occurances 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 = ConsoleTrayApp._multiple_replace(message,
+ ConsoleTrayApp.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(
+ ConsoleTrayApp.color(text))
diff --git a/repos/avalon-core b/repos/avalon-core
index 0d9a228fdb..e9882d0fff 160000
--- a/repos/avalon-core
+++ b/repos/avalon-core
@@ -1 +1 @@
-Subproject commit 0d9a228fdb2eb08fe6caa30f25fe2a34fead1a03
+Subproject commit e9882d0ffff27fed03a03459f496c29da0310cd2