diff --git a/client/ayon_core/hooks/pre_non_python_host_launch.py b/client/ayon_core/hooks/pre_non_python_host_launch.py deleted file mode 100644 index fed4c99447..0000000000 --- a/client/ayon_core/hooks/pre_non_python_host_launch.py +++ /dev/null @@ -1,58 +0,0 @@ -import os - -from ayon_core.lib import get_ayon_launcher_args -from ayon_core.lib.applications import ( - get_non_python_host_kwargs, - PreLaunchHook, - LaunchTypes, -) - -from ayon_core import AYON_CORE_ROOT - - -class NonPythonHostHook(PreLaunchHook): - """Launch arguments preparation. - - Non python host implementation do not launch host directly but use - python script which launch the host. For these cases it is necessary to - prepend python (or ayon) executable and script path before application's. - """ - app_groups = {"harmony", "photoshop", "aftereffects"} - - order = 20 - launch_types = {LaunchTypes.local} - - def execute(self): - # Pop executable - executable_path = self.launch_context.launch_args.pop(0) - - # Pop rest of launch arguments - There should not be other arguments! - remainders = [] - while self.launch_context.launch_args: - remainders.append(self.launch_context.launch_args.pop(0)) - - script_path = os.path.join( - AYON_CORE_ROOT, - "scripts", - "non_python_host_launch.py" - ) - - new_launch_args = get_ayon_launcher_args( - "run", script_path, executable_path - ) - # Add workfile path if exists - workfile_path = self.data["last_workfile_path"] - if ( - self.data.get("start_last_workfile") - and workfile_path - and os.path.exists(workfile_path)): - new_launch_args.append(workfile_path) - - # Append as whole list as these areguments should not be separated - self.launch_context.launch_args.append(new_launch_args) - - if remainders: - self.launch_context.launch_args.extend(remainders) - - self.launch_context.kwargs = \ - get_non_python_host_kwargs(self.launch_context.kwargs) diff --git a/client/ayon_core/hosts/aftereffects/__init__.py b/client/ayon_core/hosts/aftereffects/__init__.py index ae750d05b6..02ab287629 100644 --- a/client/ayon_core/hosts/aftereffects/__init__.py +++ b/client/ayon_core/hosts/aftereffects/__init__.py @@ -1,6 +1,12 @@ -from .addon import AfterEffectsAddon +from .addon import ( + AFTEREFFECTS_ADDON_ROOT, + AfterEffectsAddon, + get_launch_script_path, +) __all__ = ( + "AFTEREFFECTS_ADDON_ROOT", "AfterEffectsAddon", + "get_launch_script_path", ) diff --git a/client/ayon_core/hosts/aftereffects/addon.py b/client/ayon_core/hosts/aftereffects/addon.py index 46d0818247..fc54043c1d 100644 --- a/client/ayon_core/hosts/aftereffects/addon.py +++ b/client/ayon_core/hosts/aftereffects/addon.py @@ -1,5 +1,9 @@ +import os + from ayon_core.addon import AYONAddon, IHostAddon +AFTEREFFECTS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) + class AfterEffectsAddon(AYONAddon, IHostAddon): name = "aftereffects" @@ -17,3 +21,16 @@ class AfterEffectsAddon(AYONAddon, IHostAddon): def get_workfile_extensions(self): return [".aep"] + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(AFTEREFFECTS_ADDON_ROOT, "hooks") + ] + + +def get_launch_script_path(): + return os.path.join( + AFTEREFFECTS_ADDON_ROOT, "api", "launch_script.py" + ) diff --git a/client/ayon_core/hosts/aftereffects/api/launch_logic.py b/client/ayon_core/hosts/aftereffects/api/launch_logic.py index d0e4e8beae..5a23f2cb35 100644 --- a/client/ayon_core/hosts/aftereffects/api/launch_logic.py +++ b/client/ayon_core/hosts/aftereffects/api/launch_logic.py @@ -7,7 +7,6 @@ import asyncio import functools import traceback - from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync diff --git a/client/ayon_core/scripts/non_python_host_launch.py b/client/ayon_core/hosts/aftereffects/api/launch_script.py similarity index 77% rename from client/ayon_core/scripts/non_python_host_launch.py rename to client/ayon_core/hosts/aftereffects/api/launch_script.py index 4c18fd0ccc..87926c022b 100644 --- a/client/ayon_core/scripts/non_python_host_launch.py +++ b/client/ayon_core/hosts/aftereffects/api/launch_script.py @@ -1,4 +1,4 @@ -"""Script wraps launch mechanism of non python host implementations. +"""Script wraps launch mechanism of AfterEffects implementations. Arguments passed to the script are passed to launch function in host implementation. In all cases requires host app executable and may contain @@ -8,6 +8,8 @@ workfile or others. import os import sys +from ayon_core.hosts.aftereffects.api.launch_logic import main as host_main + # Get current file to locate start point of sys.argv CURRENT_FILE = os.path.abspath(__file__) @@ -79,26 +81,9 @@ def main(argv): if after_script_idx is not None: launch_args = sys_args[after_script_idx:] - host_name = os.environ["AYON_HOST_NAME"].lower() - if host_name == "photoshop": - # TODO refactor launch logic according to AE - from ayon_core.hosts.photoshop.api.lib import main - elif host_name == "aftereffects": - from ayon_core.hosts.aftereffects.api.launch_logic import main - elif host_name == "harmony": - from ayon_core.hosts.harmony.api.lib import main - else: - title = "Unknown host name" - message = ( - "BUG: Environment variable AYON_HOST_NAME contains unknown" - " host name \"{}\"" - ).format(host_name) - show_error_messagebox(title, message) - return - if launch_args: # Launch host implementation - main(*launch_args) + host_main(*launch_args) else: # Show message box on_invalid_args(after_script_idx is None) diff --git a/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py new file mode 100644 index 0000000000..979d9ff3e5 --- /dev/null +++ b/client/ayon_core/hosts/aftereffects/hooks/pre_launch_args.py @@ -0,0 +1,91 @@ +import os +import platform +import subprocess + +from ayon_core.lib import ( + get_ayon_launcher_args, + is_using_ayon_console, +) +from ayon_core.lib.applications import ( + PreLaunchHook, + LaunchTypes, +) +from ayon_core.hosts.aftereffects import get_launch_script_path + + +def get_launch_kwargs(kwargs): + """Explicit setting of kwargs for Popen for AfterEffects. + + Expected behavior + - ayon_console opens window with logs + - ayon has stdout/stderr available for capturing + + Args: + kwargs (Union[dict, None]): Current kwargs or None. + + """ + if kwargs is None: + kwargs = {} + + if platform.system().lower() != "windows": + return kwargs + + if is_using_ayon_console(): + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + else: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + return kwargs + + +class AEPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to AE implementation before + AE executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + app_groups = {"aftereffects"} + + order = 20 + launch_types = {LaunchTypes.local} + + def execute(self): + # Pop executable + executable_path = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + script_path = get_launch_script_path() + + new_launch_args = get_ayon_launcher_args( + "run", script_path, executable_path + ) + # Add workfile path if exists + workfile_path = self.data["last_workfile_path"] + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path) + ): + new_launch_args.append(workfile_path) + + # Append as whole list as these arguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs = get_launch_kwargs( + self.launch_context.kwargs + ) diff --git a/client/ayon_core/hosts/harmony/__init__.py b/client/ayon_core/hosts/harmony/__init__.py index 9177eaa285..6454d6f9d7 100644 --- a/client/ayon_core/hosts/harmony/__init__.py +++ b/client/ayon_core/hosts/harmony/__init__.py @@ -1,10 +1,12 @@ from .addon import ( - HARMONY_HOST_DIR, + HARMONY_ADDON_ROOT, HarmonyAddon, + get_launch_script_path, ) __all__ = ( - "HARMONY_HOST_DIR", + "HARMONY_ADDON_ROOT", "HarmonyAddon", + "get_launch_script_path", ) diff --git a/client/ayon_core/hosts/harmony/addon.py b/client/ayon_core/hosts/harmony/addon.py index 476d569415..1915a7eb6f 100644 --- a/client/ayon_core/hosts/harmony/addon.py +++ b/client/ayon_core/hosts/harmony/addon.py @@ -1,7 +1,7 @@ import os from ayon_core.addon import AYONAddon, IHostAddon -HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) +HARMONY_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) class HarmonyAddon(AYONAddon, IHostAddon): @@ -11,10 +11,23 @@ class HarmonyAddon(AYONAddon, IHostAddon): def add_implementation_envs(self, env, _app): """Modify environments to contain all required for implementation.""" openharmony_path = os.path.join( - HARMONY_HOST_DIR, "vendor", "OpenHarmony" + HARMONY_ADDON_ROOT, "vendor", "OpenHarmony" ) # TODO check if is already set? What to do if is already set? env["LIB_OPENHARMONY_PATH"] = openharmony_path def get_workfile_extensions(self): return [".zip"] + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(HARMONY_ADDON_ROOT, "hooks") + ] + + +def get_launch_script_path(): + return os.path.join( + HARMONY_ADDON_ROOT, "api", "launch_script.py" + ) diff --git a/client/ayon_core/hosts/harmony/api/launch_script.py b/client/ayon_core/hosts/harmony/api/launch_script.py new file mode 100644 index 0000000000..3c809e210f --- /dev/null +++ b/client/ayon_core/hosts/harmony/api/launch_script.py @@ -0,0 +1,93 @@ +"""Script wraps launch mechanism of Harmony implementations. + +Arguments passed to the script are passed to launch function in host +implementation. In all cases requires host app executable and may contain +workfile or others. +""" + +import os +import sys + +from ayon_core.hosts.harmony.api.lib import main as host_main + +# Get current file to locate start point of sys.argv +CURRENT_FILE = os.path.abspath(__file__) + + +def show_error_messagebox(title, message, detail_message=None): + """Function will show message and process ends after closing it.""" + from qtpy import QtWidgets, QtCore + from ayon_core import style + + app = QtWidgets.QApplication([]) + app.setStyleSheet(style.load_stylesheet()) + + msgbox = QtWidgets.QMessageBox() + msgbox.setWindowTitle(title) + msgbox.setText(message) + + if detail_message: + msgbox.setDetailedText(detail_message) + + msgbox.setWindowModality(QtCore.Qt.ApplicationModal) + msgbox.show() + + sys.exit(app.exec_()) + + +def on_invalid_args(script_not_found): + """Show to user message box saying that something went wrong. + + Tell user that arguments to launch implementation are invalid with + arguments details. + + Args: + script_not_found (bool): Use different message based on this value. + """ + + title = "Invalid arguments" + joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv) + if script_not_found: + submsg = "Where couldn't find script path:\n\"{}\"" + else: + submsg = "Expected Host executable after script path:\n\"{}\"" + + message = "BUG: Got invalid arguments so can't launch Host application." + detail_message = "Process was launched with arguments:\n{}\n\n{}".format( + joined_args, + submsg.format(CURRENT_FILE) + ) + + show_error_messagebox(title, message, detail_message) + + +def main(argv): + # Modify current file path to find match in sys.argv which may be different + # on windows (different letter cases and slashes). + modified_current_file = CURRENT_FILE.replace("\\", "/").lower() + + # Create a copy of sys argv + sys_args = list(argv) + after_script_idx = None + # Find script path in sys.argv to know index of argv where host + # executable should be. + for idx, item in enumerate(sys_args): + if item.replace("\\", "/").lower() == modified_current_file: + after_script_idx = idx + 1 + break + + # Validate that there is at least one argument after script path + launch_args = None + if after_script_idx is not None: + launch_args = sys_args[after_script_idx:] + + if launch_args: + # Launch host implementation + host_main(*launch_args) + else: + # Show message box + on_invalid_args(after_script_idx is None) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/client/ayon_core/hosts/harmony/api/lib.py b/client/ayon_core/hosts/harmony/api/lib.py index bc73e19066..3c833c7b69 100644 --- a/client/ayon_core/hosts/harmony/api/lib.py +++ b/client/ayon_core/hosts/harmony/api/lib.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Utility functions used for Avalon - Harmony integration.""" +import platform import subprocess import threading import os @@ -14,15 +15,16 @@ import json import signal import time from uuid import uuid4 -from qtpy import QtWidgets, QtCore, QtGui import collections -from .server import Server +from qtpy import QtWidgets, QtCore, QtGui +from ayon_core.lib import is_using_ayon_console from ayon_core.tools.stdout_broker.app import StdOutBroker from ayon_core.tools.utils import host_tools from ayon_core import style -from ayon_core.lib.applications import get_non_python_host_kwargs + +from .server import Server # Setup logging. log = logging.getLogger(__name__) @@ -324,7 +326,18 @@ def launch_zip_file(filepath): return print("Launching {}".format(scene_path)) - kwargs = get_non_python_host_kwargs({}, False) + # QUESTION Could we use 'run_detached_process' from 'ayon_core.lib'? + kwargs = {} + if ( + platform.system().lower() == "windows" + and not is_using_ayon_console() + ): + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + process = subprocess.Popen( [ProcessContext.application_path, scene_path], **kwargs diff --git a/client/ayon_core/hosts/harmony/api/pipeline.py b/client/ayon_core/hosts/harmony/api/pipeline.py index a753a32ebb..d842ccd414 100644 --- a/client/ayon_core/hosts/harmony/api/pipeline.py +++ b/client/ayon_core/hosts/harmony/api/pipeline.py @@ -15,13 +15,13 @@ from ayon_core.pipeline import ( from ayon_core.pipeline.load import get_outdated_containers from ayon_core.pipeline.context_tools import get_current_project_folder -from ayon_core.hosts.harmony import HARMONY_HOST_DIR +from ayon_core.hosts.harmony import HARMONY_ADDON_ROOT import ayon_core.hosts.harmony.api as harmony log = logging.getLogger("ayon_core.hosts.harmony") -PLUGINS_DIR = os.path.join(HARMONY_HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(HARMONY_ADDON_ROOT, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") diff --git a/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py new file mode 100644 index 0000000000..bbad14084a --- /dev/null +++ b/client/ayon_core/hosts/harmony/hooks/pre_launch_args.py @@ -0,0 +1,91 @@ +import os +import platform +import subprocess + +from ayon_core.lib import ( + get_ayon_launcher_args, + is_using_ayon_console, +) +from ayon_core.lib.applications import ( + PreLaunchHook, + LaunchTypes, +) +from ayon_core.hosts.harmony import get_launch_script_path + + +def get_launch_kwargs(kwargs): + """Explicit setting of kwargs for Popen for Harmony. + + Expected behavior + - ayon_console opens window with logs + - ayon has stdout/stderr available for capturing + + Args: + kwargs (Union[dict, None]): Current kwargs or None. + + """ + if kwargs is None: + kwargs = {} + + if platform.system().lower() != "windows": + return kwargs + + if is_using_ayon_console(): + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + else: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + return kwargs + + +class HarmonyPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to Harmony implementation + before Harmony executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + app_groups = {"harmony"} + + order = 20 + launch_types = {LaunchTypes.local} + + def execute(self): + # Pop executable + executable_path = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + script_path = get_launch_script_path() + + new_launch_args = get_ayon_launcher_args( + "run", script_path, executable_path + ) + # Add workfile path if exists + workfile_path = self.data["last_workfile_path"] + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path) + ): + new_launch_args.append(workfile_path) + + # Append as whole list as these arguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs = get_launch_kwargs( + self.launch_context.kwargs + ) diff --git a/client/ayon_core/hosts/photoshop/__init__.py b/client/ayon_core/hosts/photoshop/__init__.py index 773f73d624..cf21b7df75 100644 --- a/client/ayon_core/hosts/photoshop/__init__.py +++ b/client/ayon_core/hosts/photoshop/__init__.py @@ -1,10 +1,12 @@ from .addon import ( + PHOTOSHOP_ADDON_ROOT, PhotoshopAddon, - PHOTOSHOP_HOST_DIR, + get_launch_script_path, ) __all__ = ( + "PHOTOSHOP_ADDON_ROOT", "PhotoshopAddon", - "PHOTOSHOP_HOST_DIR", + "get_launch_script_path", ) diff --git a/client/ayon_core/hosts/photoshop/addon.py b/client/ayon_core/hosts/photoshop/addon.py index 3016912960..65fe6a7cd1 100644 --- a/client/ayon_core/hosts/photoshop/addon.py +++ b/client/ayon_core/hosts/photoshop/addon.py @@ -1,7 +1,7 @@ import os from ayon_core.addon import AYONAddon, IHostAddon -PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) +PHOTOSHOP_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__)) class PhotoshopAddon(AYONAddon, IHostAddon): @@ -20,3 +20,17 @@ class PhotoshopAddon(AYONAddon, IHostAddon): def get_workfile_extensions(self): return [".psd", ".psb"] + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(PHOTOSHOP_ADDON_ROOT, "hooks") + ] + + +def get_launch_script_path(): + return os.path.join( + PHOTOSHOP_ADDON_ROOT, "api", "launch_script.py" + ) + diff --git a/client/ayon_core/hosts/photoshop/api/launch_script.py b/client/ayon_core/hosts/photoshop/api/launch_script.py new file mode 100644 index 0000000000..bb4de80086 --- /dev/null +++ b/client/ayon_core/hosts/photoshop/api/launch_script.py @@ -0,0 +1,93 @@ +"""Script wraps launch mechanism of Photoshop implementations. + +Arguments passed to the script are passed to launch function in host +implementation. In all cases requires host app executable and may contain +workfile or others. +""" + +import os +import sys + +from ayon_core.hosts.photoshop.api.lib import main as host_main + +# Get current file to locate start point of sys.argv +CURRENT_FILE = os.path.abspath(__file__) + + +def show_error_messagebox(title, message, detail_message=None): + """Function will show message and process ends after closing it.""" + from qtpy import QtWidgets, QtCore + from ayon_core import style + + app = QtWidgets.QApplication([]) + app.setStyleSheet(style.load_stylesheet()) + + msgbox = QtWidgets.QMessageBox() + msgbox.setWindowTitle(title) + msgbox.setText(message) + + if detail_message: + msgbox.setDetailedText(detail_message) + + msgbox.setWindowModality(QtCore.Qt.ApplicationModal) + msgbox.show() + + sys.exit(app.exec_()) + + +def on_invalid_args(script_not_found): + """Show to user message box saying that something went wrong. + + Tell user that arguments to launch implementation are invalid with + arguments details. + + Args: + script_not_found (bool): Use different message based on this value. + """ + + title = "Invalid arguments" + joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv) + if script_not_found: + submsg = "Where couldn't find script path:\n\"{}\"" + else: + submsg = "Expected Host executable after script path:\n\"{}\"" + + message = "BUG: Got invalid arguments so can't launch Host application." + detail_message = "Process was launched with arguments:\n{}\n\n{}".format( + joined_args, + submsg.format(CURRENT_FILE) + ) + + show_error_messagebox(title, message, detail_message) + + +def main(argv): + # Modify current file path to find match in sys.argv which may be different + # on windows (different letter cases and slashes). + modified_current_file = CURRENT_FILE.replace("\\", "/").lower() + + # Create a copy of sys argv + sys_args = list(argv) + after_script_idx = None + # Find script path in sys.argv to know index of argv where host + # executable should be. + for idx, item in enumerate(sys_args): + if item.replace("\\", "/").lower() == modified_current_file: + after_script_idx = idx + 1 + break + + # Validate that there is at least one argument after script path + launch_args = None + if after_script_idx is not None: + launch_args = sys_args[after_script_idx:] + + if launch_args: + # Launch host implementation + host_main(*launch_args) + else: + # Show message box + on_invalid_args(after_script_idx is None) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/client/ayon_core/hosts/photoshop/api/pipeline.py b/client/ayon_core/hosts/photoshop/api/pipeline.py index 32f66cf7fb..27cfa5a7b5 100644 --- a/client/ayon_core/hosts/photoshop/api/pipeline.py +++ b/client/ayon_core/hosts/photoshop/api/pipeline.py @@ -21,14 +21,14 @@ from ayon_core.host import ( ) from ayon_core.pipeline.load import any_outdated_containers -from ayon_core.hosts.photoshop import PHOTOSHOP_HOST_DIR +from ayon_core.hosts.photoshop import PHOTOSHOP_ADDON_ROOT from ayon_core.tools.utils import get_ayon_qt_app from . import lib log = Logger.get_logger(__name__) -PLUGINS_DIR = os.path.join(PHOTOSHOP_HOST_DIR, "plugins") +PLUGINS_DIR = os.path.join(PHOTOSHOP_ADDON_ROOT, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") diff --git a/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py new file mode 100644 index 0000000000..8358c11ca1 --- /dev/null +++ b/client/ayon_core/hosts/photoshop/hooks/pre_launch_args.py @@ -0,0 +1,91 @@ +import os +import platform +import subprocess + +from ayon_core.lib import ( + get_ayon_launcher_args, + is_using_ayon_console, +) +from ayon_core.lib.applications import ( + PreLaunchHook, + LaunchTypes, +) +from ayon_core.hosts.photoshop import get_launch_script_path + + +def get_launch_kwargs(kwargs): + """Explicit setting of kwargs for Popen for Photoshop. + + Expected behavior + - ayon_console opens window with logs + - ayon has stdout/stderr available for capturing + + Args: + kwargs (Union[dict, None]): Current kwargs or None. + + """ + if kwargs is None: + kwargs = {} + + if platform.system().lower() != "windows": + return kwargs + + if not is_using_ayon_console(): + kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE + }) + else: + kwargs.update({ + "creationflags": subprocess.CREATE_NO_WINDOW, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL + }) + return kwargs + + +class PhotoshopPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to Photoshop implementation + before Photoshop executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + app_groups = {"photoshop"} + + order = 20 + launch_types = {LaunchTypes.local} + + def execute(self): + # Pop executable + executable_path = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + script_path = get_launch_script_path() + + new_launch_args = get_ayon_launcher_args( + "run", script_path, executable_path + ) + # Add workfile path if exists + workfile_path = self.data["last_workfile_path"] + if ( + self.data.get("start_last_workfile") + and workfile_path + and os.path.exists(workfile_path) + ): + new_launch_args.append(workfile_path) + + # Append as whole list as these arguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.launch_context.launch_args.extend(remainders) + + self.launch_context.kwargs = get_launch_kwargs( + self.launch_context.kwargs + ) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index d23d807b32..2ee7eecfe3 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -152,6 +152,7 @@ from .path_tools import ( from .ayon_info import ( is_running_from_build, + is_using_ayon_console, is_staging_enabled, is_dev_mode_enabled, is_in_tests, @@ -269,6 +270,7 @@ __all__ = [ "Logger", "is_running_from_build", + "is_using_ayon_console", "is_staging_enabled", "is_dev_mode_enabled", "is_in_tests", diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 3a6039357c..2db32cbfaa 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -1891,42 +1891,3 @@ def _prepare_last_workfile(data, workdir, addons_manager): data["env"]["AYON_LAST_WORKFILE"] = last_workfile_path data["last_workfile_path"] = last_workfile_path - - -def get_non_python_host_kwargs(kwargs, allow_console=True): - """Explicit setting of kwargs for Popen for AE/PS/Harmony. - - Expected behavior - - ayon_console opens window with logs - - ayon has stdout/stderr available for capturing - - Args: - kwargs (dict) or None - allow_console (bool): use False for inner Popen opening app itself or - it will open additional console (at least for Harmony) - """ - - if kwargs is None: - kwargs = {} - - if platform.system().lower() != "windows": - return kwargs - - executable_path = os.environ.get("AYON_EXECUTABLE") - - executable_filename = "" - if executable_path: - executable_filename = os.path.basename(executable_path) - - is_gui_executable = "ayon_console" not in executable_filename - if is_gui_executable: - kwargs.update({ - "creationflags": subprocess.CREATE_NO_WINDOW, - "stdout": subprocess.DEVNULL, - "stderr": subprocess.DEVNULL - }) - elif allow_console: - kwargs.update({ - "creationflags": subprocess.CREATE_NEW_CONSOLE - }) - return kwargs diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index 3975b35bc3..fc09a7c90c 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -10,6 +10,12 @@ from .local_settings import get_local_site_id def get_ayon_launcher_version(): + """Get AYON launcher version. + + Returns: + str: Version string. + + """ version_filepath = os.path.join(os.environ["AYON_ROOT"], "version.py") if not os.path.exists(version_filepath): return None @@ -24,8 +30,8 @@ def is_running_from_build(): Returns: bool: True if running from build. - """ + """ executable_path = os.environ["AYON_EXECUTABLE"] executable_filename = os.path.basename(executable_path) if "python" in executable_filename.lower(): @@ -33,6 +39,32 @@ def is_running_from_build(): return True +def is_using_ayon_console(): + """AYON launcher console executable is used. + + This function make sense only on Windows platform. For other platforms + always returns True. True is also returned if process is running from + code. + + AYON launcher on windows has 2 executable files. First 'ayon_console.exe' + works as 'python.exe' executable, the second 'ayon.exe' works as + 'pythonw.exe' executable. The difference is way how stdout/stderr is + handled (especially when calling subprocess). + + Returns: + bool: True if console executable is used. + + """ + if ( + platform.system().lower() != "windows" + or is_running_from_build() + ): + return True + executable_path = os.environ["AYON_EXECUTABLE"] + executable_filename = os.path.basename(executable_path) + return "ayon_console" in executable_filename + + def is_staging_enabled(): return os.getenv("AYON_USE_STAGING") == "1"