mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
* set 'QT_SCALE_FACTOR_ROUNDING_POLICY' to 'PassThrough' * implemented 'get_openpype_qt_app' which set all openpype related attributes * implemented get app functions in igniter and ayon common * removed env varaibles 'QT_SCALE_FACTOR_ROUNDING_POLICY' * formatting fixes * fix line length * fix args
389 lines
11 KiB
Python
389 lines
11 KiB
Python
import os
|
|
import sys
|
|
import subprocess
|
|
import collections
|
|
import logging
|
|
import asyncio
|
|
import functools
|
|
import traceback
|
|
|
|
|
|
from wsrpc_aiohttp import (
|
|
WebSocketRoute,
|
|
WebSocketAsync
|
|
)
|
|
|
|
from qtpy import QtCore
|
|
|
|
from openpype.lib import Logger
|
|
from openpype.tests.lib import is_in_tests
|
|
from openpype.pipeline import install_host, legacy_io
|
|
from openpype.modules import ModulesManager
|
|
from openpype.tools.utils import host_tools, get_openpype_qt_app
|
|
from openpype.tools.adobe_webserver.app import WebServerTool
|
|
|
|
from .ws_stub import get_stub
|
|
from .lib import set_settings
|
|
|
|
log = logging.getLogger(__name__)
|
|
log.setLevel(logging.DEBUG)
|
|
|
|
|
|
def safe_excepthook(*args):
|
|
traceback.print_exception(*args)
|
|
|
|
|
|
def main(*subprocess_args):
|
|
"""Main entrypoint to AE launching, called from pre hook."""
|
|
sys.excepthook = safe_excepthook
|
|
|
|
from openpype.hosts.aftereffects.api import AfterEffectsHost
|
|
|
|
host = AfterEffectsHost()
|
|
install_host(host)
|
|
|
|
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
|
|
app = get_openpype_qt_app()
|
|
app.setQuitOnLastWindowClosed(False)
|
|
|
|
launcher = ProcessLauncher(subprocess_args)
|
|
launcher.start()
|
|
|
|
if os.environ.get("HEADLESS_PUBLISH"):
|
|
manager = ModulesManager()
|
|
webpublisher_addon = manager["webpublisher"]
|
|
|
|
launcher.execute_in_main_thread(
|
|
functools.partial(
|
|
webpublisher_addon.headless_publish,
|
|
log,
|
|
"CloseAE",
|
|
is_in_tests()
|
|
)
|
|
)
|
|
|
|
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
|
|
save = False
|
|
if os.getenv("WORKFILES_SAVE_AS"):
|
|
save = True
|
|
|
|
launcher.execute_in_main_thread(
|
|
lambda: host_tools.show_tool_by_name("workfiles", save=save)
|
|
)
|
|
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
def show_tool_by_name(tool_name):
|
|
kwargs = {}
|
|
if tool_name == "loader":
|
|
kwargs["use_context"] = True
|
|
|
|
host_tools.show_tool_by_name(tool_name, **kwargs)
|
|
|
|
|
|
class ProcessLauncher(QtCore.QObject):
|
|
"""Launches webserver, connects to it, runs main thread."""
|
|
route_name = "AfterEffects"
|
|
_main_thread_callbacks = collections.deque()
|
|
|
|
def __init__(self, subprocess_args):
|
|
self._subprocess_args = subprocess_args
|
|
self._log = None
|
|
|
|
super(ProcessLauncher, self).__init__()
|
|
|
|
# Keep track if launcher was alreadu started
|
|
self._started = False
|
|
|
|
self._process = None
|
|
self._websocket_server = None
|
|
|
|
start_process_timer = QtCore.QTimer()
|
|
start_process_timer.setInterval(100)
|
|
|
|
loop_timer = QtCore.QTimer()
|
|
loop_timer.setInterval(200)
|
|
|
|
start_process_timer.timeout.connect(self._on_start_process_timer)
|
|
loop_timer.timeout.connect(self._on_loop_timer)
|
|
|
|
self._start_process_timer = start_process_timer
|
|
self._loop_timer = loop_timer
|
|
|
|
@property
|
|
def log(self):
|
|
if self._log is None:
|
|
self._log = Logger.get_logger("{}-launcher".format(
|
|
self.route_name))
|
|
return self._log
|
|
|
|
@property
|
|
def websocket_server_is_running(self):
|
|
if self._websocket_server is not None:
|
|
return self._websocket_server.is_running
|
|
return False
|
|
|
|
@property
|
|
def is_process_running(self):
|
|
if self._process is not None:
|
|
return self._process.poll() is None
|
|
return False
|
|
|
|
@property
|
|
def is_host_connected(self):
|
|
"""Returns True if connected, False if app is not running at all."""
|
|
if not self.is_process_running:
|
|
return False
|
|
|
|
try:
|
|
|
|
_stub = get_stub()
|
|
if _stub:
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
return None
|
|
|
|
@classmethod
|
|
def execute_in_main_thread(cls, callback):
|
|
cls._main_thread_callbacks.append(callback)
|
|
|
|
def start(self):
|
|
if self._started:
|
|
return
|
|
self.log.info("Started launch logic of AfterEffects")
|
|
self._started = True
|
|
self._start_process_timer.start()
|
|
|
|
def exit(self):
|
|
""" Exit whole application. """
|
|
if self._start_process_timer.isActive():
|
|
self._start_process_timer.stop()
|
|
if self._loop_timer.isActive():
|
|
self._loop_timer.stop()
|
|
|
|
if self._websocket_server is not None:
|
|
self._websocket_server.stop()
|
|
|
|
if self._process:
|
|
self._process.kill()
|
|
self._process.wait()
|
|
|
|
QtCore.QCoreApplication.exit()
|
|
|
|
def _on_loop_timer(self):
|
|
# TODO find better way and catch errors
|
|
# Run only callbacks that are in queue at the moment
|
|
cls = self.__class__
|
|
for _ in range(len(cls._main_thread_callbacks)):
|
|
if cls._main_thread_callbacks:
|
|
callback = cls._main_thread_callbacks.popleft()
|
|
callback()
|
|
|
|
if not self.is_process_running:
|
|
self.log.info("Host process is not running. Closing")
|
|
self.exit()
|
|
|
|
elif not self.websocket_server_is_running:
|
|
self.log.info("Websocket server is not running. Closing")
|
|
self.exit()
|
|
|
|
def _on_start_process_timer(self):
|
|
# TODO add try except validations for each part in this method
|
|
# Start server as first thing
|
|
if self._websocket_server is None:
|
|
self._init_server()
|
|
return
|
|
|
|
# TODO add waiting time
|
|
# Wait for webserver
|
|
if not self.websocket_server_is_running:
|
|
return
|
|
|
|
# Start application process
|
|
if self._process is None:
|
|
self._start_process()
|
|
self.log.info("Waiting for host to connect")
|
|
return
|
|
|
|
# TODO add waiting time
|
|
# Wait until host is connected
|
|
if self.is_host_connected:
|
|
self._start_process_timer.stop()
|
|
self._loop_timer.start()
|
|
elif (
|
|
not self.is_process_running
|
|
or not self.websocket_server_is_running
|
|
):
|
|
self.exit()
|
|
|
|
def _init_server(self):
|
|
if self._websocket_server is not None:
|
|
return
|
|
|
|
self.log.debug(
|
|
"Initialization of websocket server for host communication"
|
|
)
|
|
|
|
self._websocket_server = websocket_server = WebServerTool()
|
|
if websocket_server.port_occupied(
|
|
websocket_server.host_name,
|
|
websocket_server.port
|
|
):
|
|
self.log.info(
|
|
"Server already running, sending actual context and exit."
|
|
)
|
|
asyncio.run(websocket_server.send_context_change(self.route_name))
|
|
self.exit()
|
|
return
|
|
|
|
# Add Websocket route
|
|
websocket_server.add_route("*", "/ws/", WebSocketAsync)
|
|
# Add after effects route to websocket handler
|
|
|
|
print("Adding {} route".format(self.route_name))
|
|
WebSocketAsync.add_route(
|
|
self.route_name, AfterEffectsRoute
|
|
)
|
|
self.log.info("Starting websocket server for host communication")
|
|
websocket_server.start_server()
|
|
|
|
def _start_process(self):
|
|
if self._process is not None:
|
|
return
|
|
self.log.info("Starting host process")
|
|
try:
|
|
self._process = subprocess.Popen(
|
|
self._subprocess_args,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL
|
|
)
|
|
except Exception:
|
|
self.log.info("exce", exc_info=True)
|
|
self.exit()
|
|
|
|
|
|
class AfterEffectsRoute(WebSocketRoute):
|
|
"""
|
|
One route, mimicking external application (like Harmony, etc).
|
|
All functions could be called from client.
|
|
'do_notify' function calls function on the client - mimicking
|
|
notification after long running job on the server or similar
|
|
"""
|
|
instance = None
|
|
|
|
def init(self, **kwargs):
|
|
# Python __init__ must be return "self".
|
|
# This method might return anything.
|
|
log.debug("someone called AfterEffects route")
|
|
self.instance = self
|
|
return kwargs
|
|
|
|
# server functions
|
|
async def ping(self):
|
|
log.debug("someone called AfterEffects route ping")
|
|
|
|
# This method calls function on the client side
|
|
# client functions
|
|
async def set_context(self, project, asset, task):
|
|
"""
|
|
Sets 'project' and 'asset' to envs, eg. setting context
|
|
|
|
Args:
|
|
project (str)
|
|
asset (str)
|
|
"""
|
|
log.info("Setting context change")
|
|
log.info("project {} asset {} ".format(project, asset))
|
|
if project:
|
|
legacy_io.Session["AVALON_PROJECT"] = project
|
|
os.environ["AVALON_PROJECT"] = project
|
|
if asset:
|
|
legacy_io.Session["AVALON_ASSET"] = asset
|
|
os.environ["AVALON_ASSET"] = asset
|
|
if task:
|
|
legacy_io.Session["AVALON_TASK"] = task
|
|
os.environ["AVALON_TASK"] = task
|
|
|
|
async def read(self):
|
|
log.debug("aftereffects.read client calls server server calls "
|
|
"aftereffects client")
|
|
return await self.socket.call('aftereffects.read')
|
|
|
|
# panel routes for tools
|
|
async def workfiles_route(self):
|
|
self._tool_route("workfiles")
|
|
|
|
async def loader_route(self):
|
|
self._tool_route("loader")
|
|
|
|
async def publish_route(self):
|
|
self._tool_route("publisher")
|
|
|
|
async def sceneinventory_route(self):
|
|
self._tool_route("sceneinventory")
|
|
|
|
async def setresolution_route(self):
|
|
self._settings_route(False, True)
|
|
|
|
async def setframes_route(self):
|
|
self._settings_route(True, False)
|
|
|
|
async def setall_route(self):
|
|
self._settings_route(True, True)
|
|
|
|
async def experimental_tools_route(self):
|
|
self._tool_route("experimental_tools")
|
|
|
|
def _tool_route(self, _tool_name):
|
|
"""The address accessed when clicking on the buttons."""
|
|
|
|
partial_method = functools.partial(show_tool_by_name,
|
|
_tool_name)
|
|
|
|
ProcessLauncher.execute_in_main_thread(partial_method)
|
|
|
|
# Required return statement.
|
|
return "nothing"
|
|
|
|
def _settings_route(self, frames, resolution):
|
|
partial_method = functools.partial(set_settings,
|
|
frames,
|
|
resolution)
|
|
|
|
ProcessLauncher.execute_in_main_thread(partial_method)
|
|
|
|
# Required return statement.
|
|
return "nothing"
|
|
|
|
def create_placeholder_route(self):
|
|
from openpype.hosts.aftereffects.api.workfile_template_builder import \
|
|
create_placeholder
|
|
partial_method = functools.partial(create_placeholder)
|
|
|
|
ProcessLauncher.execute_in_main_thread(partial_method)
|
|
|
|
# Required return statement.
|
|
return "nothing"
|
|
|
|
def update_placeholder_route(self):
|
|
from openpype.hosts.aftereffects.api.workfile_template_builder import \
|
|
update_placeholder
|
|
partial_method = functools.partial(update_placeholder)
|
|
|
|
ProcessLauncher.execute_in_main_thread(partial_method)
|
|
|
|
# Required return statement.
|
|
return "nothing"
|
|
|
|
def build_workfile_template_route(self):
|
|
from openpype.hosts.aftereffects.api.workfile_template_builder import \
|
|
build_workfile_template
|
|
partial_method = functools.partial(build_workfile_template)
|
|
|
|
ProcessLauncher.execute_in_main_thread(partial_method)
|
|
|
|
# Required return statement.
|
|
return "nothing"
|