This repository has been archived by the owner on Sep 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 129
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
03e555c
commit ed9f3b5
Showing
9 changed files
with
1,942 additions
and
943 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
"""Helper functions or classes for Webserver module. | ||
These must not be imported in module itself to not break Python 2 | ||
applications. | ||
""" | ||
|
||
import inspect | ||
from aiohttp.http_exceptions import HttpBadRequest | ||
from aiohttp.web_exceptions import HTTPMethodNotAllowed | ||
from aiohttp.web_request import Request | ||
|
||
|
||
DEFAULT_METHODS = ("GET", "POST", "PUT", "DELETE") | ||
|
||
|
||
class RestApiEndpoint: | ||
"""Helper endpoint class for single endpoint. | ||
Class can define `get`, `post`, `put` or `delete` async methods for the | ||
endpoint. | ||
""" | ||
def __init__(self): | ||
methods = {} | ||
|
||
for method_name in DEFAULT_METHODS: | ||
method = getattr(self, method_name.lower(), None) | ||
if method: | ||
methods[method_name.upper()] = method | ||
|
||
self.methods = methods | ||
|
||
async def dispatch(self, request: Request): | ||
method = self.methods.get(request.method.upper()) | ||
if not method: | ||
raise HTTPMethodNotAllowed("", DEFAULT_METHODS) | ||
|
||
wanted_args = list(inspect.signature(method).parameters.keys()) | ||
|
||
available_args = request.match_info.copy() | ||
available_args["request"] = request | ||
|
||
unsatisfied_args = set(wanted_args) - set(available_args.keys()) | ||
if unsatisfied_args: | ||
# Expected match info that doesn't exist | ||
raise HttpBadRequest("") | ||
|
||
return await method(**{ | ||
arg_name: available_args[arg_name] | ||
for arg_name in wanted_args | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import aiohttp | ||
from aiohttp import web | ||
import json | ||
import logging | ||
from concurrent.futures import CancelledError | ||
from qtpy import QtWidgets | ||
|
||
from openpype.modules import ITrayService | ||
|
||
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_api_route('*', "/ws/host_listener", self.websocket_handler) | ||
|
||
def _host_is_connecting(self, host_name, label): | ||
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) | ||
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 |
Oops, something went wrong.