From 7bef86ac79a246487437e50e6c2f2252491f7bf4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 15 Jul 2024 12:55:57 +0200 Subject: [PATCH 001/144] Enable Validate Outdated Containers by default for Fusion --- server/settings/publish_plugins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 36bb3f7340..1ca487969f 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -964,7 +964,8 @@ DEFAULT_PUBLISH_VALUES = { "nuke", "harmony", "photoshop", - "aftereffects" + "aftereffects", + "fusion" ], "enabled": True, "optional": True, From ec419f556906c4e3a804b02b1c139cb82e1b91ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:12:26 +0200 Subject: [PATCH 002/144] added 'ensure_is_process_ready' method to addon base class --- client/ayon_core/addon/base.py | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index b9ecff4233..c85f38b32a 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -67,6 +67,56 @@ MOVED_ADDON_MILESTONE_VERSIONS = { } +class ProcessPreparationError(Exception): + """Exception that can be used when process preparation failed. + + The message is showed to user (either as UI dialog or printed). If + different error is raised a "generic" error message is showed to user + with option to copy error message to clipboard. + + """ + pass + + +class ProcessContext: + """Context of child process. + + Notes: + This class is used to pass context to child process. It can be used + to use different behavior of addon based on information in + the context. + The context can be enhanced in future versions. + + Args: + addon_name (Optional[str]): Addon name which triggered process. + addon_version (Optional[str]): Addon version which triggered process. + project_name (Optional[str]): Project name. Can be filled in case + process is triggered for specific project. Some addons can have + different behavior based on project. + headless (Optional[bool]): Is process running in headless mode. + + """ + def __init__( + self, + addon_name: Optional[str] = None, + addon_version: Optional[str] = None, + project_name: Optional[str] = None, + headless: Optional[bool] = None, + **kwargs, + ): + if headless is None: + # TODO use lib function to get headless mode + headless = os.getenv("AYON_HEADLESS_MODE") == "1" + self.addon_name: Optional[str] = addon_name + self.addon_version: Optional[str] = addon_version + self.project_name: Optional[str] = project_name + self.headless: bool = headless + + if kwargs: + unknown_keys = ", ".join([f'"{key}"' for key in kwargs.keys()]) + print(f"Unknown keys in ProcessContext: {unknown_keys}") + + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing AYON addons. @@ -588,7 +638,29 @@ class AYONAddon(object): Args: enabled_addons (list[AYONAddon]): Addons that are enabled. """ + pass + def ensure_is_process_ready( + self, process_context: ProcessContext + ): + """Make sure addon is prepared for a process. + + This method is called when some action makes sure that addon has set + necessary data. For example if user should be logged in + and filled credentials in environment variables this method should + ask user for credentials. + + Implementation of this method is optional. + + Note: + The logic can be similar to logic in tray, but in tray not require + to be logged in. + + Args: + process_context (ProcessContext): Context of child + process. + + """ pass def get_global_environments(self): From 9de1f236f16166f51fdfed836284cdf1fd0ed3ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:12:52 +0200 Subject: [PATCH 003/144] prepare utils to run process preparation --- client/ayon_core/addon/__init__.py | 8 +++ client/ayon_core/addon/utils.py | 112 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 client/ayon_core/addon/utils.py diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index fe8865c730..27af2955ed 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -9,12 +9,17 @@ from .interfaces import ( ) from .base import ( + ProcessPreparationError, AYONAddon, AddonsManager, TrayAddonsManager, load_addons, ) +from .utils import ( + ensure_addons_are_process_ready, +) + __all__ = ( "click_wrap", @@ -25,8 +30,11 @@ __all__ = ( "ITrayService", "IHostAddon", + "ProcessPreparationError", "AYONAddon", "AddonsManager", "TrayAddonsManager", "load_addons", + + "ensure_addons_are_process_ready", ) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py new file mode 100644 index 0000000000..092ce502af --- /dev/null +++ b/client/ayon_core/addon/utils.py @@ -0,0 +1,112 @@ +import os +import sys +import contextlib +import tempfile +import json +from io import StringIO +from typing import Optional + +from ayon_core.lib import run_ayon_launcher_process + +from .base import AddonsManager, ProcessContext, ProcessPreparationError + + +def _handle_error( + process_context: ProcessContext, + message: str, + detail: Optional[str], +): + """Handle error in process ready preparation. + + Shows UI to inform user about the error, or prints the message + to stdout if running in headless mode. + + Args: + process_context (ProcessContext): The context in which the + error occurred. + message (str): The message to show. + detail (Optional[str]): The detail message to show (usually + traceback). + + """ + if process_context.headless: + print(detail) + print(f"{10*'*'}\n{message}\n{10*'*'}") + return + + current_dir = os.path.dirname(os.path.abspath(__file__)) + script_path = os.path.join(current_dir, "ui", "process_ready_error.py") + with tempfile.NamedTemporaryFile("w", delete=False) as tmp: + tmp_path = tmp.name + + try: + with open(tmp_path, "w") as stream: + json.dump( + {"message": message, "detail": detail}, + stream + ) + + run_ayon_launcher_process( + "run", script_path, tmp_path + ) + + finally: + os.remove(tmp_path) + + +def ensure_addons_are_process_ready( + process_context: ProcessContext, + addons_manager: Optional[AddonsManager] = None, + exit_on_failure: bool = True, +): + """Ensure all enabled addons are ready to be used in the given context. + + Call this method only in AYON launcher process and as first thing + to avoid possible clashes with preparation. For example 'QApplication' + should not be created. + + Args: + process_context (ProcessContext): The context in which the + addons should be prepared. + addons_manager (Optional[AddonsManager]): The addons + manager to use. If not provided, a new one will be created. + exit_on_failure (bool, optional): If True, the process will exit + if an error occurs. Defaults to True. + + Returns: + Optional[Exception]: The exception that occurred during the + preparation, if any. + + """ + if addons_manager is None: + addons_manager = AddonsManager() + + exc = None + message = None + failed = False + use_detail = False + with contextlib.redirect_stdout(StringIO()) as stdout: + for addon in addons_manager.get_enabled_addons(): + failed = True + try: + addon.ensure_is_process_ready(process_context) + failed = False + except ProcessPreparationError as exc: + message = str(exc) + + except BaseException as exc: + message = "An unexpected error occurred." + use_detail = True + + if failed: + break + + if failed: + detail = None + if use_detail: + detail = stdout.get_value() + + _handle_error(process_context, message, detail) + if not exit_on_failure: + return exc + sys.exit(1) From 01983de888db8118437c6feb104b576cbffc362f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:13:31 +0200 Subject: [PATCH 004/144] implemented dialog showing error --- .../ayon_core/addon/ui/process_ready_error.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 client/ayon_core/addon/ui/process_ready_error.py diff --git a/client/ayon_core/addon/ui/process_ready_error.py b/client/ayon_core/addon/ui/process_ready_error.py new file mode 100644 index 0000000000..78e2b9b9bb --- /dev/null +++ b/client/ayon_core/addon/ui/process_ready_error.py @@ -0,0 +1,132 @@ +import sys +import json +from typing import Optional + +from qtpy import QtWidgets, QtCore + +from ayon_core.style import load_stylesheet +from ayon_core.tools.utils import get_ayon_qt_app + + +class DetailDialog(QtWidgets.QDialog): + def __init__(self, detail, parent): + super().__init__(parent) + + self.setWindowTitle("Detail") + + detail_input = QtWidgets.QPlainTextEdit(self) + detail_input.setPlainText(detail) + detail_input.setReadOnly(True) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(detail_input, 1) + + def showEvent(self, event): + self.resize(600, 400) + super().showEvent(event) + + +class ErrorDialog(QtWidgets.QDialog): + def __init__( + self, + message: str, + detail: Optional[str], + parent: Optional[QtWidgets.QWidget] = None + ): + super().__init__(parent) + + self.setWindowTitle("Preparation failed") + self.setWindowFlags( + self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint + ) + + message_label = QtWidgets.QLabel(self) + + detail_wrapper = QtWidgets.QWidget(self) + + detail_label = QtWidgets.QLabel(detail_wrapper) + + detail_layout = QtWidgets.QVBoxLayout(detail_wrapper) + detail_layout.setContentsMargins(0, 0, 0, 0) + detail_layout.addWidget(detail_label) + + btns_wrapper = QtWidgets.QWidget(self) + + copy_detail_btn = QtWidgets.QPushButton("Copy detail", btns_wrapper) + show_detail_btn = QtWidgets.QPushButton("Show detail", btns_wrapper) + confirm_btn = QtWidgets.QPushButton("Close", btns_wrapper) + + btns_layout = QtWidgets.QHBoxLayout(btns_wrapper) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(copy_detail_btn, 0) + btns_layout.addWidget(show_detail_btn, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(detail_wrapper, 1) + layout.addWidget(btns_wrapper, 0) + + copy_detail_btn.clicked.connect(self._on_copy_clicked) + show_detail_btn.clicked.connect(self._on_show_detail_clicked) + confirm_btn.clicked.connect(self._on_confirm_clicked) + + self._message_label = message_label + self._detail_wrapper = detail_wrapper + self._detail_label = detail_label + + self._copy_detail_btn = copy_detail_btn + self._show_detail_btn = show_detail_btn + self._confirm_btn = confirm_btn + + self._detail_dialog = None + + self._detail = detail + + self.set_message(message, detail) + + def showEvent(self, event): + self.setStyleSheet(load_stylesheet()) + self.resize(320, 140) + super().showEvent(event) + + def set_message(self, message, detail): + self._message_label.setText(message) + self._detail = detail + + for widget in ( + self._copy_detail_btn, + self._show_detail_btn, + ): + widget.setVisible(bool(detail)) + + def _on_copy_clicked(self): + if self._detail: + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self._detail) + + def _on_show_detail_clicked(self): + if self._detail_dialog is None: + self._detail_dialog = DetailDialog(self._detail, self) + self._detail_dialog.show() + + def _on_confirm_clicked(self): + self.accept() + + +def main(): + json_path = sys.argv[-1] + with open(json_path, "r") as stream: + data = json.load(stream) + + message = data["message"] + detail = data["detail"] + app = get_ayon_qt_app() + dialog = ErrorDialog(message, detail) + dialog.show() + app.exec_() + + +if __name__ == "__main__": + main() From 03317229fa0073f8624357ac04ad1706bcff574d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:10:35 +0200 Subject: [PATCH 005/144] add missing import --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index c85f38b32a..4c27cca9d7 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -10,6 +10,7 @@ import threading import collections from uuid import uuid4 from abc import ABCMeta, abstractmethod +from typing import Optional import six import appdirs From c86613922f035162ed801c9dd95c8698022112a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:15:05 +0200 Subject: [PATCH 006/144] better handling of the error --- client/ayon_core/addon/__init__.py | 2 + client/ayon_core/addon/utils.py | 63 ++++++++++++++++++------------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index 27af2955ed..44f495b2a1 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -10,6 +10,7 @@ from .interfaces import ( from .base import ( ProcessPreparationError, + ProcessContext, AYONAddon, AddonsManager, TrayAddonsManager, @@ -31,6 +32,7 @@ __all__ = ( "IHostAddon", "ProcessPreparationError", + "ProcessContext", "AYONAddon", "AddonsManager", "TrayAddonsManager", diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 092ce502af..77dc5a26f6 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -3,6 +3,7 @@ import sys import contextlib import tempfile import json +import traceback from io import StringIO from typing import Optional @@ -21,6 +22,10 @@ def _handle_error( Shows UI to inform user about the error, or prints the message to stdout if running in headless mode. + Todos: + Make this functionality with the dialog as unified function, so it can + be used elsewhere. + Args: process_context (ProcessContext): The context in which the error occurred. @@ -30,7 +35,8 @@ def _handle_error( """ if process_context.headless: - print(detail) + if detail: + print(detail) print(f"{10*'*'}\n{message}\n{10*'*'}") return @@ -38,16 +44,15 @@ def _handle_error( script_path = os.path.join(current_dir, "ui", "process_ready_error.py") with tempfile.NamedTemporaryFile("w", delete=False) as tmp: tmp_path = tmp.name + json.dump( + {"message": message, "detail": detail}, + tmp.file + ) try: - with open(tmp_path, "w") as stream: - json.dump( - {"message": message, "detail": detail}, - stream - ) - run_ayon_launcher_process( - "run", script_path, tmp_path + "run", script_path, tmp_path, + creationflags=0 ) finally: @@ -85,27 +90,35 @@ def ensure_addons_are_process_ready( message = None failed = False use_detail = False - with contextlib.redirect_stdout(StringIO()) as stdout: - for addon in addons_manager.get_enabled_addons(): - failed = True - try: - addon.ensure_is_process_ready(process_context) - failed = False - except ProcessPreparationError as exc: - message = str(exc) + output = StringIO() + with contextlib.redirect_stdout(output): + with contextlib.redirect_stderr(output): + for addon in addons_manager.get_enabled_addons(): + failed = True + try: + addon.ensure_is_process_ready(process_context) + failed = False + except ProcessPreparationError as exc: + message = str(exc) + print(f"Addon preparation failed: '{addon.name}'") + print(message) - except BaseException as exc: - message = "An unexpected error occurred." - use_detail = True + except BaseException as exc: + message = "An unexpected error occurred." + print(f"Addon preparation failed: '{addon.name}'") + print(message) + # Print the traceback so it is in the output + traceback.print_exception(*sys.exc_info()) + use_detail = True - if failed: - break + if failed: + break + output_str = output.getvalue() + # Print stdout/stderr to console as it was redirected + print(output_str) if failed: - detail = None - if use_detail: - detail = stdout.get_value() - + detail = output_str if use_detail else None _handle_error(process_context, message, detail) if not exit_on_failure: return exc From 54f22696a964154c9a1a113cc24bd486ed7ec58d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:27:39 +0200 Subject: [PATCH 007/144] 'run_ayon_launcher_process' can add sys path to python path --- client/ayon_core/lib/execute.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index e89c8f22ee..f61d892324 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -179,7 +179,7 @@ def clean_envs_for_ayon_process(env=None): return env -def run_ayon_launcher_process(*args, **kwargs): +def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): """Execute AYON process with passed arguments and wait. Wrapper for 'run_process' which prepends AYON executable arguments @@ -209,6 +209,14 @@ def run_ayon_launcher_process(*args, **kwargs): # - fill more if you find more env = clean_envs_for_ayon_process(os.environ) + if add_sys_paths: + new_pythonpath = list(sys.path) + for path in env.get("PYTHONPATH", "").split(os.pathsep): + if not path or path in new_pythonpath: + continue + new_pythonpath.append(path) + env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) + return run_subprocess(args, env=env, **kwargs) From 909e88baa799dc6f226c8bca746fffa697a0fa1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:28:09 +0200 Subject: [PATCH 008/144] run the UI by adding sys path and skipping bootstrap --- client/ayon_core/addon/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 77dc5a26f6..3ad112b562 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -51,8 +51,11 @@ def _handle_error( try: run_ayon_launcher_process( - "run", script_path, tmp_path, - creationflags=0 + "--skip-bootstrap", + script_path, + tmp_path, + add_sys_paths=True, + creationflags=0, ) finally: From 4fefd1e8e5270e77b9a66778373873e99513c526 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:53:50 +0200 Subject: [PATCH 009/144] Fix grammar Co-authored-by: Roy Nieterau --- client/ayon_core/addon/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 4c27cca9d7..0c173ee6f6 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -71,8 +71,8 @@ MOVED_ADDON_MILESTONE_VERSIONS = { class ProcessPreparationError(Exception): """Exception that can be used when process preparation failed. - The message is showed to user (either as UI dialog or printed). If - different error is raised a "generic" error message is showed to user + The message is shown to user (either as UI dialog or printed). If + different error is raised a "generic" error message is shown to user with option to copy error message to clipboard. """ @@ -654,7 +654,7 @@ class AYONAddon(object): Implementation of this method is optional. Note: - The logic can be similar to logic in tray, but in tray not require + The logic can be similar to logic in tray, but tray does not require to be logged in. Args: From 9c06d8c8a28ed8f9e27c222cbe844470c6812704 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:07:10 +0200 Subject: [PATCH 010/144] move current tray implementation to 'ui' subfolder --- client/ayon_core/cli_commands.py | 4 ++-- client/ayon_core/tools/tray/{ => ui}/__init__.py | 0 client/ayon_core/tools/tray/{ => ui}/__main__.py | 0 client/ayon_core/tools/tray/{ => ui}/dialogs.py | 0 .../ayon_core/tools/tray/{ => ui}/images/gifts.png | Bin client/ayon_core/tools/tray/{ => ui}/info_widget.py | 0 client/ayon_core/tools/tray/{ => ui}/tray.py | 0 7 files changed, 2 insertions(+), 2 deletions(-) rename client/ayon_core/tools/tray/{ => ui}/__init__.py (100%) rename client/ayon_core/tools/tray/{ => ui}/__main__.py (100%) rename client/ayon_core/tools/tray/{ => ui}/dialogs.py (100%) rename client/ayon_core/tools/tray/{ => ui}/images/gifts.png (100%) rename client/ayon_core/tools/tray/{ => ui}/info_widget.py (100%) rename client/ayon_core/tools/tray/{ => ui}/tray.py (100%) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 35b7e294de..774ee3e847 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -13,11 +13,11 @@ class Commands: @staticmethod def launch_tray(): from ayon_core.lib import Logger - from ayon_core.tools import tray + from ayon_core.tools.tray.ui import main Logger.set_process_name("Tray") - tray.main() + main() @staticmethod def add_addons(click_func): diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/ui/__init__.py similarity index 100% rename from client/ayon_core/tools/tray/__init__.py rename to client/ayon_core/tools/tray/ui/__init__.py diff --git a/client/ayon_core/tools/tray/__main__.py b/client/ayon_core/tools/tray/ui/__main__.py similarity index 100% rename from client/ayon_core/tools/tray/__main__.py rename to client/ayon_core/tools/tray/ui/__main__.py diff --git a/client/ayon_core/tools/tray/dialogs.py b/client/ayon_core/tools/tray/ui/dialogs.py similarity index 100% rename from client/ayon_core/tools/tray/dialogs.py rename to client/ayon_core/tools/tray/ui/dialogs.py diff --git a/client/ayon_core/tools/tray/images/gifts.png b/client/ayon_core/tools/tray/ui/images/gifts.png similarity index 100% rename from client/ayon_core/tools/tray/images/gifts.png rename to client/ayon_core/tools/tray/ui/images/gifts.png diff --git a/client/ayon_core/tools/tray/info_widget.py b/client/ayon_core/tools/tray/ui/info_widget.py similarity index 100% rename from client/ayon_core/tools/tray/info_widget.py rename to client/ayon_core/tools/tray/ui/info_widget.py diff --git a/client/ayon_core/tools/tray/tray.py b/client/ayon_core/tools/tray/ui/tray.py similarity index 100% rename from client/ayon_core/tools/tray/tray.py rename to client/ayon_core/tools/tray/ui/tray.py From bca296a953624db36552c05e81ee2b0bf6d9d66d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:35:13 +0200 Subject: [PATCH 011/144] more webserver to tray tool --- client/ayon_core/tools/tray/__init__.py | 49 +++++++++++++++++++ .../tray}/webserver/__init__.py | 0 .../tray}/webserver/base_routes.py | 0 .../tray}/webserver/cors_middleware.py | 0 .../tray}/webserver/host_console_listener.py | 0 .../tray}/webserver/server.py | 0 .../tray}/webserver/structures.py | 0 .../tray}/webserver/version.py | 0 .../tray}/webserver/webserver_module.py | 0 9 files changed, 49 insertions(+) create mode 100644 client/ayon_core/tools/tray/__init__.py rename client/ayon_core/{modules => tools/tray}/webserver/__init__.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/base_routes.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/cors_middleware.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/host_console_listener.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/server.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/structures.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/version.py (100%) rename client/ayon_core/{modules => tools/tray}/webserver/webserver_module.py (100%) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py new file mode 100644 index 0000000000..b57461b88f --- /dev/null +++ b/client/ayon_core/tools/tray/__init__.py @@ -0,0 +1,49 @@ +import os +from typing import Optional, Dict, Any + +import ayon_api + + +def _get_default_server_url() -> str: + return os.getenv("AYON_SERVER_URL") + + +def _get_default_variant() -> str: + return ayon_api.get_default_settings_variant() + + +def get_tray_store_dir() -> str: + pass + + +def get_tray_information( + sever_url: str, variant: str +) -> Optional[Dict[str, Any]]: + pass + + +def validate_tray_server(server_url: str) -> bool: + tray_info = get_tray_information(server_url) + if tray_info is None: + return False + return True + + +def get_tray_server_url( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> Optional[str]: + if not server_url: + server_url = _get_default_server_url() + if not variant: + variant = _get_default_variant() + + +def is_tray_running( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> bool: + server_url = get_tray_server_url(server_url, variant) + if server_url and validate_tray_server(server_url): + return True + return False diff --git a/client/ayon_core/modules/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py similarity index 100% rename from client/ayon_core/modules/webserver/__init__.py rename to client/ayon_core/tools/tray/webserver/__init__.py diff --git a/client/ayon_core/modules/webserver/base_routes.py b/client/ayon_core/tools/tray/webserver/base_routes.py similarity index 100% rename from client/ayon_core/modules/webserver/base_routes.py rename to client/ayon_core/tools/tray/webserver/base_routes.py diff --git a/client/ayon_core/modules/webserver/cors_middleware.py b/client/ayon_core/tools/tray/webserver/cors_middleware.py similarity index 100% rename from client/ayon_core/modules/webserver/cors_middleware.py rename to client/ayon_core/tools/tray/webserver/cors_middleware.py diff --git a/client/ayon_core/modules/webserver/host_console_listener.py b/client/ayon_core/tools/tray/webserver/host_console_listener.py similarity index 100% rename from client/ayon_core/modules/webserver/host_console_listener.py rename to client/ayon_core/tools/tray/webserver/host_console_listener.py diff --git a/client/ayon_core/modules/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py similarity index 100% rename from client/ayon_core/modules/webserver/server.py rename to client/ayon_core/tools/tray/webserver/server.py diff --git a/client/ayon_core/modules/webserver/structures.py b/client/ayon_core/tools/tray/webserver/structures.py similarity index 100% rename from client/ayon_core/modules/webserver/structures.py rename to client/ayon_core/tools/tray/webserver/structures.py diff --git a/client/ayon_core/modules/webserver/version.py b/client/ayon_core/tools/tray/webserver/version.py similarity index 100% rename from client/ayon_core/modules/webserver/version.py rename to client/ayon_core/tools/tray/webserver/version.py diff --git a/client/ayon_core/modules/webserver/webserver_module.py b/client/ayon_core/tools/tray/webserver/webserver_module.py similarity index 100% rename from client/ayon_core/modules/webserver/webserver_module.py rename to client/ayon_core/tools/tray/webserver/webserver_module.py From 1bc97337540db527f1895a14e24a310ad3a1e47d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:25:04 +0200 Subject: [PATCH 012/144] fix webserver import --- client/ayon_core/tools/stdout_broker/broker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/stdout_broker/broker.py b/client/ayon_core/tools/stdout_broker/broker.py index 291936008b..4f7118e2a8 100644 --- a/client/ayon_core/tools/stdout_broker/broker.py +++ b/client/ayon_core/tools/stdout_broker/broker.py @@ -8,7 +8,7 @@ from datetime import datetime import websocket from ayon_core.lib import Logger -from ayon_core.modules.webserver import HostMsgAction +from ayon_core.tools.tray.webserver import HostMsgAction log = Logger.get_logger(__name__) From 28bff3a9d0215ac5ffaeff593250ee40313cca2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:41:23 +0200 Subject: [PATCH 013/144] fix loader tool grouping with status filtering --- client/ayon_core/tools/loader/ui/products_model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 734be5dd90..9fc89f5fb3 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -127,6 +127,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = None self._last_folder_ids = [] + self._last_status_names = None self._last_project_statuses = {} self._last_status_icons_by_name = {} @@ -165,7 +166,11 @@ class ProductsModel(QtGui.QStandardItemModel): return self._grouping_enabled = enable_grouping # Ignore change if groups are not available - self.refresh(self._last_project_name, self._last_folder_ids) + self.refresh( + self._last_project_name, + self._last_folder_ids, + self._last_status_names + ) def flags(self, index): # Make the version column editable @@ -459,6 +464,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = project_name self._last_folder_ids = folder_ids + self._last_status_names = status_names status_items = self._controller.get_project_status_items(project_name) self._last_project_statuses = { status_item.name: status_item From 447a20619b374c9a67bb5bbafa9de7d38aed967f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:50:53 +0200 Subject: [PATCH 014/144] don't handle status names filter on model refresh --- .../tools/loader/ui/products_model.py | 21 +++++-------------- .../tools/loader/ui/products_widget.py | 3 +-- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 9fc89f5fb3..c4a738d9df 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -127,7 +127,6 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = None self._last_folder_ids = [] - self._last_status_names = None self._last_project_statuses = {} self._last_status_icons_by_name = {} @@ -168,8 +167,7 @@ class ProductsModel(QtGui.QStandardItemModel): # Ignore change if groups are not available self.refresh( self._last_project_name, - self._last_folder_ids, - self._last_status_names + self._last_folder_ids ) def flags(self, index): @@ -459,12 +457,11 @@ class ProductsModel(QtGui.QStandardItemModel): def get_last_project_name(self): return self._last_project_name - def refresh(self, project_name, folder_ids, status_names): + def refresh(self, project_name, folder_ids): self._clear() self._last_project_name = project_name self._last_folder_ids = folder_ids - self._last_status_names = status_names status_items = self._controller.get_project_status_items(project_name) self._last_project_statuses = { status_item.name: status_item @@ -492,17 +489,9 @@ class ProductsModel(QtGui.QStandardItemModel): } last_version_by_product_id = {} for product_item in product_items: - all_versions = list(product_item.version_items.values()) - all_versions.sort() - versions = [ - version_item - for version_item in all_versions - if status_names is None or version_item.status in status_names - ] - if versions: - last_version = versions[-1] - else: - last_version = all_versions[-1] + versions = list(product_item.version_items.values()) + versions.sort() + last_version = versions[-1] last_version_by_product_id[product_item.product_id] = ( last_version ) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index e37c327a17..5fa2716714 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -321,8 +321,7 @@ class ProductsWidget(QtWidgets.QWidget): def _refresh_model(self): self._products_model.refresh( self._selected_project_name, - self._selected_folder_ids, - self._products_proxy_model.get_statuses_filter() + self._selected_folder_ids ) def _on_context_menu(self, point): From 22f7a9d2af5f4dae6fe314cd3fe48158f37254aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:08:58 +0200 Subject: [PATCH 015/144] moved tray addons manager to tray --- client/ayon_core/addon/README.md | 4 - client/ayon_core/addon/__init__.py | 2 - client/ayon_core/addon/base.py | 182 ----------------- client/ayon_core/modules/base.py | 4 - client/ayon_core/tools/tray/addons_manager.py | 184 ++++++++++++++++++ client/ayon_core/tools/tray/ui/tray.py | 2 +- 6 files changed, 185 insertions(+), 193 deletions(-) create mode 100644 client/ayon_core/tools/tray/addons_manager.py diff --git a/client/ayon_core/addon/README.md b/client/ayon_core/addon/README.md index e1c04ea0d6..ded2d50e9c 100644 --- a/client/ayon_core/addon/README.md +++ b/client/ayon_core/addon/README.md @@ -86,7 +86,3 @@ AYON addons should contain separated logic of specific kind of implementation, s "inventory": [] } ``` - -### TrayAddonsManager -- inherits from `AddonsManager` -- has specific implementation for AYON Tray and handle `ITrayAddon` methods diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index fe8865c730..c7eccd7b6c 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -11,7 +11,6 @@ from .interfaces import ( from .base import ( AYONAddon, AddonsManager, - TrayAddonsManager, load_addons, ) @@ -27,6 +26,5 @@ __all__ = ( "AYONAddon", "AddonsManager", - "TrayAddonsManager", "load_addons", ) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 308494b4d8..5cabf3e5e0 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -1338,185 +1338,3 @@ class AddonsManager: " 'get_host_module' please use 'get_host_addon' instead." ) return self.get_host_addon(host_name) - - -class TrayAddonsManager(AddonsManager): - # Define order of addons in menu - # TODO find better way how to define order - addons_menu_order = ( - "user", - "ftrack", - "kitsu", - "launcher_tool", - "avalon", - "clockify", - "traypublish_tool", - "log_viewer", - ) - - def __init__(self, settings=None): - super(TrayAddonsManager, self).__init__(settings, initialize=False) - - self.tray_manager = None - - self.doubleclick_callbacks = {} - self.doubleclick_callback = None - - def add_doubleclick_callback(self, addon, callback): - """Register double-click callbacks on tray icon. - - Currently, there is no way how to determine which is launched. Name of - callback can be defined with `doubleclick_callback` attribute. - - Missing feature how to define default callback. - - Args: - addon (AYONAddon): Addon object. - callback (FunctionType): Function callback. - """ - - callback_name = "_".join([addon.name, callback.__name__]) - if callback_name not in self.doubleclick_callbacks: - self.doubleclick_callbacks[callback_name] = callback - if self.doubleclick_callback is None: - self.doubleclick_callback = callback_name - return - - self.log.warning(( - "Callback with name \"{}\" is already registered." - ).format(callback_name)) - - def initialize(self, tray_manager, tray_menu): - self.tray_manager = tray_manager - self.initialize_addons() - self.tray_init() - self.connect_addons() - self.tray_menu(tray_menu) - - def get_enabled_tray_addons(self): - """Enabled tray addons. - - Returns: - list[AYONAddon]: Enabled addons that inherit from tray interface. - """ - - return [ - addon - for addon in self.get_enabled_addons() - if isinstance(addon, ITrayAddon) - ] - - def restart_tray(self): - if self.tray_manager: - self.tray_manager.restart() - - def tray_init(self): - report = {} - time_start = time.time() - prev_start_time = time_start - for addon in self.get_enabled_tray_addons(): - try: - addon._tray_manager = self.tray_manager - addon.tray_init() - addon.tray_initialized = True - except Exception: - self.log.warning( - "Addon \"{}\" crashed on `tray_init`.".format( - addon.name - ), - exc_info=True - ) - - now = time.time() - report[addon.__class__.__name__] = now - prev_start_time - prev_start_time = now - - if self._report is not None: - report[self._report_total_key] = time.time() - time_start - self._report["Tray init"] = report - - def tray_menu(self, tray_menu): - ordered_addons = [] - enabled_by_name = { - addon.name: addon - for addon in self.get_enabled_tray_addons() - } - - for name in self.addons_menu_order: - addon_by_name = enabled_by_name.pop(name, None) - if addon_by_name: - ordered_addons.append(addon_by_name) - ordered_addons.extend(enabled_by_name.values()) - - report = {} - time_start = time.time() - prev_start_time = time_start - for addon in ordered_addons: - if not addon.tray_initialized: - continue - - try: - addon.tray_menu(tray_menu) - except Exception: - # Unset initialized mark - addon.tray_initialized = False - self.log.warning( - "Addon \"{}\" crashed on `tray_menu`.".format( - addon.name - ), - exc_info=True - ) - now = time.time() - report[addon.__class__.__name__] = now - prev_start_time - prev_start_time = now - - if self._report is not None: - report[self._report_total_key] = time.time() - time_start - self._report["Tray menu"] = report - - def start_addons(self): - report = {} - time_start = time.time() - prev_start_time = time_start - for addon in self.get_enabled_tray_addons(): - if not addon.tray_initialized: - if isinstance(addon, ITrayService): - addon.set_service_failed_icon() - continue - - try: - addon.tray_start() - except Exception: - self.log.warning( - "Addon \"{}\" crashed on `tray_start`.".format( - addon.name - ), - exc_info=True - ) - now = time.time() - report[addon.__class__.__name__] = now - prev_start_time - prev_start_time = now - - if self._report is not None: - report[self._report_total_key] = time.time() - time_start - self._report["Addons start"] = report - - def on_exit(self): - for addon in self.get_enabled_tray_addons(): - if addon.tray_initialized: - try: - addon.tray_exit() - except Exception: - self.log.warning( - "Addon \"{}\" crashed on `tray_exit`.".format( - addon.name - ), - exc_info=True - ) - - # DEPRECATED - def get_enabled_tray_modules(self): - return self.get_enabled_tray_addons() - - def start_modules(self): - self.start_addons() diff --git a/client/ayon_core/modules/base.py b/client/ayon_core/modules/base.py index 3f2a7d4ea5..df412d141e 100644 --- a/client/ayon_core/modules/base.py +++ b/client/ayon_core/modules/base.py @@ -3,7 +3,6 @@ from ayon_core.addon import ( AYONAddon, AddonsManager, - TrayAddonsManager, load_addons, ) from ayon_core.addon.base import ( @@ -12,18 +11,15 @@ from ayon_core.addon.base import ( ) ModulesManager = AddonsManager -TrayModulesManager = TrayAddonsManager load_modules = load_addons __all__ = ( "AYONAddon", "AddonsManager", - "TrayAddonsManager", "load_addons", "OpenPypeModule", "OpenPypeAddOn", "ModulesManager", - "TrayModulesManager", "load_modules", ) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py new file mode 100644 index 0000000000..307b5fba34 --- /dev/null +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -0,0 +1,184 @@ +import time +from ayon_core.addon import AddonsManager, ITrayAddon, ITrayService + + +class TrayAddonsManager(AddonsManager): + # Define order of addons in menu + # TODO find better way how to define order + addons_menu_order = ( + "user", + "ftrack", + "kitsu", + "launcher_tool", + "avalon", + "clockify", + "traypublish_tool", + "log_viewer", + ) + + def __init__(self, settings=None): + super(TrayAddonsManager, self).__init__(settings, initialize=False) + + self.tray_manager = None + + self.doubleclick_callbacks = {} + self.doubleclick_callback = None + + def add_doubleclick_callback(self, addon, callback): + """Register double-click callbacks on tray icon. + + Currently, there is no way how to determine which is launched. Name of + callback can be defined with `doubleclick_callback` attribute. + + Missing feature how to define default callback. + + Args: + addon (AYONAddon): Addon object. + callback (FunctionType): Function callback. + """ + + callback_name = "_".join([addon.name, callback.__name__]) + if callback_name not in self.doubleclick_callbacks: + self.doubleclick_callbacks[callback_name] = callback + if self.doubleclick_callback is None: + self.doubleclick_callback = callback_name + return + + self.log.warning(( + "Callback with name \"{}\" is already registered." + ).format(callback_name)) + + def initialize(self, tray_manager, tray_menu): + self.tray_manager = tray_manager + self.initialize_addons() + self.tray_init() + self.connect_addons() + self.tray_menu(tray_menu) + + def get_enabled_tray_addons(self): + """Enabled tray addons. + + Returns: + list[AYONAddon]: Enabled addons that inherit from tray interface. + """ + + return [ + addon + for addon in self.get_enabled_addons() + if isinstance(addon, ITrayAddon) + ] + + def restart_tray(self): + if self.tray_manager: + self.tray_manager.restart() + + def tray_init(self): + report = {} + time_start = time.time() + prev_start_time = time_start + for addon in self.get_enabled_tray_addons(): + try: + addon._tray_manager = self.tray_manager + addon.tray_init() + addon.tray_initialized = True + except Exception: + self.log.warning( + "Addon \"{}\" crashed on `tray_init`.".format( + addon.name + ), + exc_info=True + ) + + now = time.time() + report[addon.__class__.__name__] = now - prev_start_time + prev_start_time = now + + if self._report is not None: + report[self._report_total_key] = time.time() - time_start + self._report["Tray init"] = report + + def tray_menu(self, tray_menu): + ordered_addons = [] + enabled_by_name = { + addon.name: addon + for addon in self.get_enabled_tray_addons() + } + + for name in self.addons_menu_order: + addon_by_name = enabled_by_name.pop(name, None) + if addon_by_name: + ordered_addons.append(addon_by_name) + ordered_addons.extend(enabled_by_name.values()) + + report = {} + time_start = time.time() + prev_start_time = time_start + for addon in ordered_addons: + if not addon.tray_initialized: + continue + + try: + addon.tray_menu(tray_menu) + except Exception: + # Unset initialized mark + addon.tray_initialized = False + self.log.warning( + "Addon \"{}\" crashed on `tray_menu`.".format( + addon.name + ), + exc_info=True + ) + now = time.time() + report[addon.__class__.__name__] = now - prev_start_time + prev_start_time = now + + if self._report is not None: + report[self._report_total_key] = time.time() - time_start + self._report["Tray menu"] = report + + def start_addons(self): + report = {} + time_start = time.time() + prev_start_time = time_start + for addon in self.get_enabled_tray_addons(): + if not addon.tray_initialized: + if isinstance(addon, ITrayService): + addon.set_service_failed_icon() + continue + + try: + addon.tray_start() + except Exception: + self.log.warning( + "Addon \"{}\" crashed on `tray_start`.".format( + addon.name + ), + exc_info=True + ) + now = time.time() + report[addon.__class__.__name__] = now - prev_start_time + prev_start_time = now + + if self._report is not None: + report[self._report_total_key] = time.time() - time_start + self._report["Addons start"] = report + + def on_exit(self): + for addon in self.get_enabled_tray_addons(): + if addon.tray_initialized: + try: + addon.tray_exit() + except Exception: + self.log.warning( + "Addon \"{}\" crashed on `tray_exit`.".format( + addon.name + ), + exc_info=True + ) + + # DEPRECATED + def get_enabled_tray_modules(self): + return self.get_enabled_tray_addons() + + def start_modules(self): + self.start_addons() diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index c0b90dd764..798b76ce80 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -21,12 +21,12 @@ from ayon_core.settings import get_studio_settings from ayon_core.addon import ( ITrayAction, ITrayService, - TrayAddonsManager, ) from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, ) +from ayon_core.tools.tray.addons_manager import TrayAddonsManager from .info_widget import InfoWidget from .dialogs import ( From fee111dd97b6d90113f2539227b458c7807ff407 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:09:26 +0200 Subject: [PATCH 016/144] removed deprecated methods --- client/ayon_core/tools/tray/addons_manager.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index 307b5fba34..e7c1243c5a 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -175,10 +175,3 @@ class TrayAddonsManager(AddonsManager): ), exc_info=True ) - - # DEPRECATED - def get_enabled_tray_modules(self): - return self.get_enabled_tray_addons() - - def start_modules(self): - self.start_addons() From eec0d4a0c828ece5f9c5d973880f08079681ab04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:10:52 +0200 Subject: [PATCH 017/144] pass tray manager on initialization --- client/ayon_core/tools/tray/addons_manager.py | 9 ++++----- client/ayon_core/tools/tray/ui/tray.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index e7c1243c5a..b05a336eed 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -16,10 +16,10 @@ class TrayAddonsManager(AddonsManager): "log_viewer", ) - def __init__(self, settings=None): - super(TrayAddonsManager, self).__init__(settings, initialize=False) + def __init__(self, tray_manager): + super().__init__(initialize=False) - self.tray_manager = None + self.tray_manager = tray_manager self.doubleclick_callbacks = {} self.doubleclick_callback = None @@ -48,8 +48,7 @@ class TrayAddonsManager(AddonsManager): "Callback with name \"{}\" is already registered." ).format(callback_name)) - def initialize(self, tray_manager, tray_menu): - self.tray_manager = tray_manager + def initialize(self, tray_menu): self.initialize_addons() self.tray_init() self.connect_addons() diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 798b76ce80..613d9c9e2e 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -56,7 +56,7 @@ class TrayManager: update_check_interval = 5 self._update_check_interval = update_check_interval * 60 * 1000 - self._addons_manager = TrayAddonsManager() + self._addons_manager = TrayAddonsManager(self) self.errors = [] @@ -103,7 +103,7 @@ class TrayManager: def initialize_addons(self): """Add addons to tray.""" - self._addons_manager.initialize(self, self.tray_widget.menu) + self._addons_manager.initialize(self.tray_widget.menu) admin_submenu = ITrayAction.admin_submenu(self.tray_widget.menu) self.tray_widget.menu.addMenu(admin_submenu) From 0b71ec7399873d04cbb0ba9ee4bdefebccc28489 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:11:15 +0200 Subject: [PATCH 018/144] make tray manager private attribute --- client/ayon_core/tools/tray/addons_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index b05a336eed..366d2de404 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -19,7 +19,7 @@ class TrayAddonsManager(AddonsManager): def __init__(self, tray_manager): super().__init__(initialize=False) - self.tray_manager = tray_manager + self._tray_manager = tray_manager self.doubleclick_callbacks = {} self.doubleclick_callback = None @@ -68,8 +68,8 @@ class TrayAddonsManager(AddonsManager): ] def restart_tray(self): - if self.tray_manager: - self.tray_manager.restart() + if self._tray_manager: + self._tray_manager.restart() def tray_init(self): report = {} @@ -77,7 +77,7 @@ class TrayAddonsManager(AddonsManager): prev_start_time = time_start for addon in self.get_enabled_tray_addons(): try: - addon._tray_manager = self.tray_manager + addon._tray_manager = self._tray_manager addon.tray_init() addon.tray_initialized = True except Exception: From 50e196d0cfda847c28cec2e7677e4a0fe02b8ec2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:11:53 +0200 Subject: [PATCH 019/144] removed not existing items from menu order --- client/ayon_core/tools/tray/addons_manager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index 366d2de404..706895ab3c 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -6,14 +6,10 @@ class TrayAddonsManager(AddonsManager): # Define order of addons in menu # TODO find better way how to define order addons_menu_order = ( - "user", "ftrack", "kitsu", "launcher_tool", - "avalon", "clockify", - "traypublish_tool", - "log_viewer", ) def __init__(self, tray_manager): From 74e2a9dc00c679d186394bc701c77fba20b82579 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:13:34 +0200 Subject: [PATCH 020/144] move lib functions to lib.py --- client/ayon_core/tools/tray/__init__.py | 49 ------------------------ client/ayon_core/tools/tray/lib.py | 50 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 49 deletions(-) create mode 100644 client/ayon_core/tools/tray/lib.py diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index b57461b88f..e69de29bb2 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,49 +0,0 @@ -import os -from typing import Optional, Dict, Any - -import ayon_api - - -def _get_default_server_url() -> str: - return os.getenv("AYON_SERVER_URL") - - -def _get_default_variant() -> str: - return ayon_api.get_default_settings_variant() - - -def get_tray_store_dir() -> str: - pass - - -def get_tray_information( - sever_url: str, variant: str -) -> Optional[Dict[str, Any]]: - pass - - -def validate_tray_server(server_url: str) -> bool: - tray_info = get_tray_information(server_url) - if tray_info is None: - return False - return True - - -def get_tray_server_url( - server_url: Optional[str] = None, - variant: Optional[str] = None -) -> Optional[str]: - if not server_url: - server_url = _get_default_server_url() - if not variant: - variant = _get_default_variant() - - -def is_tray_running( - server_url: Optional[str] = None, - variant: Optional[str] = None -) -> bool: - server_url = get_tray_server_url(server_url, variant) - if server_url and validate_tray_server(server_url): - return True - return False diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py new file mode 100644 index 0000000000..52e603daf0 --- /dev/null +++ b/client/ayon_core/tools/tray/lib.py @@ -0,0 +1,50 @@ +@@ -1,49 +0,0 @@ +import os +from typing import Optional, Dict, Any + +import ayon_api + + +def _get_default_server_url() -> str: + return os.getenv("AYON_SERVER_URL") + + +def _get_default_variant() -> str: + return ayon_api.get_default_settings_variant() + + +def get_tray_store_dir() -> str: + pass + + +def get_tray_information( + sever_url: str, variant: str +) -> Optional[Dict[str, Any]]: + pass + + +def validate_tray_server(server_url: str) -> bool: + tray_info = get_tray_information(server_url) + if tray_info is None: + return False + return True + + +def get_tray_server_url( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> Optional[str]: + if not server_url: + server_url = _get_default_server_url() + if not variant: + variant = _get_default_variant() + + +def is_tray_running( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> bool: + server_url = get_tray_server_url(server_url, variant) + if server_url and validate_tray_server(server_url): + return True + return False From 6bd87b019d9c20629bd034ba1a36232cf55bd120 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:14:20 +0200 Subject: [PATCH 021/144] simplified 'TrayAddonsManager' import --- client/ayon_core/tools/tray/__init__.py | 6 ++++++ client/ayon_core/tools/tray/ui/tray.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index e69de29bb2..534e7100f5 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -0,0 +1,6 @@ +from .addons_manager import TrayAddonsManager + + +__all__ = ( + "TrayAddonsManager", +) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 613d9c9e2e..3dd822e4c5 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -26,7 +26,7 @@ from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, ) -from ayon_core.tools.tray.addons_manager import TrayAddonsManager +from ayon_core.tools.tray import TrayAddonsManager from .info_widget import InfoWidget from .dialogs import ( From 182cc138c8c28df5cafc07be463b11b0c59886e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:42:47 +0200 Subject: [PATCH 022/144] store selection selection model to variable to avoid garbage collection --- client/ayon_core/tools/loader/ui/products_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 5fa2716714..84e764f403 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -188,7 +188,8 @@ class ProductsWidget(QtWidgets.QWidget): products_model.refreshed.connect(self._on_refresh) products_view.customContextMenuRequested.connect( self._on_context_menu) - products_view.selectionModel().selectionChanged.connect( + products_view_sel_model = products_view.selectionModel() + products_view_sel_model.selectionChanged.connect( self._on_selection_change) products_model.version_changed.connect(self._on_version_change) version_delegate.version_changed.connect( From 119d8c838021bb0d181d4838cb3853f090de312f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:43:13 +0200 Subject: [PATCH 023/144] fix product types and status names filtering --- .../tools/loader/ui/products_model.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index c4a738d9df..97ab11a07e 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -538,10 +538,11 @@ class ProductsModel(QtGui.QStandardItemModel): for product_name, product_items in groups.items(): group_product_types |= {p.product_type for p in product_items} for product_item in product_items: - group_product_types |= { + group_status_names |= { version_item.status for version_item in product_item.version_items.values() } + group_product_types.add(product_item.product_type) if len(product_items) == 1: top_items.append(product_items[0]) @@ -584,13 +585,15 @@ class ProductsModel(QtGui.QStandardItemModel): product_name, product_items = path_info (merged_color_hex, merged_color_qt) = self._get_next_color() merged_color = qtawesome.icon( - "fa.circle", color=merged_color_qt) + "fa.circle", color=merged_color_qt + ) merged_item = self._get_merged_model_item( product_name, len(product_items), merged_color_hex) merged_item.setData(merged_color, QtCore.Qt.DecorationRole) new_items.append(merged_item) merged_product_types = set() + merged_status_names = set() new_merged_items = [] for product_item in product_items: item = self._get_product_model_item( @@ -603,9 +606,21 @@ class ProductsModel(QtGui.QStandardItemModel): ) new_merged_items.append(item) merged_product_types.add(product_item.product_type) + merged_status_names |= { + version_item.status + for version_item in ( + product_item.version_items.values() + ) + } merged_item.setData( - "|".join(merged_product_types), PRODUCT_TYPE_ROLE) + "|".join(merged_product_types), + PRODUCT_TYPE_ROLE + ) + merged_item.setData( + "|".join(merged_status_names), + STATUS_NAME_FILTER_ROLE + ) if new_merged_items: merged_item.appendRows(new_merged_items) From 58ae174f71dbc179f742417ee0d70d232e589b1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:33:19 +0200 Subject: [PATCH 024/144] removed unnecessary 'EditorInfo' --- .../tools/loader/ui/products_delegates.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 0ed8fe8fe7..9753da37af 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -140,12 +140,6 @@ class VersionComboBox(QtWidgets.QComboBox): self.value_changed.emit(self._product_id, value) -class EditorInfo: - def __init__(self, widget): - self.widget = widget - self.added = False - - class VersionDelegate(QtWidgets.QStyledItemDelegate): """A delegate that display version integer formatted as version string.""" @@ -154,7 +148,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._editor_by_id: Dict[str, EditorInfo] = {} + self._editor_by_id: Dict[str, VersionComboBox] = {} self._statuses_filter = None def displayText(self, value, locale): @@ -164,8 +158,8 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): def set_statuses_filter(self, status_names): self._statuses_filter = set(status_names) - for info in self._editor_by_id.values(): - info.widget.set_statuses_filter(status_names) + for widget in self._editor_by_id.values(): + widget.set_statuses_filter(status_names) def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) @@ -229,11 +223,11 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): editor = VersionComboBox(product_id, parent) editor.setProperty("itemId", item_id) - self._editor_by_id[item_id] = EditorInfo(editor) - editor.value_changed.connect(self._on_editor_change) editor.destroyed.connect(self._on_destroy) + self._editor_by_id[item_id] = editor + return editor def setEditorData(self, editor, index): @@ -242,12 +236,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): # Current value of the index versions = index.data(VERSION_NAME_EDIT_ROLE) or [] version_id = index.data(VERSION_ID_ROLE) + editor.update_versions(versions, version_id) editor.set_statuses_filter(self._statuses_filter) - item_id = editor.property("itemId") - self._editor_by_id[item_id].added = True - def setModelData(self, editor, model, index): """Apply the integer version back in the model""" From 642304d8bd774dc13ff928aef85a0f9ad4fef74f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:33:53 +0200 Subject: [PATCH 025/144] fomratting change --- client/ayon_core/tools/loader/ui/products_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 84e764f403..748a1b5fb8 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -186,12 +186,12 @@ class ProductsWidget(QtWidgets.QWidget): products_proxy_model.rowsInserted.connect(self._on_rows_inserted) products_proxy_model.rowsMoved.connect(self._on_rows_moved) products_model.refreshed.connect(self._on_refresh) + products_model.version_changed.connect(self._on_version_change) products_view.customContextMenuRequested.connect( self._on_context_menu) products_view_sel_model = products_view.selectionModel() products_view_sel_model.selectionChanged.connect( self._on_selection_change) - products_model.version_changed.connect(self._on_version_change) version_delegate.version_changed.connect( self._on_version_delegate_change ) From 05e71ef3326947338803b5d6675a9a10f8754f99 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:34:21 +0200 Subject: [PATCH 026/144] fix 'set_product_version' for items under group/s --- client/ayon_core/tools/loader/ui/products_model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 97ab11a07e..400138ff9a 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -156,9 +156,8 @@ class ProductsModel(QtGui.QStandardItemModel): if product_item is None: return - self.setData( - product_item.index(), version_id, VERSION_NAME_EDIT_ROLE - ) + index = self.indexFromItem(product_item) + self.setData(index, version_id, VERSION_NAME_EDIT_ROLE) def set_enable_grouping(self, enable_grouping): if enable_grouping is self._grouping_enabled: From 0b53b8f33640f002c5c0a747e1b87799c39cc696 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:42:58 +0200 Subject: [PATCH 027/144] fix 'get_product_item_indexes' --- client/ayon_core/tools/loader/ui/products_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 400138ff9a..bc24d4d7f7 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -132,7 +132,7 @@ class ProductsModel(QtGui.QStandardItemModel): def get_product_item_indexes(self): return [ - item.index() + self.indexFromItem(item) for item in self._items_by_id.values() ] From f6cca927e1aeb473a12e2065bd775db89c3c80d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:47:12 +0200 Subject: [PATCH 028/144] removed 'TrayModulesManager' import --- client/ayon_core/modules/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/modules/__init__.py b/client/ayon_core/modules/__init__.py index 0dfd7d663c..f4e381f4a0 100644 --- a/client/ayon_core/modules/__init__.py +++ b/client/ayon_core/modules/__init__.py @@ -17,7 +17,6 @@ from .base import ( load_modules, ModulesManager, - TrayModulesManager, ) @@ -38,5 +37,4 @@ __all__ = ( "load_modules", "ModulesManager", - "TrayModulesManager", ) From f80b82add98b8bb7cb60201eb39ea44094ab4db9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:50:55 +0200 Subject: [PATCH 029/144] changed webserver from addon to feature of tray --- client/ayon_core/tools/tray/addons_manager.py | 15 ++ .../tools/tray/webserver/__init__.py | 9 +- .../tools/tray/webserver/base_routes.py | 3 +- .../tray/webserver/host_console_listener.py | 32 ++-- .../ayon_core/tools/tray/webserver/server.py | 19 ++- .../ayon_core/tools/tray/webserver/version.py | 1 - .../tools/tray/webserver/webserver_module.py | 142 +++++++----------- 7 files changed, 99 insertions(+), 122 deletions(-) delete mode 100644 client/ayon_core/tools/tray/webserver/version.py diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index 706895ab3c..ad265298d0 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -1,5 +1,7 @@ import time + from ayon_core.addon import AddonsManager, ITrayAddon, ITrayService +from ayon_core.tools.tray.webserver import TrayWebserver class TrayAddonsManager(AddonsManager): @@ -16,10 +18,15 @@ class TrayAddonsManager(AddonsManager): super().__init__(initialize=False) self._tray_manager = tray_manager + self._tray_webserver = None self.doubleclick_callbacks = {} self.doubleclick_callback = None + def get_doubleclick_callback(self): + callback_name = self.doubleclick_callback + return self.doubleclick_callbacks.get(callback_name) + def add_doubleclick_callback(self, addon, callback): """Register double-click callbacks on tray icon. @@ -68,6 +75,7 @@ class TrayAddonsManager(AddonsManager): self._tray_manager.restart() def tray_init(self): + self._tray_webserver = TrayWebserver(self._tray_manager) report = {} time_start = time.time() prev_start_time = time_start @@ -92,6 +100,11 @@ class TrayAddonsManager(AddonsManager): report[self._report_total_key] = time.time() - time_start self._report["Tray init"] = report + def connect_addons(self): + enabled_addons = self.get_enabled_addons() + self._tray_webserver.connect_with_addons(enabled_addons) + super().connect_addons() + def tray_menu(self, tray_menu): ordered_addons = [] enabled_by_name = { @@ -132,6 +145,7 @@ class TrayAddonsManager(AddonsManager): self._report["Tray menu"] = report def start_addons(self): + self._tray_webserver.start() report = {} time_start = time.time() prev_start_time = time_start @@ -159,6 +173,7 @@ class TrayAddonsManager(AddonsManager): self._report["Addons start"] = report def on_exit(self): + self._tray_webserver.stop() for addon in self.get_enabled_tray_addons(): if addon.tray_initialized: try: diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index 32f2c55f65..db7c2a7c77 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,13 +1,8 @@ -from .version import __version__ from .structures import HostMsgAction -from .webserver_module import ( - WebServerAddon -) +from .webserver_module import TrayWebserver __all__ = ( - "__version__", - "HostMsgAction", - "WebServerAddon", + "TrayWebserver", ) diff --git a/client/ayon_core/tools/tray/webserver/base_routes.py b/client/ayon_core/tools/tray/webserver/base_routes.py index f4f1abe16c..82568c201c 100644 --- a/client/ayon_core/tools/tray/webserver/base_routes.py +++ b/client/ayon_core/tools/tray/webserver/base_routes.py @@ -1,7 +1,6 @@ """Helper functions or classes for Webserver module. -These must not be imported in module itself to not break Python 2 -applications. +These must not be imported in module itself to not break in-DCC process. """ import inspect diff --git a/client/ayon_core/tools/tray/webserver/host_console_listener.py b/client/ayon_core/tools/tray/webserver/host_console_listener.py index 2efd768e24..3ec57d2598 100644 --- a/client/ayon_core/tools/tray/webserver/host_console_listener.py +++ b/client/ayon_core/tools/tray/webserver/host_console_listener.py @@ -22,9 +22,9 @@ class IconType: class HostListener: - def __init__(self, webserver, module): + def __init__(self, webserver, tray_manager): self._window_per_id = {} - self.module = module + self._tray_manager = tray_manager self.webserver = webserver self._window_per_id = {} # dialogs per host name self._action_per_id = {} # QAction per host name @@ -32,8 +32,9 @@ class HostListener: 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 + """ Initialize dialog, adds to submenu.""" + ITrayService.services_submenu(self._tray_manager) + services_submenu = self._tray_manager.get_services_submenu() action = QtWidgets.QAction(label, services_submenu) action.triggered.connect(lambda: self.show_widget(host_name)) @@ -73,8 +74,9 @@ class HostListener: Dialog get initialized when 'host_name' is connecting. """ - self.module.execute_in_main_thread( - lambda: self._show_widget(host_name)) + self._tray_manager.execute_in_main_thread( + self._show_widget, host_name + ) def _show_widget(self, host_name): widget = self._window_per_id[host_name] @@ -95,21 +97,23 @@ class HostListener: if action == HostMsgAction.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)) + self._tray_manager.execute_in_main_thread( + self._host_is_connecting, host_name, text + ) elif action == HostMsgAction.CLOSE: # clean close self._close(host_name) await ws.close() elif action == HostMsgAction.INITIALIZED: - self.module.execute_in_main_thread( + self._tray_manager.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)) + self._set_host_icon, host_name, IconType.RUNNING + ) elif action == HostMsgAction.ADD: - self.module.execute_in_main_thread( - lambda: self._add_text(host_name, text)) + self._tray_manager.execute_in_main_thread( + self._add_text, host_name, text + ) elif msg.type == aiohttp.WSMsgType.ERROR: print('ws connection closed with exception %s' % ws.exception()) @@ -131,7 +135,7 @@ class HostListener: def _close(self, host_name): """ Clean close - remove from menu, delete widget.""" - services_submenu = self.module._services_submenu + services_submenu = self._tray_manager.get_services_submenu() action = self._action_per_id.pop(host_name) services_submenu.removeAction(action) widget = self._window_per_id.pop(host_name) diff --git a/client/ayon_core/tools/tray/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py index 99d9badb6a..2e0d1b258c 100644 --- a/client/ayon_core/tools/tray/webserver/server.py +++ b/client/ayon_core/tools/tray/webserver/server.py @@ -1,6 +1,7 @@ import re import threading import asyncio +from typing import Callable, Optional from aiohttp import web @@ -11,7 +12,9 @@ from .cors_middleware import cors_middleware class WebServerManager: """Manger that care about web server thread.""" - def __init__(self, port=None, host=None): + def __init__( + self, port: Optional[int] = None, host: Optional[str] = None + ): self._log = None self.port = port or 8079 @@ -40,14 +43,14 @@ class WebServerManager: return self._log @property - def url(self): - return "http://{}:{}".format(self.host, self.port) + def url(self) -> str: + return f"http://{self.host}:{self.port}" - def add_route(self, *args, **kwargs): - self.app.router.add_route(*args, **kwargs) + def add_route(self, request_method: str, path: str, handler: Callable): + self.app.router.add_route(request_method, path, handler) - def add_static(self, *args, **kwargs): - self.app.router.add_static(*args, **kwargs) + def add_static(self, prefix: str, path: str): + self.app.router.add_static(prefix, path) def start_server(self): if self.webserver_thread and not self.webserver_thread.is_alive(): @@ -68,7 +71,7 @@ class WebServerManager: ) @property - def is_running(self): + def is_running(self) -> bool: if not self.webserver_thread: return False return self.webserver_thread.is_running diff --git a/client/ayon_core/tools/tray/webserver/version.py b/client/ayon_core/tools/tray/webserver/version.py deleted file mode 100644 index 5becc17c04..0000000000 --- a/client/ayon_core/tools/tray/webserver/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.0.0" diff --git a/client/ayon_core/tools/tray/webserver/webserver_module.py b/client/ayon_core/tools/tray/webserver/webserver_module.py index 997b6f754c..0a19fd5b07 100644 --- a/client/ayon_core/tools/tray/webserver/webserver_module.py +++ b/client/ayon_core/tools/tray/webserver/webserver_module.py @@ -1,47 +1,62 @@ -"""WebServerAddon spawns aiohttp server in asyncio loop. +"""TrayWebserver spawns aiohttp server in asyncio loop. -Main usage of the module is in AYON tray where make sense to add ability -of other modules to add theirs routes. Module which would want use that -option must have implemented method `webserver_initialization` which must -expect `WebServerManager` object where is possible to add routes or paths -with handlers. +Usage is to add ability to register routes from addons, or for inner calls +of tray. Addon which would want use that option must have implemented method +webserver_initialization` which must expect `WebServerManager` object where +is possible to add routes or paths with handlers. WebServerManager is by default created only in tray. -It is possible to create server manager without using module logic at all -using `create_new_server_manager`. That can be handy for standalone scripts -with predefined host and port and separated routes and logic. - Running multiple servers in one process is not recommended and probably won't work as expected. It is because of few limitations connected to asyncio module. - -When module's `create_server_manager` is called it is also set environment -variable "AYON_WEBSERVER_URL". Which should lead to root access point -of server. """ import os import socket from ayon_core import resources -from ayon_core.addon import AYONAddon, ITrayService +from ayon_core.lib import Logger -from .version import __version__ +from .server import WebServerManager +from .host_console_listener import HostListener -class WebServerAddon(AYONAddon, ITrayService): - name = "webserver" - version = __version__ - label = "WebServer" - +class TrayWebserver: webserver_url_env = "AYON_WEBSERVER_URL" - def initialize(self, settings): - self._server_manager = None - self._host_listener = None - + def __init__(self, tray_manager): + self._log = None + self._tray_manager = tray_manager self._port = self.find_free_port() - self._webserver_url = None + + self._server_manager = WebServerManager(self._port, None) + + webserver_url = self._server_manager.url + self._webserver_url = webserver_url + + self._host_listener = HostListener(self, self._tray_manager) + + static_prefix = "/res" + self._server_manager.add_static(static_prefix, resources.RESOURCES_DIR) + statisc_url = "{}{}".format( + self._webserver_url, static_prefix + ) + + os.environ[self.webserver_url_env] = str(webserver_url) + os.environ["AYON_STATICS_SERVER"] = statisc_url + + # Deprecated + os.environ["OPENPYPE_WEBSERVER_URL"] = str(webserver_url) + os.environ["OPENPYPE_STATICS_SERVER"] = statisc_url + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger("TrayWebserver") + return self._log + + def add_route(self, *args, **kwargs): + self._server_manager.add_route(*args, **kwargs) @property def server_manager(self): @@ -73,72 +88,36 @@ class WebServerAddon(AYONAddon, ITrayService): """ return self._webserver_url - def connect_with_addons(self, enabled_modules): + def connect_with_addons(self, enabled_addons): if not self._server_manager: return - for module in enabled_modules: - if not hasattr(module, "webserver_initialization"): + for addon in enabled_addons: + if not hasattr(addon, "webserver_initialization"): continue try: - module.webserver_initialization(self._server_manager) + addon.webserver_initialization(self._server_manager) except Exception: self.log.warning( - ( - "Failed to connect module \"{}\" to webserver." - ).format(module.name), + f"Failed to connect addon \"{addon.name}\" to webserver.", exc_info=True ) - def tray_init(self): - self.create_server_manager() - self._add_resources_statics() - self._add_listeners() + def start(self): + self._start_server() - def tray_start(self): - self.start_server() + def stop(self): + self._stop_server() - def tray_exit(self): - self.stop_server() - - def start_server(self): + def _start_server(self): if self._server_manager is not None: self._server_manager.start_server() - def stop_server(self): + def _stop_server(self): if self._server_manager is not None: self._server_manager.stop_server() - @staticmethod - def create_new_server_manager(port=None, host=None): - """Create webserver manager for passed port and host. - - Args: - port(int): Port on which wil webserver listen. - host(str): Host name or IP address. Default is 'localhost'. - - Returns: - WebServerManager: Prepared manager. - """ - from .server import WebServerManager - - return WebServerManager(port, host) - - def create_server_manager(self): - if self._server_manager is not None: - return - - self._server_manager = self.create_new_server_manager(self._port) - self._server_manager.on_stop_callbacks.append( - self.set_service_failed_icon - ) - - webserver_url = self._server_manager.url - os.environ["OPENPYPE_WEBSERVER_URL"] = str(webserver_url) - os.environ[self.webserver_url_env] = str(webserver_url) - self._webserver_url = webserver_url - @staticmethod def find_free_port( port_from=None, port_to=None, exclude_ports=None, host=None @@ -193,20 +172,3 @@ class WebServerAddon(AYONAddon, ITrayService): break return found_port - - def _add_resources_statics(self): - static_prefix = "/res" - self._server_manager.add_static(static_prefix, resources.RESOURCES_DIR) - statisc_url = "{}{}".format( - self._webserver_url, static_prefix - ) - - os.environ["AYON_STATICS_SERVER"] = statisc_url - os.environ["OPENPYPE_STATICS_SERVER"] = statisc_url - - def _add_listeners(self): - from . import host_console_listener - - self._host_listener = host_console_listener.HostListener( - self._server_manager, self - ) From bdd79ea708ac767b2a58c8457c41aca221257597 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:51:38 +0200 Subject: [PATCH 030/144] rename webserver_module.py to webserver.py --- client/ayon_core/tools/tray/webserver/__init__.py | 2 +- .../tools/tray/webserver/{webserver_module.py => webserver.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename client/ayon_core/tools/tray/webserver/{webserver_module.py => webserver.py} (100%) diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index db7c2a7c77..92b5c54e43 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,5 +1,5 @@ from .structures import HostMsgAction -from .webserver_module import TrayWebserver +from .webserver import TrayWebserver __all__ = ( diff --git a/client/ayon_core/tools/tray/webserver/webserver_module.py b/client/ayon_core/tools/tray/webserver/webserver.py similarity index 100% rename from client/ayon_core/tools/tray/webserver/webserver_module.py rename to client/ayon_core/tools/tray/webserver/webserver.py From c0d878aa0382f1ae82659190516caf30663dae2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:52:48 +0200 Subject: [PATCH 031/144] added option to get services submenu via tray manager --- client/ayon_core/tools/tray/ui/tray.py | 34 ++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 3dd822e4c5..2b038bcb5d 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -67,12 +67,12 @@ class TrayManager: self._main_thread_callbacks = collections.deque() self._execution_in_progress = None self._closing = False + self._services_submenu = None @property def doubleclick_callback(self): """Double-click callback for Tray icon.""" - callback_name = self._addons_manager.doubleclick_callback - return self._addons_manager.doubleclick_callbacks.get(callback_name) + return self._addons_manager.get_doubleclick_callback() def execute_doubleclick(self): """Execute double click callback in main thread.""" @@ -103,26 +103,26 @@ class TrayManager: def initialize_addons(self): """Add addons to tray.""" - self._addons_manager.initialize(self.tray_widget.menu) + tray_menu = self.tray_widget.menu + self._addons_manager.initialize(tray_menu) - admin_submenu = ITrayAction.admin_submenu(self.tray_widget.menu) - self.tray_widget.menu.addMenu(admin_submenu) + admin_submenu = ITrayAction.admin_submenu(tray_menu) + tray_menu.addMenu(admin_submenu) # Add services if they are - services_submenu = ITrayService.services_submenu( - self.tray_widget.menu - ) - self.tray_widget.menu.addMenu(services_submenu) + services_submenu = ITrayService.services_submenu(tray_menu) + self._services_submenu = services_submenu + tray_menu.addMenu(services_submenu) # Add separator - self.tray_widget.menu.addSeparator() + tray_menu.addSeparator() self._add_version_item() # Add Exit action to menu exit_action = QtWidgets.QAction("Exit", self.tray_widget) exit_action.triggered.connect(self.tray_widget.exit) - self.tray_widget.menu.addAction(exit_action) + tray_menu.addAction(exit_action) # Tell each addon which addons were imported self._addons_manager.start_addons() @@ -147,6 +147,9 @@ class TrayManager: self.execute_in_main_thread(self._startup_validations) + def get_services_submenu(self): + return self._services_submenu + def restart(self): """Restart Tray tool. @@ -319,9 +322,10 @@ class TrayManager: self._update_check_timer.timeout.emit() def _add_version_item(self): + tray_menu = self.tray_widget.menu login_action = QtWidgets.QAction("Login", self.tray_widget) login_action.triggered.connect(self._on_ayon_login) - self.tray_widget.menu.addAction(login_action) + tray_menu.addAction(login_action) version_string = os.getenv("AYON_VERSION", "AYON Info") version_action = QtWidgets.QAction(version_string, self.tray_widget) @@ -333,9 +337,9 @@ class TrayManager: restart_action.triggered.connect(self._on_restart_action) restart_action.setVisible(False) - self.tray_widget.menu.addAction(version_action) - self.tray_widget.menu.addAction(restart_action) - self.tray_widget.menu.addSeparator() + tray_menu.addAction(version_action) + tray_menu.addAction(restart_action) + tray_menu.addSeparator() self._restart_action = restart_action From 996998d53cdecbd81bc7ae5972c884240c672ad1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:52:58 +0200 Subject: [PATCH 032/144] use addon variables over module variables --- client/ayon_core/addon/base.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 5cabf3e5e0..cd952edffc 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -923,20 +923,20 @@ class AddonsManager: report = {} time_start = time.time() prev_start_time = time_start - enabled_modules = self.get_enabled_addons() - self.log.debug("Has {} enabled modules.".format(len(enabled_modules))) - for module in enabled_modules: + enabled_addons = self.get_enabled_addons() + self.log.debug("Has {} enabled addons.".format(len(enabled_addons))) + for addon in enabled_addons: try: - if not is_func_marked(module.connect_with_addons): - module.connect_with_addons(enabled_modules) + if not is_func_marked(addon.connect_with_addons): + addon.connect_with_addons(enabled_addons) - elif hasattr(module, "connect_with_modules"): + elif hasattr(addon, "connect_with_modules"): self.log.warning(( "DEPRECATION WARNING: Addon '{}' still uses" " 'connect_with_modules' method. Please switch to use" " 'connect_with_addons' method." - ).format(module.name)) - module.connect_with_modules(enabled_modules) + ).format(addon.name)) + addon.connect_with_modules(enabled_addons) except Exception: self.log.error( @@ -945,7 +945,7 @@ class AddonsManager: ) now = time.time() - report[module.__class__.__name__] = now - prev_start_time + report[addon.__class__.__name__] = now - prev_start_time prev_start_time = now if self._report is not None: From 5498bccf8545d45151be2180167a5b4409a8633f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:02:46 +0200 Subject: [PATCH 033/144] tray is somewhat capable of hangling single tray running --- client/ayon_core/cli_commands.py | 5 +- client/ayon_core/tools/tray/__init__.py | 5 + client/ayon_core/tools/tray/addons_manager.py | 4 + client/ayon_core/tools/tray/lib.py | 195 ++++++++++++++++-- client/ayon_core/tools/tray/ui/tray.py | 45 +++- .../tools/tray/webserver/__init__.py | 2 + .../tray/webserver/host_console_listener.py | 2 - .../ayon_core/tools/tray/webserver/server.py | 19 ++ .../tools/tray/webserver/webserver.py | 10 +- 9 files changed, 254 insertions(+), 33 deletions(-) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 774ee3e847..9b19620e9a 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -12,10 +12,7 @@ class Commands: """ @staticmethod def launch_tray(): - from ayon_core.lib import Logger - from ayon_core.tools.tray.ui import main - - Logger.set_process_name("Tray") + from ayon_core.tools.tray import main main() diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index 534e7100f5..001b37e129 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,6 +1,11 @@ from .addons_manager import TrayAddonsManager +from .lib import ( + is_tray_running, + main, +) __all__ = ( "TrayAddonsManager", + "main", ) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index ad265298d0..5acf89c06d 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -146,6 +146,7 @@ class TrayAddonsManager(AddonsManager): def start_addons(self): self._tray_webserver.start() + report = {} time_start = time.time() prev_start_time = time_start @@ -185,3 +186,6 @@ class TrayAddonsManager(AddonsManager): ), exc_info=True ) + + def get_tray_webserver(self): + return self._tray_webserver diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 52e603daf0..ba16e5cbc5 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -1,8 +1,27 @@ -@@ -1,49 +0,0 @@ import os -from typing import Optional, Dict, Any +import json +import hashlib +import subprocess +import csv +import time +import signal +from typing import Optional, Dict, Tuple, Any import ayon_api +import requests + +from ayon_core.lib import Logger +from ayon_core.lib.local_settings import get_ayon_appdirs + + +class TrayState: + NOT_RUNNING = 0 + STARTING = 1 + RUNNING = 2 + + +class TrayIsRunningError(Exception): + pass def _get_default_server_url() -> str: @@ -13,38 +32,170 @@ def _get_default_variant() -> str: return ayon_api.get_default_settings_variant() -def get_tray_store_dir() -> str: - pass +def _get_server_and_variant( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> Tuple[str, str]: + if not server_url: + server_url = _get_default_server_url() + if not variant: + variant = _get_default_variant() + return server_url, variant -def get_tray_information( - sever_url: str, variant: str +def _windows_pid_is_running(pid: int) -> bool: + args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"] + output = subprocess.check_output(args) + csv_content = csv.DictReader(output.decode("utf-8").splitlines()) + # if "PID" not in csv_content.fieldnames: + # return False + for _ in csv_content: + return True + return False + + +def _create_tray_hash(server_url: str, variant: str) -> str: + data = f"{server_url}|{variant}" + return hashlib.sha256(data.encode()).hexdigest() + + +def get_tray_storage_dir() -> str: + return get_ayon_appdirs("tray") + + +def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]: + # TODO implement server side information + response = requests.get(f"{tray_url}/tray") + try: + response.raise_for_status() + except requests.HTTPError: + return None + return response.json() + + +def _get_tray_info_filepath( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> str: + hash_dir = get_tray_storage_dir() + server_url, variant = _get_server_and_variant(server_url, variant) + filename = _create_tray_hash(server_url, variant) + return os.path.join(hash_dir, filename) + + +def get_tray_file_info( + server_url: Optional[str] = None, + variant: Optional[str] = None ) -> Optional[Dict[str, Any]]: - pass - - -def validate_tray_server(server_url: str) -> bool: - tray_info = get_tray_information(server_url) - if tray_info is None: - return False - return True + filepath = _get_tray_info_filepath(server_url, variant) + if not os.path.exists(filepath): + return None + try: + with open(filepath, "r") as stream: + data = json.load(stream) + except Exception: + return None + return data def get_tray_server_url( server_url: Optional[str] = None, variant: Optional[str] = None ) -> Optional[str]: - if not server_url: - server_url = _get_default_server_url() - if not variant: - variant = _get_default_variant() + data = get_tray_file_info(server_url, variant) + if data is None: + return None + return data.get("url") + + +def set_tray_server_url(tray_url: str, started: bool): + filepath = _get_tray_info_filepath() + if os.path.exists(filepath): + info = get_tray_file_info() + if info.get("pid") != os.getpid(): + raise TrayIsRunningError("Tray is already running.") + os.makedirs(os.path.dirname(filepath), exist_ok=True) + data = { + "url": tray_url, + "pid": os.getpid(), + "started": started + } + with open(filepath, "w") as stream: + json.dump(data, stream) + + +def remove_tray_server_url(): + filepath = _get_tray_info_filepath() + if not os.path.exists(filepath): + return + with open(filepath, "r") as stream: + data = json.load(stream) + if data.get("pid") != os.getpid(): + return + os.remove(filepath) + + +def get_tray_information( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> Optional[Dict[str, Any]]: + tray_url = get_tray_server_url(server_url, variant) + return _get_tray_information(tray_url) + + +def get_tray_state( + server_url: Optional[str] = None, + variant: Optional[str] = None +): + file_info = get_tray_file_info(server_url, variant) + if file_info is None: + return TrayState.NOT_RUNNING + + if file_info.get("started") is False: + return TrayState.STARTING + + tray_url = file_info.get("url") + info = _get_tray_information(tray_url) + if not info: + # Remove the information as the tray is not running + remove_tray_server_url() + return TrayState.NOT_RUNNING + return TrayState.RUNNING def is_tray_running( server_url: Optional[str] = None, variant: Optional[str] = None ) -> bool: - server_url = get_tray_server_url(server_url, variant) - if server_url and validate_tray_server(server_url): - return True - return False + state = get_tray_state(server_url, variant) + return state != TrayState.NOT_RUNNING + + +def main(): + from ayon_core.tools.tray.ui import main + + Logger.set_process_name("Tray") + + state = get_tray_state() + if state == TrayState.RUNNING: + # TODO send some information to tray? + print("Tray is already running.") + return + + if state == TrayState.STARTING: + print("Tray is starting.") + return + # TODO try to handle stuck tray? + time.sleep(5) + state = get_tray_state() + if state == TrayState.RUNNING: + return + if state == TrayState.STARTING: + file_info = get_tray_file_info() or {} + pid = file_info.get("pid") + if pid is not None: + os.kill(pid, signal.SIGTERM) + remove_tray_server_url() + + main() + diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 2b038bcb5d..6900e80ed5 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -1,10 +1,12 @@ import os import sys +import time import collections import atexit - +import json import platform +from aiohttp.web_response import Response import ayon_api from qtpy import QtCore, QtGui, QtWidgets @@ -27,6 +29,11 @@ from ayon_core.tools.utils import ( get_ayon_qt_app, ) from ayon_core.tools.tray import TrayAddonsManager +from ayon_core.tools.tray.lib import ( + set_tray_server_url, + remove_tray_server_url, + TrayIsRunningError, +) from .info_widget import InfoWidget from .dialogs import ( @@ -68,6 +75,7 @@ class TrayManager: self._execution_in_progress = None self._closing = False self._services_submenu = None + self._start_time = time.time() @property def doubleclick_callback(self): @@ -105,6 +113,15 @@ class TrayManager: tray_menu = self.tray_widget.menu self._addons_manager.initialize(tray_menu) + webserver = self._addons_manager.get_tray_webserver() + try: + set_tray_server_url(webserver.webserver_url, False) + except TrayIsRunningError: + self.log.error("Tray is already running.") + self.exit() + return + + webserver.add_route("GET", "/tray", self._get_web_tray_info) admin_submenu = ITrayAction.admin_submenu(tray_menu) tray_menu.addMenu(admin_submenu) @@ -125,7 +142,15 @@ class TrayManager: tray_menu.addAction(exit_action) # Tell each addon which addons were imported - self._addons_manager.start_addons() + # TODO Capture only webserver issues (the only thing that can crash). + try: + self._addons_manager.start_addons() + except Exception: + self.log.error( + "Failed to start addons.", + exc_info=True + ) + return self.exit() # Print time report self._addons_manager.print_report() @@ -147,6 +172,8 @@ class TrayManager: self.execute_in_main_thread(self._startup_validations) + set_tray_server_url(webserver.webserver_url, True) + def get_services_submenu(self): return self._services_submenu @@ -213,6 +240,7 @@ class TrayManager: self.tray_widget.exit() def on_exit(self): + remove_tray_server_url() self._addons_manager.on_exit() def execute_in_main_thread(self, callback, *args, **kwargs): @@ -225,6 +253,19 @@ class TrayManager: return item + async def _get_web_tray_info(self, request): + return Response(text=json.dumps({ + "bundle": os.getenv("AYON_BUNDLE_NAME"), + "dev_mode": is_dev_mode_enabled(), + "staging_mode": is_staging_enabled(), + "addons": { + addon.name: addon.version + for addon in self._addons_manager.get_enabled_addons() + }, + "installer_version": os.getenv("AYON_VERSION"), + "running_time": time.time() - self._start_time, + })) + def _on_update_check_timer(self): try: bundles = ayon_api.get_bundles() diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index 92b5c54e43..938e7205b4 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,8 +1,10 @@ from .structures import HostMsgAction +from .base_routes import RestApiEndpoint from .webserver import TrayWebserver __all__ = ( "HostMsgAction", + "RestApiEndpoint", "TrayWebserver", ) diff --git a/client/ayon_core/tools/tray/webserver/host_console_listener.py b/client/ayon_core/tools/tray/webserver/host_console_listener.py index 3ec57d2598..2c1a7ae9b5 100644 --- a/client/ayon_core/tools/tray/webserver/host_console_listener.py +++ b/client/ayon_core/tools/tray/webserver/host_console_listener.py @@ -23,9 +23,7 @@ class IconType: class HostListener: def __init__(self, webserver, tray_manager): - self._window_per_id = {} self._tray_manager = tray_manager - self.webserver = webserver self._window_per_id = {} # dialogs per host name self._action_per_id = {} # QAction per host name diff --git a/client/ayon_core/tools/tray/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py index 2e0d1b258c..5b6e7e52d4 100644 --- a/client/ayon_core/tools/tray/webserver/server.py +++ b/client/ayon_core/tools/tray/webserver/server.py @@ -52,6 +52,25 @@ class WebServerManager: def add_static(self, prefix: str, path: str): self.app.router.add_static(prefix, path) + def add_addon_route( + self, + addon_name: str, + path: str, + request_method: str, + handler: Callable + ) -> str: + path = path.lstrip("/") + full_path = f"/addons/{addon_name}/{path}" + self.app.router.add_route(request_method, full_path, handler) + return full_path + + def add_addon_static( + self, addon_name: str, prefix: str, path: str + ) -> str: + full_path = f"/addons/{addon_name}/{prefix}" + self.app.router.add_static(full_path, path) + return full_path + def start_server(self): if self.webserver_thread and not self.webserver_thread.is_alive(): self.webserver_thread.start() diff --git a/client/ayon_core/tools/tray/webserver/webserver.py b/client/ayon_core/tools/tray/webserver/webserver.py index 0a19fd5b07..a013bdf19a 100644 --- a/client/ayon_core/tools/tray/webserver/webserver.py +++ b/client/ayon_core/tools/tray/webserver/webserver.py @@ -13,6 +13,7 @@ work as expected. It is because of few limitations connected to asyncio module. import os import socket +from typing import Callable from ayon_core import resources from ayon_core.lib import Logger @@ -39,7 +40,7 @@ class TrayWebserver: static_prefix = "/res" self._server_manager.add_static(static_prefix, resources.RESOURCES_DIR) statisc_url = "{}{}".format( - self._webserver_url, static_prefix + webserver_url, static_prefix ) os.environ[self.webserver_url_env] = str(webserver_url) @@ -55,8 +56,11 @@ class TrayWebserver: self._log = Logger.get_logger("TrayWebserver") return self._log - def add_route(self, *args, **kwargs): - self._server_manager.add_route(*args, **kwargs) + def add_route(self, request_method: str, path: str, handler: Callable): + self._server_manager.add_route(request_method, path, handler) + + def add_static(self, prefix: str, path: str): + self._server_manager.add_static(prefix, path) @property def server_manager(self): From 39cc1f2877f125ce47155be52837019f7ba36003 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:51:06 +0200 Subject: [PATCH 034/144] base of upload logic in integrate plugin --- client/ayon_core/plugins/publish/integrate.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index a2cf910fa6..8ab48ce9e5 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -6,6 +6,7 @@ import copy import clique import pyblish.api from ayon_api import ( + get_server_api_connection, get_attributes_for_type, get_product_by_name, get_version_by_name, @@ -25,6 +26,7 @@ from ayon_core.lib.file_transaction import ( DuplicateDestinationError ) from ayon_core.pipeline.publish import ( + get_publish_repre_path, KnownPublishError, get_publish_template_name, ) @@ -348,6 +350,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("{}".format(op_session.to_data())) op_session.commit() + self._upload_reviewable(project_name, version_entity["id"], instance) + # Backwards compatibility used in hero integration. # todo: can we avoid the need to store this? instance.data["published_representations"] = { @@ -984,6 +988,57 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "hash_type": "op3", } + def _upload_reviewable(self, project_name, version_id, instance): + uploaded_labels = set() + ayon_con = get_server_api_connection() + base_headers = ayon_con.get_headers() + endpoint = ( + f"/api/projects/{project_name}/versions/{version_id}/reviewables" + ) + for repre in instance.data["representations"]: + repre_tags = repre.get("tags") or [] + # Ignore representations that are not reviewable + if "webreview" not in repre_tags: + continue + + # exclude representations with are going to be published on farm + if "publish_on_farm" in repre_tags: + continue + + # Skip thumbnails + if repre.get("thumbnail") or "thumbnail" in repre_tags: + continue + + # include only thumbnail representations + repre_path = get_publish_repre_path( + instance, repre, False + ) + repre_ext = os.path.splitext(repre_path)[-1].lower() + + label = repre.get("outputName") + if not label: + # TODO how to label the reviewable if there is no output name? + label = "Main" + + # Make sure label is unique + orig_label = label + idx = 0 + while label in uploaded_labels: + idx += 1 + label = f"{orig_label}_{idx}" + + uploaded_labels.add(label) + + # Upload the reviewable + self.log.info(f"Uploading reviewable '{label}' ...") + headers = copy.deepcopy(base_headers) + ayon_con.upload_file( + f"/api/projects/{project_name}/versions/{version_id}/reviewables", + repre_path, + headers=headers + ) + + def _validate_path_in_project_roots(self, anatomy, file_path): """Checks if 'file_path' starts with any of the roots. From facf2425ecbb01883b6bd80f5b80e153e64c8df2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:51:22 +0200 Subject: [PATCH 035/144] mark openpype keys --- client/ayon_core/plugins/publish/integrate.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 8ab48ce9e5..3c29438f78 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -116,18 +116,19 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the database even if not used by the destination template db_representation_context_keys = [ "project", - "asset", "hierarchy", "folder", "task", "product", - "subset", - "family", "version", "representation", "username", "user", - "output" + "output", + # OpenPype keys - should be removed + "asset", # folder[name] + "subset", # product[name] + "family", # product[type] ] def process(self, instance): From fbe987c3f42a1c4299622e76d3705679c6b81546 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:55:08 +0200 Subject: [PATCH 036/144] don't store '_tray_manager' to traywebserver --- client/ayon_core/tools/tray/webserver/webserver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/webserver/webserver.py b/client/ayon_core/tools/tray/webserver/webserver.py index a013bdf19a..0a532305e3 100644 --- a/client/ayon_core/tools/tray/webserver/webserver.py +++ b/client/ayon_core/tools/tray/webserver/webserver.py @@ -27,7 +27,6 @@ class TrayWebserver: def __init__(self, tray_manager): self._log = None - self._tray_manager = tray_manager self._port = self.find_free_port() self._server_manager = WebServerManager(self._port, None) @@ -35,7 +34,7 @@ class TrayWebserver: webserver_url = self._server_manager.url self._webserver_url = webserver_url - self._host_listener = HostListener(self, self._tray_manager) + self._host_listener = HostListener(self, tray_manager) static_prefix = "/res" self._server_manager.add_static(static_prefix, resources.RESOURCES_DIR) From 3268240c6bd002b447ef96c36ff569aafab57ad1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 15:11:42 +0200 Subject: [PATCH 037/144] implemented 'get_media_mime_type' --- client/ayon_core/lib/__init__.py | 2 + client/ayon_core/lib/transcoding.py | 87 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 1f864284cd..12c391d867 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -109,6 +109,7 @@ from .transcoding import ( convert_ffprobe_fps_value, convert_ffprobe_fps_to_float, get_rescaled_command_arguments, + get_media_mime_type, ) from .plugin_tools import ( @@ -209,6 +210,7 @@ __all__ = [ "convert_ffprobe_fps_value", "convert_ffprobe_fps_to_float", "get_rescaled_command_arguments", + "get_media_mime_type", "compile_list_of_regexes", diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index bff28614ea..7ccd2ce819 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -6,6 +6,7 @@ import collections import tempfile import subprocess import platform +from typing import Optional import xml.etree.ElementTree @@ -1455,3 +1456,89 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): input_arg += ":ch={}".format(input_channels_str) return input_arg, channels_arg + + +def _get_media_mime_type_from_ftyp(content): + if content[8:10] == b"qt": + return "video/quicktime" + + if content[8:12] == b"isom": + return "video/mp4" + if content[8:12] in (b"M4V\x20", b"mp42"): + return "video/mp4v" + if content[8:13] in (b"3gpis"): + return "video/3gpp" + # ( + # b"avc1", b"iso2", b"isom", b"mmp4", b"mp41", b"mp71", + # b"msnv", b"ndas", b"ndsc", b"ndsh", b"ndsm", b"ndsp", b"ndss", + # b"ndxc", b"ndxh", b"ndxm", b"ndxp", b"ndxs" + # ) + return None + + +def get_media_mime_type(filepath: str) -> Optional[str]: + """Determine Mime-Type of a file. + + Args: + filepath (str): Path to file. + + Returns: + Optional[str]: Mime type or None if is unknown mime type. + + """ + if not filepath or not os.path.exists(filepath): + return None + + with open(filepath, "rb") as stream: + content = stream.read() + + content_len = len(content) + # Pre-validation (largest definition check) + # - hopefully there cannot be media defined in less than 12 bytes + if content_len < 12: + return None + + # FTYP + if content[4:8] == b"ftyp": + return _get_media_mime_type_from_ftyp(content) + + # BMP + if content[0:2] == b"BM": + return "image/bmp" + + # Tiff + if content[0:2] in (b"MM", b"II"): + return "tiff" + + # PNG + if content[0:4] == b"\211PNG": + return "image/png" + + # SVG + if b'xmlns="http://www.w3.org/2000/svg"' in content: + return "image/svg+xml" + + # JPEG, JFIF or Exif + if ( + content[0:4] == b"\xff\xd8\xff\xdb" + or content[6:10] in (b"JFIF", b"Exif") + ): + return "image/jpeg" + + # Webp + if content[0:4] == b"RIFF" and content[8:12] == b"WEBP": + return "image/webp" + + # Gif + if content[0:6] in (b"GIF87a", b"GIF89a"): + return "gif" + + # Adobe PhotoShop file (8B > Adobe, PS > PhotoShop) + if content[0:4] == b"8BPS": + return "image/vnd.adobe.photoshop" + + # Windows ICO > this might be wild guess as multiple files can start + # with this header + if content[0:4] == b"\x00\x00\x01\x00": + return "image/x-icon" + return None From b87b9cc35493920af6a4da4bfb53bb6cf7002df0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 15:12:05 +0200 Subject: [PATCH 038/144] properly implement upload --- client/ayon_core/plugins/publish/integrate.py | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 3c29438f78..ababbd88fc 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -20,7 +20,7 @@ from ayon_api.operations import ( ) from ayon_api.utils import create_entity_id -from ayon_core.lib import source_hash +from ayon_core.lib import source_hash, get_media_mime_type from ayon_core.lib.file_transaction import ( FileTransaction, DuplicateDestinationError @@ -990,12 +990,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): } def _upload_reviewable(self, project_name, version_id, instance): - uploaded_labels = set() ayon_con = get_server_api_connection() base_headers = ayon_con.get_headers() - endpoint = ( - f"/api/projects/{project_name}/versions/{version_id}/reviewables" - ) + + uploaded_labels = set() for repre in instance.data["representations"]: repre_tags = repre.get("tags") or [] # Ignore representations that are not reviewable @@ -1014,13 +1012,25 @@ class IntegrateAsset(pyblish.api.InstancePlugin): repre_path = get_publish_repre_path( instance, repre, False ) - repre_ext = os.path.splitext(repre_path)[-1].lower() + if not repre_path or not os.path.exists(repre_path): + # TODO log skipper path + continue + content_type = get_media_mime_type(repre_path) + if not content_type: + self.log.warning("Could not determine Content-Type") + continue + + # Use output name as label if available label = repre.get("outputName") - if not label: - # TODO how to label the reviewable if there is no output name? - label = "Main" + query = "" + if label: + query = f"?label={label}" + endpoint = ( + f"/api/projects/{project_name}" + f"/versions/{version_id}/reviewables{query}" + ) # Make sure label is unique orig_label = label idx = 0 @@ -1033,13 +1043,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Upload the reviewable self.log.info(f"Uploading reviewable '{label}' ...") headers = copy.deepcopy(base_headers) + headers["Content-Type"] = content_type + headers["x-file-name"] = os.path.basename(repre_path) + self.log.info(f"Uploading reviewable {repre_path}") ayon_con.upload_file( - f"/api/projects/{project_name}/versions/{version_id}/reviewables", + endpoint, repre_path, headers=headers ) - def _validate_path_in_project_roots(self, anatomy, file_path): """Checks if 'file_path' starts with any of the roots. From 8bac6f0277a10967a7636b37181781c351e066dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:15:16 +0200 Subject: [PATCH 039/144] fix upload method --- client/ayon_core/plugins/publish/integrate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index ababbd88fc..18cd3e5cdd 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -11,6 +11,7 @@ from ayon_api import ( get_product_by_name, get_version_by_name, get_representations, + RequestTypes, ) from ayon_api.operations import ( OperationsSession, @@ -1028,7 +1029,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): query = f"?label={label}" endpoint = ( - f"/api/projects/{project_name}" + f"/projects/{project_name}" f"/versions/{version_id}/reviewables{query}" ) # Make sure label is unique @@ -1049,7 +1050,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ayon_con.upload_file( endpoint, repre_path, - headers=headers + headers=headers, + request_type=RequestTypes.post, ) def _validate_path_in_project_roots(self, anatomy, file_path): From 595aae2f416f1ffb67830efc473c9890de9803c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:15:37 +0200 Subject: [PATCH 040/144] modified default extract review settings --- server/settings/publish_plugins.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 36bb3f7340..987eefb39e 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1011,7 +1011,8 @@ DEFAULT_PUBLISH_VALUES = { "ext": "png", "tags": [ "ftrackreview", - "kitsureview" + "kitsureview", + "webreview" ], "burnins": [], "ffmpeg_args": { @@ -1051,7 +1052,8 @@ DEFAULT_PUBLISH_VALUES = { "tags": [ "burnin", "ftrackreview", - "kitsureview" + "kitsureview", + "webreview" ], "burnins": [], "ffmpeg_args": { @@ -1063,7 +1065,10 @@ DEFAULT_PUBLISH_VALUES = { "output": [ "-pix_fmt yuv420p", "-crf 18", - "-intra" + "-c:a acc", + "-b:a 192k", + "-g 1", + "-movflags faststart" ] }, "filter": { From 194944e86c8f7a6a543025285a64d2031a9ea1ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:35:39 +0200 Subject: [PATCH 041/144] validate supported version --- client/ayon_core/plugins/publish/integrate.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 18cd3e5cdd..ce8a955911 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -992,6 +992,14 @@ class IntegrateAsset(pyblish.api.InstancePlugin): def _upload_reviewable(self, project_name, version_id, instance): ayon_con = get_server_api_connection() + major, minor, _, _, _ = ayon_con.get_server_version_tuple() + if (major, minor) < (1, 3): + self.log.info( + "Skipping reviewable upload, supported from server 1.3.x." + f" User server version {ayon_con.get_server_version()}" + ) + return + base_headers = ayon_con.get_headers() uploaded_labels = set() From bb93a744bc19b0285e80f90c72df9de215ecc4d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:34:08 +0200 Subject: [PATCH 042/144] fix possible clash of content type --- client/ayon_core/plugins/publish/integrate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index ce8a955911..ebea0447b2 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -1000,8 +1000,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ) return - base_headers = ayon_con.get_headers() - uploaded_labels = set() for repre in instance.data["representations"]: repre_tags = repre.get("tags") or [] @@ -1051,8 +1049,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Upload the reviewable self.log.info(f"Uploading reviewable '{label}' ...") - headers = copy.deepcopy(base_headers) - headers["Content-Type"] = content_type + + headers = ayon_con.get_headers(content_type) headers["x-file-name"] = os.path.basename(repre_path) self.log.info(f"Uploading reviewable {repre_path}") ayon_con.upload_file( From 432ead8178e7f75229c67924e411f2ac6d3055e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:54:12 +0200 Subject: [PATCH 043/144] removed invalid 3gpp --- client/ayon_core/lib/transcoding.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 7ccd2ce819..ead8b621b9 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1466,8 +1466,6 @@ def _get_media_mime_type_from_ftyp(content): return "video/mp4" if content[8:12] in (b"M4V\x20", b"mp42"): return "video/mp4v" - if content[8:13] in (b"3gpis"): - return "video/3gpp" # ( # b"avc1", b"iso2", b"isom", b"mmp4", b"mp41", b"mp71", # b"msnv", b"ndas", b"ndsc", b"ndsh", b"ndsm", b"ndsp", b"ndss", From 2af09665865b33b6104ceb23fa4c415d3fe9a4ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:02:21 +0200 Subject: [PATCH 044/144] Fix support for `ayon+settings://core/tools/loader/product_type_filter_profiles` in Loader UI --- client/ayon_core/tools/loader/abstract.py | 21 +++++++++++ client/ayon_core/tools/loader/control.py | 36 +++++++++++++++++-- .../tools/loader/ui/product_types_widget.py | 20 +++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 6a68af1eb5..e7e8488d05 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import List from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -346,6 +347,16 @@ class ActionItem: return cls(**data) +class ProductTypesFilter: + """Product types filter. + + Defines the filtering for product types. + """ + def __init__(self, product_types: List[str], is_include: bool): + self.product_types: List[str] = product_types + self.is_include: bool = is_include + + class _BaseLoaderController(ABC): """Base loader controller abstraction. @@ -1006,3 +1017,13 @@ class FrontendLoaderController(_BaseLoaderController): """ pass + + @abstractmethod + def get_current_context_product_types_filter(self): + """Return product type filter for current context. + + Returns: + ProductTypesFilter: Product type filter for current context + """ + + pass diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index f4b00e985f..085b1a0b31 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -3,7 +3,10 @@ import uuid import ayon_api -from ayon_core.lib import NestedCacheItem, CacheItem +from ayon_core.settings import get_current_project_settings +from ayon_core.pipeline import get_current_host_name +from ayon_core.pipeline.context_tools import get_current_task_entity +from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import Anatomy, get_current_context from ayon_core.host import ILoadHost @@ -13,7 +16,11 @@ from ayon_core.tools.common_models import ( ThumbnailsModel, ) -from .abstract import BackendLoaderController, FrontendLoaderController +from .abstract import ( + BackendLoaderController, + FrontendLoaderController, + ProductTypesFilter +) from .models import ( SelectionModel, ProductsModel, @@ -425,3 +432,28 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def _emit_event(self, topic, data=None): self._event_system.emit(topic, data or {}, "controller") + + def get_current_context_product_types_filter(self): + context = get_current_context() + # There may be cases where there is no current context, like + # Tray Loader so we only do this when we have a context + if all(context.values()): + settings = get_current_project_settings() + profiles = settings["core"]["tools"]["loader"]["product_type_filter_profiles"] # noqa + if profiles: + task_entity = get_current_task_entity(fields={"taskType"}) + profile = filter_profiles(profiles, key_values={ + "hosts": get_current_host_name(), + "task_types": (task_entity or {}).get("taskType") + }) + if profile: + return ProductTypesFilter( + is_include=profile["is_include"], + product_types=profile["filter_product_types"] + ) + + # Default to all as allowed + return ProductTypesFilter( + is_include=False, + product_types=[] + ) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 180994fd7f..4e024c4417 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -151,6 +151,7 @@ class ProductTypesView(QtWidgets.QListView): ) self._controller = controller + self._refresh_product_types_filter = False self._product_types_model = product_types_model self._product_types_proxy_model = product_types_proxy_model @@ -162,7 +163,26 @@ class ProductTypesView(QtWidgets.QListView): project_name = event["project_name"] self._product_types_model.refresh(project_name) + def showEvent(self, event): + self._refresh_product_types_filter = True + super().showEvent(event) + def _on_refresh_finished(self): + + # Apply product types filter + if self._refresh_product_types_filter: + product_types_filter = ( + self._controller.get_current_context_product_types_filter() + ) + if product_types_filter.is_include: + self._on_disable_all() + else: + self._on_enable_all() + self._product_types_model.change_states( + product_types_filter.is_include, + product_types_filter.product_types + ) + self.filter_changed.emit() def _on_filter_change(self): From 674375093df91cbe5d1046a2a4f7fcc85f171277 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:02:57 +0200 Subject: [PATCH 045/144] Remove empty default product type filter --- server/settings/tools.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 3ed12d3d0a..ca19d495f8 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -499,14 +499,7 @@ DEFAULT_TOOLS_VALUES = { "workfile_lock_profiles": [] }, "loader": { - "product_type_filter_profiles": [ - { - "hosts": [], - "task_types": [], - "is_include": True, - "filter_product_types": [] - } - ] + "product_type_filter_profiles": [] }, "publish": { "template_name_profiles": [ From ef1f94016a0a220ede9d2af6d9945453c15c769a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:04:47 +0200 Subject: [PATCH 046/144] Add missing `imagesequence` product type --- server/settings/tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/tools.py b/server/settings/tools.py index ca19d495f8..62674eee2c 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -195,6 +195,7 @@ def _product_types_enum(): "editorial", "gizmo", "image", + "imagesequence", "layout", "look", "matchmove", From 9106fbba5d8b1edf4457a8b54da0e417e69a5f92 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:05:16 +0200 Subject: [PATCH 047/144] Remove `usdShade` product type that does not actually exist in AYON --- server/settings/tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 62674eee2c..9368e29990 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -213,7 +213,6 @@ def _product_types_enum(): "setdress", "take", "usd", - "usdShade", "vdbcache", "vrayproxy", "workfile", From 7b01a73e7eb7716a411ef3a1bc10dd7ca2454f91 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:08:11 +0200 Subject: [PATCH 048/144] support older ayon api --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index ebea0447b2..ab86855b10 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -11,8 +11,8 @@ from ayon_api import ( get_product_by_name, get_version_by_name, get_representations, - RequestTypes, ) +from ayon_api.server_api import RequestTypes from ayon_api.operations import ( OperationsSession, new_product_entity, From a6a65418e3a039a4b85995edaa4372d511ce1647 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:12:36 +0200 Subject: [PATCH 049/144] duplicated code to integrate hero version --- .../plugins/publish/integrate_hero_version.py | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 4fb8b886a9..3e85226f75 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -10,10 +10,12 @@ from ayon_api.operations import ( OperationsSession, new_version_entity, ) +from ayon_api.server_api import RequestTypes from ayon_api.utils import create_entity_id -from ayon_core.lib import create_hard_link, source_hash +from ayon_core.lib import create_hard_link, source_hash, get_media_mime_type from ayon_core.pipeline.publish import ( + get_publish_repre_path, get_publish_template_name, OptionalPyblishPluginMixin, ) @@ -475,6 +477,9 @@ class IntegrateHeroVersion( ) op_session.commit() + self._upload_reviewable( + project_name, new_hero_version["id"], instance + ) # Remove backuped previous hero if ( @@ -672,3 +677,73 @@ class IntegrateHeroVersion( file_name = os.path.basename(value) file_name, _ = os.path.splitext(file_name) return file_name + + def _upload_reviewable(self, project_name, version_id, instance): + ayon_con = ayon_api.get_server_api_connection() + major, minor, _, _, _ = ayon_con.get_server_version_tuple() + if (major, minor) < (1, 3): + self.log.info( + "Skipping reviewable upload, supported from server 1.3.x." + f" User server version {ayon_con.get_server_version()}" + ) + return + + uploaded_labels = set() + for repre in instance.data["representations"]: + repre_tags = repre.get("tags") or [] + # Ignore representations that are not reviewable + if "webreview" not in repre_tags: + continue + + # exclude representations with are going to be published on farm + if "publish_on_farm" in repre_tags: + continue + + # Skip thumbnails + if repre.get("thumbnail") or "thumbnail" in repre_tags: + continue + + # include only thumbnail representations + repre_path = get_publish_repre_path( + instance, repre, False + ) + if not repre_path or not os.path.exists(repre_path): + # TODO log skipper path + continue + + content_type = get_media_mime_type(repre_path) + if not content_type: + self.log.warning("Could not determine Content-Type") + continue + + # Use output name as label if available + label = repre.get("outputName") + query = "" + if label: + query = f"?label={label}" + + endpoint = ( + f"/projects/{project_name}" + f"/versions/{version_id}/reviewables{query}" + ) + # Make sure label is unique + orig_label = label + idx = 0 + while label in uploaded_labels: + idx += 1 + label = f"{orig_label}_{idx}" + + uploaded_labels.add(label) + + # Upload the reviewable + self.log.info(f"Uploading reviewable '{label}' ...") + + headers = ayon_con.get_headers(content_type) + headers["x-file-name"] = os.path.basename(repre_path) + self.log.info(f"Uploading reviewable {repre_path}") + ayon_con.upload_file( + endpoint, + repre_path, + headers=headers, + request_type=RequestTypes.post, + ) From becf14ed68a5aa2d60a9dca712972baef0194325 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:34:37 +0200 Subject: [PATCH 050/144] Update client/ayon_core/tools/loader/control.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/control.py | 58 +++++++++++++++--------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 085b1a0b31..fa0443d876 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -433,27 +433,43 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def _emit_event(self, topic, data=None): self._event_system.emit(topic, data or {}, "controller") - def get_current_context_product_types_filter(self): - context = get_current_context() - # There may be cases where there is no current context, like - # Tray Loader so we only do this when we have a context - if all(context.values()): - settings = get_current_project_settings() - profiles = settings["core"]["tools"]["loader"]["product_type_filter_profiles"] # noqa - if profiles: - task_entity = get_current_task_entity(fields={"taskType"}) - profile = filter_profiles(profiles, key_values={ - "hosts": get_current_host_name(), - "task_types": (task_entity or {}).get("taskType") - }) - if profile: - return ProductTypesFilter( - is_include=profile["is_include"], - product_types=profile["filter_product_types"] - ) - - # Default to all as allowed - return ProductTypesFilter( + def get_product_types_filter(self, project_name): + output = ProductTypesFilter( is_include=False, product_types=[] ) + # Without host is not determined context + if self._host is None: + return output + + context = self.get_current_context() + if ( + not all(context.values()) + or context["project_name"] != project_name + ): + return output + settings = get_current_project_settings() + profiles = ( + settings + ["core"] + ["tools"] + ["loader"] + ["product_type_filter_profiles"] + ) + if not profiles: + return output + task_entity = get_current_task_entity(fields={"taskType"}) + host_name = getattr(self._host, "name", get_current_host_name()) + profile = filter_profiles( + profiles, + { + "hosts": host_name, + "task_types": (task_entity or {}).get("taskType") + } + ) + if profile: + output = ProductTypesFilter( + is_include=profile["is_include"], + product_types=profile["filter_product_types"] + ) + return output From 7105ca8d736f351738fcb391a213193d0f835a83 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:36:13 +0200 Subject: [PATCH 051/144] Refactor method --- client/ayon_core/tools/loader/abstract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index e7e8488d05..dfc83cfc20 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1019,8 +1019,11 @@ class FrontendLoaderController(_BaseLoaderController): pass @abstractmethod - def get_current_context_product_types_filter(self): - """Return product type filter for current context. + def get_product_types_filter(self, project_name): + """Return product type filter for project name (and current context). + + Args: + project_name (str): Project name. Returns: ProductTypesFilter: Product type filter for current context From d8fcc4c85cd131b8a18f5162fcb8724435dc7f9d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:49:44 +0200 Subject: [PATCH 052/144] Fix for refactored method --- client/ayon_core/tools/loader/ui/product_types_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 4e024c4417..5401e8830e 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -171,8 +171,9 @@ class ProductTypesView(QtWidgets.QListView): # Apply product types filter if self._refresh_product_types_filter: + project_name = self._controller.get_selected_project_name() product_types_filter = ( - self._controller.get_current_context_product_types_filter() + self._controller.get_product_types_filter(project_name) ) if product_types_filter.is_include: self._on_disable_all() From 88959b2c54a5a9569d9e426a29587bd829c3b72f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 14:04:38 +0200 Subject: [PATCH 053/144] Move product types filter logic to the model --- .../tools/loader/ui/product_types_widget.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 5401e8830e..ff62ec0bd5 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -71,6 +71,21 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): self._refreshing = False self.refreshed.emit() + def reset_product_types_filter(self): + + project_name = self._controller.get_selected_project_name() + product_types_filter = ( + self._controller.get_product_types_filter(project_name) + ) + if product_types_filter.is_include: + self.change_state_for_all(False) + else: + self.change_state_for_all(True) + self.change_states( + product_types_filter.is_include, + product_types_filter.product_types + ) + def setData(self, index, value, role=None): checkstate_changed = False if role is None: @@ -169,20 +184,9 @@ class ProductTypesView(QtWidgets.QListView): def _on_refresh_finished(self): - # Apply product types filter + # Apply product types filter on first show if self._refresh_product_types_filter: - project_name = self._controller.get_selected_project_name() - product_types_filter = ( - self._controller.get_product_types_filter(project_name) - ) - if product_types_filter.is_include: - self._on_disable_all() - else: - self._on_enable_all() - self._product_types_model.change_states( - product_types_filter.is_include, - product_types_filter.product_types - ) + self._product_types_model.reset_product_types_filter() self.filter_changed.emit() From f61675b10e85c309c33a794fd5911c45af788425 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:05:24 +0200 Subject: [PATCH 054/144] separate upload review to separated plugin --- client/ayon_core/plugins/publish/integrate.py | 75 +------------- .../plugins/publish/integrate_hero_version.py | 80 +-------------- .../plugins/publish/integrate_review.py | 98 +++++++++++++++++++ 3 files changed, 103 insertions(+), 150 deletions(-) create mode 100644 client/ayon_core/plugins/publish/integrate_review.py diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index ab86855b10..6837145f5d 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -6,13 +6,11 @@ import copy import clique import pyblish.api from ayon_api import ( - get_server_api_connection, get_attributes_for_type, get_product_by_name, get_version_by_name, get_representations, ) -from ayon_api.server_api import RequestTypes from ayon_api.operations import ( OperationsSession, new_product_entity, @@ -21,13 +19,12 @@ from ayon_api.operations import ( ) from ayon_api.utils import create_entity_id -from ayon_core.lib import source_hash, get_media_mime_type +from ayon_core.lib import source_hash from ayon_core.lib.file_transaction import ( FileTransaction, DuplicateDestinationError ) from ayon_core.pipeline.publish import ( - get_publish_repre_path, KnownPublishError, get_publish_template_name, ) @@ -990,76 +987,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "hash_type": "op3", } - def _upload_reviewable(self, project_name, version_id, instance): - ayon_con = get_server_api_connection() - major, minor, _, _, _ = ayon_con.get_server_version_tuple() - if (major, minor) < (1, 3): - self.log.info( - "Skipping reviewable upload, supported from server 1.3.x." - f" User server version {ayon_con.get_server_version()}" - ) - return - - uploaded_labels = set() - for repre in instance.data["representations"]: - repre_tags = repre.get("tags") or [] - # Ignore representations that are not reviewable - if "webreview" not in repre_tags: - continue - - # exclude representations with are going to be published on farm - if "publish_on_farm" in repre_tags: - continue - - # Skip thumbnails - if repre.get("thumbnail") or "thumbnail" in repre_tags: - continue - - # include only thumbnail representations - repre_path = get_publish_repre_path( - instance, repre, False - ) - if not repre_path or not os.path.exists(repre_path): - # TODO log skipper path - continue - - content_type = get_media_mime_type(repre_path) - if not content_type: - self.log.warning("Could not determine Content-Type") - continue - - # Use output name as label if available - label = repre.get("outputName") - query = "" - if label: - query = f"?label={label}" - - endpoint = ( - f"/projects/{project_name}" - f"/versions/{version_id}/reviewables{query}" - ) - # Make sure label is unique - orig_label = label - idx = 0 - while label in uploaded_labels: - idx += 1 - label = f"{orig_label}_{idx}" - - uploaded_labels.add(label) - - # Upload the reviewable - self.log.info(f"Uploading reviewable '{label}' ...") - - headers = ayon_con.get_headers(content_type) - headers["x-file-name"] = os.path.basename(repre_path) - self.log.info(f"Uploading reviewable {repre_path}") - ayon_con.upload_file( - endpoint, - repre_path, - headers=headers, - request_type=RequestTypes.post, - ) - def _validate_path_in_project_roots(self, anatomy, file_path): """Checks if 'file_path' starts with any of the roots. diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 3e85226f75..2163596864 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -10,12 +10,10 @@ from ayon_api.operations import ( OperationsSession, new_version_entity, ) -from ayon_api.server_api import RequestTypes from ayon_api.utils import create_entity_id -from ayon_core.lib import create_hard_link, source_hash, get_media_mime_type +from ayon_core.lib import create_hard_link, source_hash from ayon_core.pipeline.publish import ( - get_publish_repre_path, get_publish_template_name, OptionalPyblishPluginMixin, ) @@ -267,6 +265,9 @@ class IntegrateHeroVersion( project_name, "version", new_hero_version ) + # Store hero entity to 'instance.data' + instance.data["heroVersionEntity"] = new_hero_version + # Separate old representations into `to replace` and `to delete` old_repres_to_replace = {} old_repres_to_delete = {} @@ -477,9 +478,6 @@ class IntegrateHeroVersion( ) op_session.commit() - self._upload_reviewable( - project_name, new_hero_version["id"], instance - ) # Remove backuped previous hero if ( @@ -677,73 +675,3 @@ class IntegrateHeroVersion( file_name = os.path.basename(value) file_name, _ = os.path.splitext(file_name) return file_name - - def _upload_reviewable(self, project_name, version_id, instance): - ayon_con = ayon_api.get_server_api_connection() - major, minor, _, _, _ = ayon_con.get_server_version_tuple() - if (major, minor) < (1, 3): - self.log.info( - "Skipping reviewable upload, supported from server 1.3.x." - f" User server version {ayon_con.get_server_version()}" - ) - return - - uploaded_labels = set() - for repre in instance.data["representations"]: - repre_tags = repre.get("tags") or [] - # Ignore representations that are not reviewable - if "webreview" not in repre_tags: - continue - - # exclude representations with are going to be published on farm - if "publish_on_farm" in repre_tags: - continue - - # Skip thumbnails - if repre.get("thumbnail") or "thumbnail" in repre_tags: - continue - - # include only thumbnail representations - repre_path = get_publish_repre_path( - instance, repre, False - ) - if not repre_path or not os.path.exists(repre_path): - # TODO log skipper path - continue - - content_type = get_media_mime_type(repre_path) - if not content_type: - self.log.warning("Could not determine Content-Type") - continue - - # Use output name as label if available - label = repre.get("outputName") - query = "" - if label: - query = f"?label={label}" - - endpoint = ( - f"/projects/{project_name}" - f"/versions/{version_id}/reviewables{query}" - ) - # Make sure label is unique - orig_label = label - idx = 0 - while label in uploaded_labels: - idx += 1 - label = f"{orig_label}_{idx}" - - uploaded_labels.add(label) - - # Upload the reviewable - self.log.info(f"Uploading reviewable '{label}' ...") - - headers = ayon_con.get_headers(content_type) - headers["x-file-name"] = os.path.basename(repre_path) - self.log.info(f"Uploading reviewable {repre_path}") - ayon_con.upload_file( - endpoint, - repre_path, - headers=headers, - request_type=RequestTypes.post, - ) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py new file mode 100644 index 0000000000..1c62f25d94 --- /dev/null +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -0,0 +1,98 @@ +import os + +import pyblish.api +import ayon_api +from ayon_api.server_api import RequestTypes + +from ayon_core.lib import get_media_mime_type +from ayon_core.pipeline.publish import get_publish_repre_path + + +class IntegrateAYONReview(pyblish.api.InstancePlugin): + label = "Integrate AYON Review" + # Must happen after IntegrateNew + order = pyblish.api.IntegratorOrder + 0.15 + + def process(self, instance): + project_name = instance.context.data["projectName"] + src_version_entity = instance.data.get("versionEntity") + src_hero_version_entity = instance.data.get("heroVersionEntity") + for version_entity in ( + src_version_entity, + src_hero_version_entity, + ): + if not version_entity: + continue + + version_id = version_entity["id"] + self._upload_reviewable(project_name, version_id, instance) + + def _upload_reviewable(self, project_name, version_id, instance): + ayon_con = ayon_api.get_server_api_connection() + major, minor, _, _, _ = ayon_con.get_server_version_tuple() + if (major, minor) < (1, 3): + self.log.info( + "Skipping reviewable upload, supported from server 1.3.x." + f" User server version {ayon_con.get_server_version()}" + ) + return + + uploaded_labels = set() + for repre in instance.data["representations"]: + repre_tags = repre.get("tags") or [] + # Ignore representations that are not reviewable + if "webreview" not in repre_tags: + continue + + # exclude representations with are going to be published on farm + if "publish_on_farm" in repre_tags: + continue + + # Skip thumbnails + if repre.get("thumbnail") or "thumbnail" in repre_tags: + continue + + # include only thumbnail representations + repre_path = get_publish_repre_path( + instance, repre, False + ) + if not repre_path or not os.path.exists(repre_path): + # TODO log skipper path + continue + + content_type = get_media_mime_type(repre_path) + if not content_type: + self.log.warning("Could not determine Content-Type") + continue + + # Use output name as label if available + label = repre.get("outputName") + query = "" + if label: + query = f"?label={label}" + + endpoint = ( + f"/projects/{project_name}" + f"/versions/{version_id}/reviewables{query}" + ) + # Make sure label is unique + orig_label = label + idx = 0 + while label in uploaded_labels: + idx += 1 + label = f"{orig_label}_{idx}" + + uploaded_labels.add(label) + + # Upload the reviewable + self.log.info(f"Uploading reviewable '{label}' ...") + + headers = ayon_con.get_headers(content_type) + headers["x-file-name"] = os.path.basename(repre_path) + self.log.info(f"Uploading reviewable {repre_path}") + ayon_con.upload_file( + endpoint, + repre_path, + headers=headers, + request_type=RequestTypes.post, + ) \ No newline at end of file From eda080d86d987775697728949973019d8d2c1a1d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 15:59:16 +0200 Subject: [PATCH 055/144] Added profile to filter environment variables on farm --- server/settings/main.py | 46 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 40e16e7e91..1329f465e0 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -169,6 +169,46 @@ class VersionStartCategoryModel(BaseSettingsModel): ) +class EnvironmentReplacementModel(BaseSettingsModel): + environment_key: str = SettingsField("", title="Enviroment variable") + pattern: str = SettingsField("", title="Pattern") + replacement: str = SettingsField("", title="Replacement") + + +class FilterFarmEnvironmentModel(BaseSettingsModel): + _layout = "expanded" + + hosts: list[str] = SettingsField( + default_factory=list, + title="Host names" + ) + + task_types: list[str] = SettingsField( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + + task_names: list[str] = SettingsField( + default_factory=list, + title="Task names" + ) + + folders: list[str] = SettingsField( + default_factory=list, + title="Folders" + ) + + skip_environment: list[str] = SettingsField( + default_factory=list, + title="Skip environment variables" + ) + replace_in_environment: list[EnvironmentReplacementModel] = SettingsField( + default_factory=list, + title="Replace values in environment" + ) + + class CoreSettings(BaseSettingsModel): studio_name: str = SettingsField("", title="Studio name", scope=["studio"]) studio_code: str = SettingsField("", title="Studio code", scope=["studio"]) @@ -219,6 +259,9 @@ class CoreSettings(BaseSettingsModel): title="Project environments", section="---" ) + filter_farm_environment: list[FilterFarmEnvironmentModel] = SettingsField( + default_factory=list, + ) @validator( "environments", @@ -313,5 +356,6 @@ DEFAULT_VALUES = { "project_environments": json.dumps( {}, indent=4 - ) + ), + "filter_farm_environment": [], } From 274ed655e9631aacc616695028eb29b59637b226 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:00:14 +0200 Subject: [PATCH 056/144] Added hook filtering farm environment variables Should be triggered only on farm. Used to modify env var on farm machines like license path etc. --- .../hooks/pre_filter_farm_environments.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 client/ayon_core/hooks/pre_filter_farm_environments.py diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py new file mode 100644 index 0000000000..9a52f53950 --- /dev/null +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -0,0 +1,78 @@ +import copy +import re + +from ayon_applications import PreLaunchHook, LaunchTypes +from ayon_core.lib import filter_profiles + + +class FilterFarmEnvironments(PreLaunchHook): + """Filter or modify calculated environment variables for farm rendering. + + This hook must run last, only after all other hooks are finished to get + correct environment for launch context. + + Implemented modifications to self.launch_context.env: + - skipping (list) of environment variable keys + - removing value in environment variable: + - supports regular expression in pattern + - doesn't remove env var if value empty! + """ + order = 1000 + + launch_types = {LaunchTypes.farm_publish} + + def execute(self): + data = self.launch_context.data + project_settings = data["project_settings"] + filter_env_profiles = ( + project_settings["core"]["filter_farm_environment"]) + + if not filter_env_profiles: + self.log.debug("No profiles found for env var filtering") + return + + task_entity = data["task_entity"] + + filter_data = { + "hosts": self.host_name, + "task_types": task_entity["taskType"], + "tasks": task_entity["name"], + "folders": data["folder_path"] + } + matching_profile = filter_profiles( + filter_env_profiles, filter_data, logger=self.log + ) + if not matching_profile: + self.log.debug("No matching profile found for env var filtering " + f"for {filter_data}") + return + + calculated_env = copy.deepcopy(self.launch_context.env) + + calculated_env = self._skip_environment_variables( + calculated_env, matching_profile) + + calculated_env = self._modify_environment_variables( + calculated_env, matching_profile) + + self.launch_context.env = calculated_env + + def _modify_environment_variables(self, calculated_env, matching_profile): + """Modify environment variable values.""" + for env_item in matching_profile["replace_in_environment"]: + value = calculated_env.get(env_item["environment_key"]) + if not value: + continue + + value = re.sub(value, env_item["pattern"], env_item["replacement"]) + calculated_env[env_item["environment_key"]] = value + + return calculated_env + + def _skip_environment_variables(self, calculated_env, matching_profile): + """Skips list of environment variable names""" + for skip_env in matching_profile["skip_environment"]: + self.log.info(f"Skipping {skip_env}") + calculated_env.pop(skip_env) + + return calculated_env From 3f4a491e8f9ce83458fc5ae76305b495690f0c57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:39:28 +0200 Subject: [PATCH 057/144] Update variable name for skipped env vars Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 1329f465e0..717897a70b 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -199,7 +199,7 @@ class FilterFarmEnvironmentModel(BaseSettingsModel): title="Folders" ) - skip_environment: list[str] = SettingsField( + skip_env_keys: list[str] = SettingsField( default_factory=list, title="Skip environment variables" ) From 291e3eaa4c217cdacde15456080ad6c963263194 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:46:41 +0200 Subject: [PATCH 058/144] Update names of profile fields to be more descriptive --- client/ayon_core/hooks/pre_filter_farm_environments.py | 6 +++--- server/settings/main.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 9a52f53950..837116d5eb 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -34,10 +34,10 @@ class FilterFarmEnvironments(PreLaunchHook): task_entity = data["task_entity"] filter_data = { - "hosts": self.host_name, + "host_names": self.host_name, "task_types": task_entity["taskType"], - "tasks": task_entity["name"], - "folders": data["folder_path"] + "task_names": task_entity["name"], + "folder_paths": data["folder_path"] } matching_profile = filter_profiles( filter_env_profiles, filter_data, logger=self.log diff --git a/server/settings/main.py b/server/settings/main.py index 1329f465e0..b6cfbe36ae 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -178,7 +178,7 @@ class EnvironmentReplacementModel(BaseSettingsModel): class FilterFarmEnvironmentModel(BaseSettingsModel): _layout = "expanded" - hosts: list[str] = SettingsField( + host_names: list[str] = SettingsField( default_factory=list, title="Host names" ) @@ -194,9 +194,9 @@ class FilterFarmEnvironmentModel(BaseSettingsModel): title="Task names" ) - folders: list[str] = SettingsField( + folder_paths: list[str] = SettingsField( default_factory=list, - title="Folders" + title="Folder paths" ) skip_environment: list[str] = SettingsField( From 3dc12c7954fae1dacba9e90db6505292a4583ce6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:49:33 +0200 Subject: [PATCH 059/144] Simplified methods for manipulating environments --- .../hooks/pre_filter_farm_environments.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 837116d5eb..95ddec990c 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -1,4 +1,3 @@ -import copy import re from ayon_applications import PreLaunchHook, LaunchTypes @@ -47,15 +46,11 @@ class FilterFarmEnvironments(PreLaunchHook): f"for {filter_data}") return - calculated_env = copy.deepcopy(self.launch_context.env) + self._skip_environment_variables( + self.launch_context.env, matching_profile) - calculated_env = self._skip_environment_variables( - calculated_env, matching_profile) - - calculated_env = self._modify_environment_variables( - calculated_env, matching_profile) - - self.launch_context.env = calculated_env + self._modify_environment_variables( + self.launch_context.env, matching_profile) def _modify_environment_variables(self, calculated_env, matching_profile): """Modify environment variable values.""" @@ -67,12 +62,8 @@ class FilterFarmEnvironments(PreLaunchHook): value = re.sub(value, env_item["pattern"], env_item["replacement"]) calculated_env[env_item["environment_key"]] = value - return calculated_env - def _skip_environment_variables(self, calculated_env, matching_profile): """Skips list of environment variable names""" for skip_env in matching_profile["skip_environment"]: self.log.info(f"Skipping {skip_env}") calculated_env.pop(skip_env) - - return calculated_env From 1acbd5129e02712550110b20e3c8005de412a3c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:31:54 +0200 Subject: [PATCH 060/144] simplified webserver logic --- client/ayon_core/tools/tray/addons_manager.py | 77 +++++++- client/ayon_core/tools/tray/ui/tray.py | 10 +- .../tools/tray/webserver/__init__.py | 7 +- .../tray/webserver/host_console_listener.py | 2 +- .../ayon_core/tools/tray/webserver/server.py | 78 +++++++- .../tools/tray/webserver/webserver.py | 177 ------------------ 6 files changed, 156 insertions(+), 195 deletions(-) delete mode 100644 client/ayon_core/tools/tray/webserver/webserver.py diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index 5acf89c06d..3e46775fc3 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -1,10 +1,19 @@ +import os import time +from typing import Callable +from ayon_core.resources import RESOURCES_DIR from ayon_core.addon import AddonsManager, ITrayAddon, ITrayService -from ayon_core.tools.tray.webserver import TrayWebserver +from ayon_core.tools.tray.webserver import ( + HostListener, + find_free_port, + WebServerManager, +) class TrayAddonsManager(AddonsManager): + # TODO do not use env variable + webserver_url_env = "AYON_WEBSERVER_URL" # Define order of addons in menu # TODO find better way how to define order addons_menu_order = ( @@ -18,11 +27,17 @@ class TrayAddonsManager(AddonsManager): super().__init__(initialize=False) self._tray_manager = tray_manager - self._tray_webserver = None + + self._host_listener = None + self._server_manager = WebServerManager(find_free_port(), None) self.doubleclick_callbacks = {} self.doubleclick_callback = None + @property + def webserver_url(self): + return self._server_manager.url + def get_doubleclick_callback(self): callback_name = self.doubleclick_callback return self.doubleclick_callbacks.get(callback_name) @@ -57,6 +72,35 @@ class TrayAddonsManager(AddonsManager): self.connect_addons() self.tray_menu(tray_menu) + def add_route(self, request_method: str, path: str, handler: Callable): + self._server_manager.add_route(request_method, path, handler) + + def add_static(self, prefix: str, path: str): + self._server_manager.add_static(prefix, path) + + def add_addon_route( + self, + addon_name: str, + path: str, + request_method: str, + handler: Callable + ) -> str: + return self._server_manager.add_addon_route( + addon_name, + path, + request_method, + handler + ) + + def add_addon_static( + self, addon_name: str, prefix: str, path: str + ) -> str: + return self._server_manager.add_addon_static( + addon_name, + prefix, + path + ) + def get_enabled_tray_addons(self): """Enabled tray addons. @@ -75,7 +119,7 @@ class TrayAddonsManager(AddonsManager): self._tray_manager.restart() def tray_init(self): - self._tray_webserver = TrayWebserver(self._tray_manager) + self._init_tray_webserver() report = {} time_start = time.time() prev_start_time = time_start @@ -101,8 +145,9 @@ class TrayAddonsManager(AddonsManager): self._report["Tray init"] = report def connect_addons(self): - enabled_addons = self.get_enabled_addons() - self._tray_webserver.connect_with_addons(enabled_addons) + self._server_manager.connect_with_addons( + self.get_enabled_addons() + ) super().connect_addons() def tray_menu(self, tray_menu): @@ -145,7 +190,7 @@ class TrayAddonsManager(AddonsManager): self._report["Tray menu"] = report def start_addons(self): - self._tray_webserver.start() + self._server_manager.start_server() report = {} time_start = time.time() @@ -174,7 +219,7 @@ class TrayAddonsManager(AddonsManager): self._report["Addons start"] = report def on_exit(self): - self._tray_webserver.stop() + self._server_manager.stop_server() for addon in self.get_enabled_tray_addons(): if addon.tray_initialized: try: @@ -188,4 +233,20 @@ class TrayAddonsManager(AddonsManager): ) def get_tray_webserver(self): - return self._tray_webserver + # TODO rename/remove method + return self._server_manager + + def _init_tray_webserver(self): + self._host_listener = HostListener(self._server_manager, self) + + webserver_url = self.webserver_url + statisc_url = f"{webserver_url}/res" + + # TODO stop using these env variables + # - function 'get_tray_server_url' should be used instead + os.environ[self.webserver_url_env] = webserver_url + os.environ["AYON_STATICS_SERVER"] = statisc_url + + # Deprecated + os.environ["OPENPYPE_WEBSERVER_URL"] = webserver_url + os.environ["OPENPYPE_STATICS_SERVER"] = statisc_url diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 6900e80ed5..eaf1245dd6 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -113,15 +113,17 @@ class TrayManager: tray_menu = self.tray_widget.menu self._addons_manager.initialize(tray_menu) - webserver = self._addons_manager.get_tray_webserver() + webserver_url = self._addons_manager.webserver_url try: - set_tray_server_url(webserver.webserver_url, False) + set_tray_server_url(webserver_url, False) except TrayIsRunningError: self.log.error("Tray is already running.") self.exit() return - webserver.add_route("GET", "/tray", self._get_web_tray_info) + self._addons_manager.add_route( + "GET", "/tray", self._get_web_tray_info + ) admin_submenu = ITrayAction.admin_submenu(tray_menu) tray_menu.addMenu(admin_submenu) @@ -172,7 +174,7 @@ class TrayManager: self.execute_in_main_thread(self._startup_validations) - set_tray_server_url(webserver.webserver_url, True) + set_tray_server_url(webserver_url, True) def get_services_submenu(self): return self._services_submenu diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index 938e7205b4..a81348365f 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,10 +1,13 @@ from .structures import HostMsgAction from .base_routes import RestApiEndpoint -from .webserver import TrayWebserver +from .server import find_free_port, WebServerManager +from .host_console_listener import HostListener __all__ = ( "HostMsgAction", "RestApiEndpoint", - "TrayWebserver", + "find_free_port", + "WebServerManager", + "HostListener", ) diff --git a/client/ayon_core/tools/tray/webserver/host_console_listener.py b/client/ayon_core/tools/tray/webserver/host_console_listener.py index 2c1a7ae9b5..200dde465c 100644 --- a/client/ayon_core/tools/tray/webserver/host_console_listener.py +++ b/client/ayon_core/tools/tray/webserver/host_console_listener.py @@ -27,7 +27,7 @@ class HostListener: 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) + webserver.add_route("*", "/ws/host_listener", self.websocket_handler) def _host_is_connecting(self, host_name, label): """ Initialize dialog, adds to submenu.""" diff --git a/client/ayon_core/tools/tray/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py index 5b6e7e52d4..d2a9b0fc6b 100644 --- a/client/ayon_core/tools/tray/webserver/server.py +++ b/client/ayon_core/tools/tray/webserver/server.py @@ -1,14 +1,74 @@ import re import threading import asyncio +import socket +import random from typing import Callable, Optional from aiohttp import web from ayon_core.lib import Logger +from ayon_core.resources import RESOURCES_DIR + from .cors_middleware import cors_middleware +def find_free_port( + port_from=None, port_to=None, exclude_ports=None, host=None +): + """Find available socket port from entered range. + + It is also possible to only check if entered port is available. + + Args: + port_from (int): Port number which is checked as first. + port_to (int): Last port that is checked in sequence from entered + `port_from`. Only `port_from` is checked if is not entered. + Nothing is processed if is equeal to `port_from`! + exclude_ports (list, tuple, set): List of ports that won't be + checked form entered range. + host (str): Host where will check for free ports. Set to + "localhost" by default. + """ + if port_from is None: + port_from = 8079 + + if port_to is None: + port_to = 65535 + + # Excluded ports (e.g. reserved for other servers/clients) + if exclude_ports is None: + exclude_ports = [] + + # Default host is localhost but it is possible to look for other hosts + if host is None: + host = "localhost" + + found_port = None + while True: + port = random.randint(port_from, port_to) + if port in exclude_ports: + continue + + sock = None + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((host, port)) + found_port = port + + except socket.error: + continue + + finally: + if sock: + sock.close() + + if found_port is not None: + break + + return found_port + + class WebServerManager: """Manger that care about web server thread.""" @@ -20,8 +80,6 @@ class WebServerManager: self.port = port or 8079 self.host = host or "localhost" - self.client = None - self.handlers = {} self.on_stop_callbacks = [] self.app = web.Application( @@ -33,9 +91,10 @@ class WebServerManager: ) # add route with multiple methods for single "external app" - self.webserver_thread = WebServerThread(self) + self.add_static("/res", RESOURCES_DIR) + @property def log(self): if self._log is None: @@ -71,6 +130,19 @@ class WebServerManager: self.app.router.add_static(full_path, path) return full_path + def connect_with_addons(self, addons): + for addon in addons: + if not hasattr(addon, "webserver_initialization"): + continue + + try: + addon.webserver_initialization(self) + except Exception: + self.log.warning( + f"Failed to connect addon \"{addon.name}\" to webserver.", + exc_info=True + ) + def start_server(self): if self.webserver_thread and not self.webserver_thread.is_alive(): self.webserver_thread.start() diff --git a/client/ayon_core/tools/tray/webserver/webserver.py b/client/ayon_core/tools/tray/webserver/webserver.py deleted file mode 100644 index 0a532305e3..0000000000 --- a/client/ayon_core/tools/tray/webserver/webserver.py +++ /dev/null @@ -1,177 +0,0 @@ -"""TrayWebserver spawns aiohttp server in asyncio loop. - -Usage is to add ability to register routes from addons, or for inner calls -of tray. Addon which would want use that option must have implemented method -webserver_initialization` which must expect `WebServerManager` object where -is possible to add routes or paths with handlers. - -WebServerManager is by default created only in tray. - -Running multiple servers in one process is not recommended and probably won't -work as expected. It is because of few limitations connected to asyncio module. -""" - -import os -import socket -from typing import Callable - -from ayon_core import resources -from ayon_core.lib import Logger - -from .server import WebServerManager -from .host_console_listener import HostListener - - -class TrayWebserver: - webserver_url_env = "AYON_WEBSERVER_URL" - - def __init__(self, tray_manager): - self._log = None - self._port = self.find_free_port() - - self._server_manager = WebServerManager(self._port, None) - - webserver_url = self._server_manager.url - self._webserver_url = webserver_url - - self._host_listener = HostListener(self, tray_manager) - - static_prefix = "/res" - self._server_manager.add_static(static_prefix, resources.RESOURCES_DIR) - statisc_url = "{}{}".format( - webserver_url, static_prefix - ) - - os.environ[self.webserver_url_env] = str(webserver_url) - os.environ["AYON_STATICS_SERVER"] = statisc_url - - # Deprecated - os.environ["OPENPYPE_WEBSERVER_URL"] = str(webserver_url) - os.environ["OPENPYPE_STATICS_SERVER"] = statisc_url - - @property - def log(self): - if self._log is None: - self._log = Logger.get_logger("TrayWebserver") - return self._log - - def add_route(self, request_method: str, path: str, handler: Callable): - self._server_manager.add_route(request_method, path, handler) - - def add_static(self, prefix: str, path: str): - self._server_manager.add_static(prefix, path) - - @property - def server_manager(self): - """ - - Returns: - Union[WebServerManager, None]: Server manager instance. - - """ - return self._server_manager - - @property - def port(self): - """ - - Returns: - int: Port on which is webserver running. - - """ - return self._port - - @property - def webserver_url(self): - """ - - Returns: - str: URL to webserver. - - """ - return self._webserver_url - - def connect_with_addons(self, enabled_addons): - if not self._server_manager: - return - - for addon in enabled_addons: - if not hasattr(addon, "webserver_initialization"): - continue - - try: - addon.webserver_initialization(self._server_manager) - except Exception: - self.log.warning( - f"Failed to connect addon \"{addon.name}\" to webserver.", - exc_info=True - ) - - def start(self): - self._start_server() - - def stop(self): - self._stop_server() - - def _start_server(self): - if self._server_manager is not None: - self._server_manager.start_server() - - def _stop_server(self): - if self._server_manager is not None: - self._server_manager.stop_server() - - @staticmethod - def find_free_port( - port_from=None, port_to=None, exclude_ports=None, host=None - ): - """Find available socket port from entered range. - - It is also possible to only check if entered port is available. - - Args: - port_from (int): Port number which is checked as first. - port_to (int): Last port that is checked in sequence from entered - `port_from`. Only `port_from` is checked if is not entered. - Nothing is processed if is equeal to `port_from`! - exclude_ports (list, tuple, set): List of ports that won't be - checked form entered range. - host (str): Host where will check for free ports. Set to - "localhost" by default. - """ - if port_from is None: - port_from = 8079 - - if port_to is None: - port_to = 65535 - - # Excluded ports (e.g. reserved for other servers/clients) - if exclude_ports is None: - exclude_ports = [] - - # Default host is localhost but it is possible to look for other hosts - if host is None: - host = "localhost" - - found_port = None - for port in range(port_from, port_to + 1): - if port in exclude_ports: - continue - - sock = None - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind((host, port)) - found_port = port - - except socket.error: - continue - - finally: - if sock: - sock.close() - - if found_port is not None: - break - - return found_port From 5a7a54f138b3bd053aa8762e31f4b71dc746dffb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:44:27 +0200 Subject: [PATCH 061/144] move host listener to UI --- client/ayon_core/tools/tray/addons_manager.py | 25 ++++++++----------- .../host_console_listener.py | 6 +++-- client/ayon_core/tools/tray/ui/tray.py | 3 +++ .../tools/tray/webserver/__init__.py | 2 -- 4 files changed, 17 insertions(+), 19 deletions(-) rename client/ayon_core/tools/tray/{webserver => ui}/host_console_listener.py (97%) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index 3e46775fc3..166b8ab5c6 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -2,10 +2,8 @@ import os import time from typing import Callable -from ayon_core.resources import RESOURCES_DIR from ayon_core.addon import AddonsManager, ITrayAddon, ITrayService from ayon_core.tools.tray.webserver import ( - HostListener, find_free_port, WebServerManager, ) @@ -28,15 +26,14 @@ class TrayAddonsManager(AddonsManager): self._tray_manager = tray_manager - self._host_listener = None - self._server_manager = WebServerManager(find_free_port(), None) + self._webserver_manager = WebServerManager(find_free_port(), None) self.doubleclick_callbacks = {} self.doubleclick_callback = None @property def webserver_url(self): - return self._server_manager.url + return self._webserver_manager.url def get_doubleclick_callback(self): callback_name = self.doubleclick_callback @@ -73,10 +70,10 @@ class TrayAddonsManager(AddonsManager): self.tray_menu(tray_menu) def add_route(self, request_method: str, path: str, handler: Callable): - self._server_manager.add_route(request_method, path, handler) + self._webserver_manager.add_route(request_method, path, handler) def add_static(self, prefix: str, path: str): - self._server_manager.add_static(prefix, path) + self._webserver_manager.add_static(prefix, path) def add_addon_route( self, @@ -85,7 +82,7 @@ class TrayAddonsManager(AddonsManager): request_method: str, handler: Callable ) -> str: - return self._server_manager.add_addon_route( + return self._webserver_manager.add_addon_route( addon_name, path, request_method, @@ -95,7 +92,7 @@ class TrayAddonsManager(AddonsManager): def add_addon_static( self, addon_name: str, prefix: str, path: str ) -> str: - return self._server_manager.add_addon_static( + return self._webserver_manager.add_addon_static( addon_name, prefix, path @@ -145,7 +142,7 @@ class TrayAddonsManager(AddonsManager): self._report["Tray init"] = report def connect_addons(self): - self._server_manager.connect_with_addons( + self._webserver_manager.connect_with_addons( self.get_enabled_addons() ) super().connect_addons() @@ -190,7 +187,7 @@ class TrayAddonsManager(AddonsManager): self._report["Tray menu"] = report def start_addons(self): - self._server_manager.start_server() + self._webserver_manager.start_server() report = {} time_start = time.time() @@ -219,7 +216,7 @@ class TrayAddonsManager(AddonsManager): self._report["Addons start"] = report def on_exit(self): - self._server_manager.stop_server() + self._webserver_manager.stop_server() for addon in self.get_enabled_tray_addons(): if addon.tray_initialized: try: @@ -234,11 +231,9 @@ class TrayAddonsManager(AddonsManager): def get_tray_webserver(self): # TODO rename/remove method - return self._server_manager + return self._webserver_manager def _init_tray_webserver(self): - self._host_listener = HostListener(self._server_manager, self) - webserver_url = self.webserver_url statisc_url = f"{webserver_url}/res" diff --git a/client/ayon_core/tools/tray/webserver/host_console_listener.py b/client/ayon_core/tools/tray/ui/host_console_listener.py similarity index 97% rename from client/ayon_core/tools/tray/webserver/host_console_listener.py rename to client/ayon_core/tools/tray/ui/host_console_listener.py index 200dde465c..ed3b3767fe 100644 --- a/client/ayon_core/tools/tray/webserver/host_console_listener.py +++ b/client/ayon_core/tools/tray/ui/host_console_listener.py @@ -22,12 +22,14 @@ class IconType: class HostListener: - def __init__(self, webserver, tray_manager): + def __init__(self, addons_manager, tray_manager): self._tray_manager = tray_manager 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) + addons_manager.add_route( + "*", "/ws/host_listener", self.websocket_handler + ) def _host_is_connecting(self, host_name, label): """ Initialize dialog, adds to submenu.""" diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index eaf1245dd6..b46821c7df 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -35,6 +35,7 @@ from ayon_core.tools.tray.lib import ( TrayIsRunningError, ) +from .host_console_listener import HostListener from .info_widget import InfoWidget from .dialogs import ( UpdateDialog, @@ -65,6 +66,8 @@ class TrayManager: self._addons_manager = TrayAddonsManager(self) + self._host_listener = HostListener(self._addons_manager, self) + self.errors = [] self._update_check_timer = None diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index a81348365f..93bfbd6aee 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,7 +1,6 @@ from .structures import HostMsgAction from .base_routes import RestApiEndpoint from .server import find_free_port, WebServerManager -from .host_console_listener import HostListener __all__ = ( @@ -9,5 +8,4 @@ __all__ = ( "RestApiEndpoint", "find_free_port", "WebServerManager", - "HostListener", ) From 3eefe4d7d090fe22628d550a9263927b66d0dd28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:00:07 +0200 Subject: [PATCH 062/144] faster existing tray validation --- client/ayon_core/tools/tray/__init__.py | 2 ++ .../tools/tray/ui/host_console_listener.py | 2 +- client/ayon_core/tools/tray/ui/tray.py | 18 ++++++++++-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index 001b37e129..d646880e15 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,3 +1,4 @@ +from .webserver import HostMsgAction from .addons_manager import TrayAddonsManager from .lib import ( is_tray_running, @@ -6,6 +7,7 @@ from .lib import ( __all__ = ( + "HostMsgAction", "TrayAddonsManager", "main", ) diff --git a/client/ayon_core/tools/tray/ui/host_console_listener.py b/client/ayon_core/tools/tray/ui/host_console_listener.py index ed3b3767fe..62bca2f51b 100644 --- a/client/ayon_core/tools/tray/ui/host_console_listener.py +++ b/client/ayon_core/tools/tray/ui/host_console_listener.py @@ -9,7 +9,7 @@ from qtpy import QtWidgets from ayon_core.addon import ITrayService from ayon_core.tools.stdout_broker.window import ConsoleDialog -from .structures import HostMsgAction +from ayon_core.tools.tray import HostMsgAction log = logging.getLogger(__name__) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index b46821c7df..0ae0e04260 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -80,6 +80,15 @@ class TrayManager: self._services_submenu = None self._start_time = time.time() + try: + set_tray_server_url( + self._addons_manager.webserver_url, False + ) + except TrayIsRunningError: + self.log.error("Tray is already running.") + self.exit() + return + @property def doubleclick_callback(self): """Double-click callback for Tray icon.""" @@ -116,13 +125,6 @@ class TrayManager: tray_menu = self.tray_widget.menu self._addons_manager.initialize(tray_menu) - webserver_url = self._addons_manager.webserver_url - try: - set_tray_server_url(webserver_url, False) - except TrayIsRunningError: - self.log.error("Tray is already running.") - self.exit() - return self._addons_manager.add_route( "GET", "/tray", self._get_web_tray_info @@ -177,7 +179,7 @@ class TrayManager: self.execute_in_main_thread(self._startup_validations) - set_tray_server_url(webserver_url, True) + set_tray_server_url(self._addons_manager.webserver_url, True) def get_services_submenu(self): return self._services_submenu From d060244d49f6185195fcca04737419b2dbb06639 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:44:42 +0200 Subject: [PATCH 063/144] remove unecessary comment --- client/ayon_core/plugins/publish/integrate_review.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 1c62f25d94..a5979e3b1c 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -52,7 +52,6 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): if repre.get("thumbnail") or "thumbnail" in repre_tags: continue - # include only thumbnail representations repre_path = get_publish_repre_path( instance, repre, False ) From 722b3a5d63a623ed46700528692c1dad020edfd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:53:21 +0200 Subject: [PATCH 064/144] fix label calculation --- .../plugins/publish/integrate_review.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index a5979e3b1c..6a79e72c1e 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -64,8 +64,7 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): self.log.warning("Could not determine Content-Type") continue - # Use output name as label if available - label = repre.get("outputName") + label = self._get_review_label(repre, uploaded_labels) query = "" if label: query = f"?label={label}" @@ -74,14 +73,6 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): f"/projects/{project_name}" f"/versions/{version_id}/reviewables{query}" ) - # Make sure label is unique - orig_label = label - idx = 0 - while label in uploaded_labels: - idx += 1 - label = f"{orig_label}_{idx}" - - uploaded_labels.add(label) # Upload the reviewable self.log.info(f"Uploading reviewable '{label}' ...") @@ -94,4 +85,16 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): repre_path, headers=headers, request_type=RequestTypes.post, - ) \ No newline at end of file + ) + + def _get_review_label(self, repre, uploaded_labels): + # Use output name as label if available + label = repre.get("outputName") + if not label: + return None + orig_label = label + idx = 0 + while label in uploaded_labels: + idx += 1 + label = f"{orig_label}_{idx}" + return label From 754f2d0e5f010cc4bcc95a6506db87b1f7bd4587 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:53:30 +0200 Subject: [PATCH 065/144] log representation path --- client/ayon_core/plugins/publish/integrate_review.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 6a79e72c1e..1adac420a6 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -61,7 +61,9 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): content_type = get_media_mime_type(repre_path) if not content_type: - self.log.warning("Could not determine Content-Type") + self.log.warning( + f"Could not determine Content-Type for {repre_path}" + ) continue label = self._get_review_label(repre, uploaded_labels) From d027b546d858cdebe299d755bb7071b3d5a8a17b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:18:31 +0200 Subject: [PATCH 066/144] added option to validate running tray --- client/ayon_core/tools/tray/lib.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index ba16e5cbc5..556d1435f0 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -99,13 +99,23 @@ def get_tray_file_info( def get_tray_server_url( + validate: Optional[bool] = False, server_url: Optional[str] = None, - variant: Optional[str] = None + variant: Optional[str] = None, ) -> Optional[str]: data = get_tray_file_info(server_url, variant) if data is None: return None - return data.get("url") + url = data.get("url") + if not url: + return None + + if not validate: + return url + + if _get_tray_information(url): + return url + return None def set_tray_server_url(tray_url: str, started: bool): From 89ad9afdd38a499a296f7e552f2d7d1699a93a65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:18:40 +0200 Subject: [PATCH 067/144] added docstrings --- client/ayon_core/tools/tray/lib.py | 92 +++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 556d1435f0..b393ad0564 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -25,10 +25,12 @@ class TrayIsRunningError(Exception): def _get_default_server_url() -> str: + """Get default AYON server url.""" return os.getenv("AYON_SERVER_URL") def _get_default_variant() -> str: + """Get default settings variant.""" return ayon_api.get_default_settings_variant() @@ -55,16 +57,31 @@ def _windows_pid_is_running(pid: int) -> bool: def _create_tray_hash(server_url: str, variant: str) -> str: + """Create tray hash for metadata filename. + + Args: + server_url (str): AYON server url. + variant (str): Settings variant. + + Returns: + str: Hash for metadata filename. + + """ data = f"{server_url}|{variant}" return hashlib.sha256(data.encode()).hexdigest() def get_tray_storage_dir() -> str: + """Get tray storage directory. + + Returns: + str: Tray storage directory where metadata files are stored. + + """ return get_ayon_appdirs("tray") def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]: - # TODO implement server side information response = requests.get(f"{tray_url}/tray") try: response.raise_for_status() @@ -87,6 +104,19 @@ def get_tray_file_info( server_url: Optional[str] = None, variant: Optional[str] = None ) -> Optional[Dict[str, Any]]: + """Get tray information from file. + + Metadata information about running tray that should contain tray + server url. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + + Returns: + Optional[Dict[str, Any]]: Tray information. + + """ filepath = _get_tray_info_filepath(server_url, variant) if not os.path.exists(filepath): return None @@ -103,6 +133,20 @@ def get_tray_server_url( server_url: Optional[str] = None, variant: Optional[str] = None, ) -> Optional[str]: + """Get tray server url. + + Does not validate if tray is running. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + validate (Optional[bool]): Validate if tray is running. + By default, does not validate. + + Returns: + Optional[str]: Tray server url. + + """ data = get_tray_file_info(server_url, variant) if data is None: return None @@ -119,6 +163,16 @@ def get_tray_server_url( def set_tray_server_url(tray_url: str, started: bool): + """Add tray server information file. + + Called from tray logic, do not use on your own. + + Args: + tray_url (str): Webserver url with port. + started (bool): If tray is started. When set to 'False' it means + that tray is starting up. + + """ filepath = _get_tray_info_filepath() if os.path.exists(filepath): info = get_tray_file_info() @@ -135,6 +189,10 @@ def set_tray_server_url(tray_url: str, started: bool): def remove_tray_server_url(): + """Remove tray information file. + + Called from tray logic, do not use on your own. + """ filepath = _get_tray_info_filepath() if not os.path.exists(filepath): return @@ -149,6 +207,16 @@ def get_tray_information( server_url: Optional[str] = None, variant: Optional[str] = None ) -> Optional[Dict[str, Any]]: + """Get information about tray. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + + Returns: + Optional[Dict[str, Any]]: Tray information. + + """ tray_url = get_tray_server_url(server_url, variant) return _get_tray_information(tray_url) @@ -156,7 +224,17 @@ def get_tray_information( def get_tray_state( server_url: Optional[str] = None, variant: Optional[str] = None -): +) -> int: + """Get tray state for AYON server and variant. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + + Returns: + int: Tray state. + + """ file_info = get_tray_file_info(server_url, variant) if file_info is None: return TrayState.NOT_RUNNING @@ -177,6 +255,16 @@ def is_tray_running( server_url: Optional[str] = None, variant: Optional[str] = None ) -> bool: + """Check if tray is running. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + + Returns: + bool: True if tray is running + + """ state = get_tray_state(server_url, variant) return state != TrayState.NOT_RUNNING From 3416c60a65c1bde2e162ba4e2d6816e0fbaac473 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:20:12 +0200 Subject: [PATCH 068/144] added some functions to init for api --- client/ayon_core/tools/tray/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index d646880e15..9dbacc54c2 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,7 +1,10 @@ from .webserver import HostMsgAction from .addons_manager import TrayAddonsManager from .lib import ( + TrayState, + get_tray_state, is_tray_running, + get_tray_server_url, main, ) @@ -9,5 +12,10 @@ from .lib import ( __all__ = ( "HostMsgAction", "TrayAddonsManager", + + "TrayState", + "get_tray_state", + "is_tray_running", + "get_tray_server_url", "main", ) From d482cf7e8afae376dba6e1bd14a61ce8c7cb64b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:40:14 +0200 Subject: [PATCH 069/144] removed unused imports --- client/ayon_core/addon/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index cd952edffc..0ffad2045e 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -22,8 +22,6 @@ from ayon_core.settings import get_studio_settings from .interfaces import ( IPluginPaths, IHostAddon, - ITrayAddon, - ITrayService ) # Files that will be always ignored on addons import From e7f816795314a931e80f28a26c6a5eb4efa70d31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:55:18 +0200 Subject: [PATCH 070/144] store tracebacks in case stdout is not available --- client/ayon_core/addon/utils.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 3ad112b562..66c2d81a79 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -93,35 +93,50 @@ def ensure_addons_are_process_ready( message = None failed = False use_detail = False + # Wrap the output in StringIO to capture it for details on fail + # - but in case stdout was invalid on start of process also store + # the tracebacks + tracebacks = [] output = StringIO() with contextlib.redirect_stdout(output): with contextlib.redirect_stderr(output): for addon in addons_manager.get_enabled_addons(): - failed = True + addon_failed = True try: addon.ensure_is_process_ready(process_context) - failed = False + addon_failed = False except ProcessPreparationError as exc: message = str(exc) print(f"Addon preparation failed: '{addon.name}'") print(message) except BaseException as exc: + use_detail = True message = "An unexpected error occurred." + formatted_traceback = "".join(traceback.format_exception( + *sys.exc_info() + )) + tracebacks.append(formatted_traceback) print(f"Addon preparation failed: '{addon.name}'") print(message) - # Print the traceback so it is in the output - traceback.print_exception(*sys.exc_info()) - use_detail = True + # Print the traceback so it is in the stdout + print(formatted_traceback) - if failed: + if addon_failed: + failed = True break output_str = output.getvalue() # Print stdout/stderr to console as it was redirected print(output_str) if failed: - detail = output_str if use_detail else None + detail = None + if use_detail: + # In case stdout was not captured, use the tracebacks + if not output_str: + output_str = "\n".join(tracebacks) + detail = output_str + _handle_error(process_context, message, detail) if not exit_on_failure: return exc From e15a2b275f4373025a18c6d9b7a9fe72d2a81edd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:00:30 +0200 Subject: [PATCH 071/144] fix ruff linting --- client/ayon_core/addon/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 66c2d81a79..8b7740c89c 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -89,7 +89,7 @@ def ensure_addons_are_process_ready( if addons_manager is None: addons_manager = AddonsManager() - exc = None + exception = None message = None failed = False use_detail = False @@ -106,11 +106,13 @@ def ensure_addons_are_process_ready( addon.ensure_is_process_ready(process_context) addon_failed = False except ProcessPreparationError as exc: + exception = exc message = str(exc) print(f"Addon preparation failed: '{addon.name}'") print(message) except BaseException as exc: + exception = exc use_detail = True message = "An unexpected error occurred." formatted_traceback = "".join(traceback.format_exception( @@ -139,5 +141,5 @@ def ensure_addons_are_process_ready( _handle_error(process_context, message, detail) if not exit_on_failure: - return exc + return exception sys.exit(1) From 05a13d63cb56a64680564af5ae9db55875ea975d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:21:18 +0200 Subject: [PATCH 072/144] fix multiple bugs --- client/ayon_core/tools/tray/lib.py | 13 +-- client/ayon_core/tools/tray/ui/tray.py | 112 +++++++++++++++---------- 2 files changed, 74 insertions(+), 51 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index b393ad0564..7b057eeb49 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -82,12 +82,12 @@ def get_tray_storage_dir() -> str: def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]: - response = requests.get(f"{tray_url}/tray") try: + response = requests.get(f"{tray_url}/tray") response.raise_for_status() - except requests.HTTPError: + return response.json() + except (requests.HTTPError, requests.ConnectionError): return None - return response.json() def _get_tray_info_filepath( @@ -196,11 +196,12 @@ def remove_tray_server_url(): filepath = _get_tray_info_filepath() if not os.path.exists(filepath): return + with open(filepath, "r") as stream: data = json.load(stream) - if data.get("pid") != os.getpid(): - return - os.remove(filepath) + + if data.get("pid") == os.getpid(): + os.remove(filepath) def get_tray_information( diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 0ae0e04260..51fde675ad 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -62,32 +62,46 @@ class TrayManager: ) if update_check_interval is None: update_check_interval = 5 - self._update_check_interval = update_check_interval * 60 * 1000 + + update_check_interval = update_check_interval * 60 * 1000 + + # create timer loop to check callback functions + main_thread_timer = QtCore.QTimer() + main_thread_timer.setInterval(300) + + update_check_timer = QtCore.QTimer() + if update_check_interval > 0: + update_check_timer.setInterval(update_check_interval) + + main_thread_timer.timeout.connect(self._main_thread_execution) + update_check_timer.timeout.connect(self._on_update_check_timer) self._addons_manager = TrayAddonsManager(self) - self._host_listener = HostListener(self._addons_manager, self) self.errors = [] - self._update_check_timer = None self._outdated_dialog = None - self._main_thread_timer = None + self._update_check_timer = update_check_timer + self._update_check_interval = update_check_interval + self._main_thread_timer = main_thread_timer self._main_thread_callbacks = collections.deque() self._execution_in_progress = None - self._closing = False self._services_submenu = None self._start_time = time.time() + self._closing = False try: set_tray_server_url( self._addons_manager.webserver_url, False ) except TrayIsRunningError: self.log.error("Tray is already running.") - self.exit() - return + self._closing = True + + def is_closing(self): + return self._closing @property def doubleclick_callback(self): @@ -122,6 +136,8 @@ class TrayManager: def initialize_addons(self): """Add addons to tray.""" + if self._closing: + return tray_menu = self.tray_widget.menu self._addons_manager.initialize(tray_menu) @@ -162,24 +178,15 @@ class TrayManager: # Print time report self._addons_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.start() - self._main_thread_timer = main_thread_timer - - update_check_timer = QtCore.QTimer() if self._update_check_interval > 0: - update_check_timer.timeout.connect(self._on_update_check_timer) - update_check_timer.setInterval(self._update_check_interval) - update_check_timer.start() - self._update_check_timer = update_check_timer + self._update_check_timer.start() self.execute_in_main_thread(self._startup_validations) - - set_tray_server_url(self._addons_manager.webserver_url, True) + set_tray_server_url( + self._addons_manager.webserver_url, True + ) def get_services_submenu(self): return self._services_submenu @@ -244,7 +251,10 @@ class TrayManager: def exit(self): self._closing = True - self.tray_widget.exit() + if self._main_thread_timer.isActive(): + self.execute_in_main_thread(self.tray_widget.exit) + else: + self.tray_widget.exit() def on_exit(self): remove_tray_server_url() @@ -349,20 +359,24 @@ class TrayManager: ) def _main_thread_execution(self): - if self._execution_in_progress: - return - self._execution_in_progress = True - for _ in range(len(self._main_thread_callbacks)): - if self._main_thread_callbacks: - item = self._main_thread_callbacks.popleft() - try: - item.execute() - except BaseException: - self.log.erorr( - "Main thread execution failed", exc_info=True - ) + try: + if self._execution_in_progress: + return + self._execution_in_progress = True + for _ in range(len(self._main_thread_callbacks)): + if self._main_thread_callbacks: + item = self._main_thread_callbacks.popleft() + try: + item.execute() + except BaseException: + self.log.erorr( + "Main thread execution failed", exc_info=True + ) - self._execution_in_progress = False + self._execution_in_progress = False + + except KeyboardInterrupt: + self.execute_in_main_thread(self.exit) def _startup_validations(self): """Run possible startup validations.""" @@ -476,19 +490,23 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def __init__(self, parent): icon = QtGui.QIcon(resources.get_ayon_icon_filepath()) - super(SystemTrayIcon, self).__init__(icon, parent) + super().__init__(icon, parent) self._exited = False + self._doubleclick = False + self._click_pos = None + self._initializing_addons = False + # Store parent - QtWidgets.QMainWindow() - self.parent = parent + self._parent = parent # Setup menu in Tray self.menu = QtWidgets.QMenu() self.menu.setStyleSheet(style.load_stylesheet()) # Set addons - self.tray_man = TrayManager(self, self.parent) + self._tray_manager = TrayManager(self, parent) # Add menu to Context of SystemTrayIcon self.setContextMenu(self.menu) @@ -508,10 +526,9 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): click_timer.timeout.connect(self._click_timer_timeout) self._click_timer = click_timer - self._doubleclick = False - self._click_pos = None - self._initializing_addons = False + def is_closing(self) -> bool: + return self._tray_manager.is_closing() @property def initializing_addons(self): @@ -520,7 +537,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def initialize_addons(self): self._initializing_addons = True try: - self.tray_man.initialize_addons() + self._tray_manager.initialize_addons() finally: self._initializing_addons = False @@ -530,7 +547,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): # Reset bool value self._doubleclick = False if doubleclick: - self.tray_man.execute_doubleclick() + self._tray_manager.execute_doubleclick() else: self._show_context_menu() @@ -544,7 +561,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def on_systray_activated(self, reason): # show contextMenu if left click if reason == QtWidgets.QSystemTrayIcon.Trigger: - if self.tray_man.doubleclick_callback: + if self._tray_manager.doubleclick_callback: self._click_pos = QtGui.QCursor().pos() self._click_timer.start() else: @@ -563,7 +580,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self._exited = True self.hide() - self.tray_man.on_exit() + self._tray_manager.on_exit() QtCore.QCoreApplication.exit() @@ -588,6 +605,11 @@ class TrayStarter(QtCore.QObject): self._start_timer = start_timer def _on_start_timer(self): + if self._tray_widget.is_closing(): + self._start_timer.stop() + self._tray_widget.exit() + return + if self._timer_counter == 0: self._timer_counter += 1 splash = self._get_splash() From b5f7162918eb528b4159e07c2445371f7095de1f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:28:50 +0200 Subject: [PATCH 073/144] fix 'set_tray_server_url' --- client/ayon_core/tools/tray/lib.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 7b057eeb49..198382b44c 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -82,6 +82,8 @@ def get_tray_storage_dir() -> str: def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]: + if not tray_url: + return None try: response = requests.get(f"{tray_url}/tray") response.raise_for_status() @@ -173,11 +175,13 @@ def set_tray_server_url(tray_url: str, started: bool): that tray is starting up. """ - filepath = _get_tray_info_filepath() - if os.path.exists(filepath): - info = get_tray_file_info() - if info.get("pid") != os.getpid(): + file_info = get_tray_file_info() + if file_info and file_info.get("pid") != os.getpid(): + tray_url = file_info.get("url") + if _get_tray_information(tray_url): raise TrayIsRunningError("Tray is already running.") + + filepath = _get_tray_info_filepath() os.makedirs(os.path.dirname(filepath), exist_ok=True) data = { "url": tray_url, From ebc1c62f29dcfaed423cf04c3a69e053361b9b4d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:39:48 +0200 Subject: [PATCH 074/144] small enhancements --- client/ayon_core/tools/tray/lib.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 198382b44c..66a494b727 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -176,9 +176,9 @@ def set_tray_server_url(tray_url: str, started: bool): """ file_info = get_tray_file_info() - if file_info and file_info.get("pid") != os.getpid(): - tray_url = file_info.get("url") - if _get_tray_information(tray_url): + if file_info and file_info["pid"] != os.getpid(): + tray_url = file_info["url"] + if not file_info["started"] or _get_tray_information(tray_url): raise TrayIsRunningError("Tray is already running.") filepath = _get_tray_info_filepath() @@ -281,14 +281,13 @@ def main(): state = get_tray_state() if state == TrayState.RUNNING: - # TODO send some information to tray? print("Tray is already running.") return if state == TrayState.STARTING: + # TODO try to handle stuck tray? print("Tray is starting.") return - # TODO try to handle stuck tray? time.sleep(5) state = get_tray_state() if state == TrayState.RUNNING: From 3f311759710495b0bfb546ce8a8f716ce8b83554 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:58:30 +0200 Subject: [PATCH 075/144] create addons manager only once for cli main --- client/ayon_core/cli.py | 31 ++++++++++++++++++++++++------- client/ayon_core/cli_commands.py | 21 --------------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 60cf5624b0..5046c1bc86 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -12,7 +12,11 @@ import acre from ayon_core import AYON_CORE_ROOT from ayon_core.addon import AddonsManager from ayon_core.settings import get_general_environments -from ayon_core.lib import initialize_ayon_connection, is_running_from_build +from ayon_core.lib import ( + initialize_ayon_connection, + is_running_from_build, + Logger, +) from .cli_commands import Commands @@ -64,7 +68,6 @@ def tray(): Commands.launch_tray() -@Commands.add_addons @main_cli.group(help="Run command line arguments of AYON addons") @click.pass_context def addon(ctx): @@ -245,11 +248,9 @@ def _set_global_environments() -> None: os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" -def _set_addons_environments(): +def _set_addons_environments(addons_manager): """Set global environments for AYON addons.""" - addons_manager = AddonsManager() - # Merge environments with current environments and update values if module_envs := addons_manager.collect_global_environments(): parsed_envs = acre.parse(module_envs) @@ -258,6 +259,21 @@ def _set_addons_environments(): os.environ.update(env) +def _add_addons(addons_manager): + """Modules/Addons can add their cli commands dynamically.""" + log = Logger.get_logger("CLI-AddModules") + for addon_obj in addons_manager.addons: + try: + addon_obj.cli(addon) + + except Exception: + log.warning( + "Failed to add cli command for module \"{}\"".format( + addon_obj.name + ), exc_info=True + ) + + def main(*args, **kwargs): initialize_ayon_connection() python_path = os.getenv("PYTHONPATH", "") @@ -281,8 +297,9 @@ def main(*args, **kwargs): print(" - global AYON ...") _set_global_environments() print(" - for addons ...") - _set_addons_environments() - + addons_manager = AddonsManager() + _set_addons_environments(addons_manager) + _add_addons(addons_manager) try: main_cli(obj={}, prog_name="ayon") except Exception: # noqa diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 35b7e294de..3feb3e2f36 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -19,27 +19,6 @@ class Commands: tray.main() - @staticmethod - def add_addons(click_func): - """Modules/Addons can add their cli commands dynamically.""" - - from ayon_core.lib import Logger - from ayon_core.addon import AddonsManager - - manager = AddonsManager() - log = Logger.get_logger("CLI-AddModules") - for addon in manager.addons: - try: - addon.cli(click_func) - - except Exception: - log.warning( - "Failed to add cli command for module \"{}\"".format( - addon.name - ), exc_info=True - ) - return click_func - @staticmethod def publish(path: str, targets: list=None, gui:bool=False) -> None: """Start headless publishing. From 3333f03a1ecced7b4599a34122242868adc001ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:14:59 +0200 Subject: [PATCH 076/144] remove invalid docstring --- client/ayon_core/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 5046c1bc86..eab21d32ad 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -120,7 +120,7 @@ def publish(path, targets, gui): """Start CLI publishing. Publish collects json from path provided as an argument. -S + """ Commands.publish(path, targets, gui) From 065195929d6e5aa03fc7c384e119380f27722039 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:21:15 +0200 Subject: [PATCH 077/144] pass addons manager to callbacks --- client/ayon_core/cli.py | 18 ++++++++++++++---- client/ayon_core/cli_commands.py | 20 +++++++++++--------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index eab21d32ad..fad0482559 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -106,23 +106,30 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup): 'addon applications extractenvironments ...' instead. """ Commands.extractenvironments( - output_json_path, project, asset, task, app, envgroup + output_json_path, + project, + asset, + task, + app, + envgroup, + ctx.obj["addons_manager"] ) @main_cli.command() +@click.pass_context @click.argument("path", required=True) @click.option("-t", "--targets", help="Targets", default=None, multiple=True) @click.option("-g", "--gui", is_flag=True, help="Show Publish UI", default=False) -def publish(path, targets, gui): +def publish(ctx, path, targets, gui): """Start CLI publishing. Publish collects json from path provided as an argument. """ - Commands.publish(path, targets, gui) + Commands.publish(path, targets, gui, ctx.obj["addons_manager"]) @main_cli.command(context_settings={"ignore_unknown_options": True}) @@ -301,7 +308,10 @@ def main(*args, **kwargs): _set_addons_environments(addons_manager) _add_addons(addons_manager) try: - main_cli(obj={}, prog_name="ayon") + main_cli( + prog_name="ayon", + obj={"addons_manager": addons_manager}, + ) except Exception: # noqa exc_info = sys.exc_info() print("!!! AYON crashed:") diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 3feb3e2f36..874062cd46 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -20,7 +20,12 @@ class Commands: tray.main() @staticmethod - def publish(path: str, targets: list=None, gui:bool=False) -> None: + def publish( + path: str, + targets: list = None, + gui: bool = False, + addons_manager=None, + ) -> None: """Start headless publishing. Publish use json from passed path argument. @@ -81,14 +86,15 @@ class Commands: install_ayon_plugins() - manager = AddonsManager() + if addons_manager is None: + addons_manager = AddonsManager() - publish_paths = manager.collect_plugin_paths()["publish"] + publish_paths = addons_manager.collect_plugin_paths()["publish"] for plugin_path in publish_paths: pyblish.api.register_plugin_path(plugin_path) - applications_addon = manager.get_enabled_addon("applications") + applications_addon = addons_manager.get_enabled_addon("applications") if applications_addon is not None: context = get_global_context() env = applications_addon.get_farm_publish_environment_variables( @@ -137,15 +143,12 @@ class Commands: @staticmethod def extractenvironments( - output_json_path, project, asset, task, app, env_group + output_json_path, project, asset, task, app, env_group, addons_manager ): """Produces json file with environment based on project and app. Called by Deadline plugin to propagate environment into render jobs. """ - - from ayon_core.addon import AddonsManager - warnings.warn( ( "Command 'extractenvironments' is deprecated and will be" @@ -155,7 +158,6 @@ class Commands: DeprecationWarning ) - addons_manager = AddonsManager() applications_addon = addons_manager.get_enabled_addon("applications") if applications_addon is None: raise RuntimeError( From d6c9b33b91ef5fd84a4a8ac573e6fff97cded74d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:29:33 +0200 Subject: [PATCH 078/144] fix 'extractenvironments' --- client/ayon_core/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index fad0482559..6c3006b78a 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -83,6 +83,7 @@ main_cli.set_alias("addon", "module") @main_cli.command() +@click.pass_context @click.argument("output_json_path") @click.option("--project", help="Project name", default=None) @click.option("--asset", help="Folder path", default=None) @@ -91,7 +92,9 @@ main_cli.set_alias("addon", "module") @click.option( "--envgroup", help="Environment group (e.g. \"farm\")", default=None ) -def extractenvironments(output_json_path, project, asset, task, app, envgroup): +def extractenvironments( + ctx, output_json_path, project, asset, task, app, envgroup +): """Extract environment variables for entered context to a json file. Entered output filepath will be created if does not exists. From bb4ae624fb2299d3095f819b13793fedfb681641 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:13:17 +0200 Subject: [PATCH 079/144] Use addons over modules --- client/ayon_core/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 6c3006b78a..e97b8f1c5a 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -271,7 +271,7 @@ def _set_addons_environments(addons_manager): def _add_addons(addons_manager): """Modules/Addons can add their cli commands dynamically.""" - log = Logger.get_logger("CLI-AddModules") + log = Logger.get_logger("CLI-AddAddons") for addon_obj in addons_manager.addons: try: addon_obj.cli(addon) From 9201a6c354f436f0dee071988be7602f2a8cfd26 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:31:40 +0200 Subject: [PATCH 080/144] added typehings --- client/ayon_core/cli_commands.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 874062cd46..900cc237d1 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -2,7 +2,9 @@ """Implementation of AYON commands.""" import os import sys -import warnings +from typing import Optional, List + +from ayon_core.addon import AddonsManager class Commands: @@ -22,9 +24,9 @@ class Commands: @staticmethod def publish( path: str, - targets: list = None, - gui: bool = False, - addons_manager=None, + targets: Optional[List[str]] = None, + gui: Optional[bool] = False, + addons_manager: Optional[AddonsManager] = None, ) -> None: """Start headless publishing. @@ -32,8 +34,9 @@ class Commands: Args: path (str): Path to JSON. - targets (list of str): List of pyblish targets. - gui (bool): Show publish UI. + targets (Optional[List[str]]): List of pyblish targets. + gui (Optional[bool]): Show publish UI. + addons_manager (Optional[AddonsManager]): Addons manager instance. Raises: RuntimeError: When there is no path to process. From 5f7dd573197e8acc9048f87c9f57586851e732cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:10:33 +0200 Subject: [PATCH 081/144] use filename if label is empty --- client/ayon_core/plugins/publish/integrate_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 1adac420a6..790c7da9c6 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -75,12 +75,12 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): f"/projects/{project_name}" f"/versions/{version_id}/reviewables{query}" ) - + filename = os.path.basename(repre_path) # Upload the reviewable - self.log.info(f"Uploading reviewable '{label}' ...") + self.log.info(f"Uploading reviewable '{label or filename}' ...") headers = ayon_con.get_headers(content_type) - headers["x-file-name"] = os.path.basename(repre_path) + headers["x-file-name"] = filename self.log.info(f"Uploading reviewable {repre_path}") ayon_con.upload_file( endpoint, From e4b6c0c7770acec99e6b217150d0d2bf40d2c50f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:12:25 +0200 Subject: [PATCH 082/144] fix warnings import --- client/ayon_core/cli_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 900cc237d1..ebc559ec4e 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -2,6 +2,7 @@ """Implementation of AYON commands.""" import os import sys +import warnings from typing import Optional, List from ayon_core.addon import AddonsManager From a2850087553b080a7c3036ca9a3b5783500d1e20 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:28:14 +0200 Subject: [PATCH 083/144] fix typo Co-authored-by: Roy Nieterau --- client/ayon_core/tools/tray/addons_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/addons_manager.py index 166b8ab5c6..3fe4bb8dd8 100644 --- a/client/ayon_core/tools/tray/addons_manager.py +++ b/client/ayon_core/tools/tray/addons_manager.py @@ -235,13 +235,13 @@ class TrayAddonsManager(AddonsManager): def _init_tray_webserver(self): webserver_url = self.webserver_url - statisc_url = f"{webserver_url}/res" + statics_url = f"{webserver_url}/res" # TODO stop using these env variables # - function 'get_tray_server_url' should be used instead os.environ[self.webserver_url_env] = webserver_url - os.environ["AYON_STATICS_SERVER"] = statisc_url + os.environ["AYON_STATICS_SERVER"] = statics_url # Deprecated os.environ["OPENPYPE_WEBSERVER_URL"] = webserver_url - os.environ["OPENPYPE_STATICS_SERVER"] = statisc_url + os.environ["OPENPYPE_STATICS_SERVER"] = statics_url From f5abf6e981821819752e94c193154609781ce95e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:47:50 +0200 Subject: [PATCH 084/144] Enhance comments and logs Co-authored-by: Roy Nieterau --- client/ayon_core/plugins/publish/integrate_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 790c7da9c6..0a6b24adb4 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import get_publish_repre_path class IntegrateAYONReview(pyblish.api.InstancePlugin): label = "Integrate AYON Review" - # Must happen after IntegrateNew + # Must happen after IntegrateAsset order = pyblish.api.IntegratorOrder + 0.15 def process(self, instance): @@ -33,7 +33,7 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): if (major, minor) < (1, 3): self.log.info( "Skipping reviewable upload, supported from server 1.3.x." - f" User server version {ayon_con.get_server_version()}" + f" Current server version {ayon_con.get_server_version()}" ) return From 136da2b4709c87bc4c7d076f2a662de07a44adfa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:57:30 +0200 Subject: [PATCH 085/144] exit if another tray is discovered --- client/ayon_core/tools/tray/ui/tray.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 51fde675ad..660c61ac94 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -184,9 +184,13 @@ class TrayManager: self._update_check_timer.start() self.execute_in_main_thread(self._startup_validations) - set_tray_server_url( - self._addons_manager.webserver_url, True - ) + try: + set_tray_server_url( + self._addons_manager.webserver_url, True + ) + except TrayIsRunningError: + self.log.warning("Other tray started meanwhile. Exiting.") + self.exit() def get_services_submenu(self): return self._services_submenu From 131afb6684541dbaebde68a540feecf00ab3a99a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:57:59 +0200 Subject: [PATCH 086/144] call 'set_tray_server_url' as soon as possible --- client/ayon_core/tools/tray/lib.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 66a494b727..a3c69480b4 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -164,13 +164,13 @@ def get_tray_server_url( return None -def set_tray_server_url(tray_url: str, started: bool): +def set_tray_server_url(tray_url: Optional[str], started: bool): """Add tray server information file. Called from tray logic, do not use on your own. Args: - tray_url (str): Webserver url with port. + tray_url (Optional[str]): Webserver url with port. started (bool): If tray is started. When set to 'False' it means that tray is starting up. @@ -299,5 +299,12 @@ def main(): os.kill(pid, signal.SIGTERM) remove_tray_server_url() + # Prepare the file with 'pid' information as soon as possible + try: + set_tray_server_url(None, False) + except TrayIsRunningError: + print("Tray is running") + sys.exit(1) + main() From aee9bd3f81547c52481f659b1ed03e36d1635ea2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:47:21 +0200 Subject: [PATCH 087/144] don't override tray_url --- client/ayon_core/tools/tray/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index a3c69480b4..7462c5d7c6 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -177,8 +177,7 @@ def set_tray_server_url(tray_url: Optional[str], started: bool): """ file_info = get_tray_file_info() if file_info and file_info["pid"] != os.getpid(): - tray_url = file_info["url"] - if not file_info["started"] or _get_tray_information(tray_url): + if not file_info["started"] or _get_tray_information(file_info["url"]): raise TrayIsRunningError("Tray is already running.") filepath = _get_tray_info_filepath() From 43f9f5114573ce03da449a05aa960b3af73daf62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:48:16 +0200 Subject: [PATCH 088/144] 'remove_tray_server_url' has force option --- client/ayon_core/tools/tray/lib.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 7462c5d7c6..555937579f 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -191,19 +191,26 @@ def set_tray_server_url(tray_url: Optional[str], started: bool): json.dump(data, stream) -def remove_tray_server_url(): +def remove_tray_server_url(force: Optional[bool] = False): """Remove tray information file. Called from tray logic, do not use on your own. + + Args: + force (Optional[bool]): Force remove tray information file. + """ filepath = _get_tray_info_filepath() if not os.path.exists(filepath): return - with open(filepath, "r") as stream: - data = json.load(stream) + try: + with open(filepath, "r") as stream: + data = json.load(stream) + except BaseException: + data = {} - if data.get("pid") == os.getpid(): + if force or not data or data.get("pid") == os.getpid(): os.remove(filepath) @@ -250,7 +257,7 @@ def get_tray_state( info = _get_tray_information(tray_url) if not info: # Remove the information as the tray is not running - remove_tray_server_url() + remove_tray_server_url(force=True) return TrayState.NOT_RUNNING return TrayState.RUNNING @@ -296,7 +303,7 @@ def main(): pid = file_info.get("pid") if pid is not None: os.kill(pid, signal.SIGTERM) - remove_tray_server_url() + remove_tray_server_url(force=True) # Prepare the file with 'pid' information as soon as possible try: From 03c93a345d190a30edd9a453d83675fd883810fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:51:17 +0200 Subject: [PATCH 089/144] impemented waiting for starting tray --- client/ayon_core/tools/tray/lib.py | 78 ++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 555937579f..e13c682ab0 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -1,6 +1,8 @@ import os +import sys import json import hashlib +import platform import subprocess import csv import time @@ -56,6 +58,28 @@ def _windows_pid_is_running(pid: int) -> bool: return False +def _is_process_running(pid: int) -> bool: + """Check whether process with pid is running.""" + if platform.system().lower() == "windows": + return _windows_pid_is_running(pid) + + if pid == 0: + return True + + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +def _kill_tray_process(pid: int): + if _is_process_running(pid): + os.kill(pid, signal.SIGTERM) + + def _create_tray_hash(server_url: str, variant: str) -> str: """Create tray hash for metadata filename. @@ -71,6 +95,38 @@ def _create_tray_hash(server_url: str, variant: str) -> str: return hashlib.sha256(data.encode()).hexdigest() +def _wait_for_starting_tray( + server_url: Optional[str] = None, + variant: Optional[str] = None, + timeout: Optional[int] = None +) -> Optional[Dict[str, Any]]: + """Wait for tray to start. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + timeout (Optional[int]): Timeout for tray validation. + + Returns: + Optional[Dict[str, Any]]: Tray file information. + + """ + if timeout is None: + timeout = 10 + started_at = time.time() + while True: + data = get_tray_file_info(server_url, variant) + if data is None: + return None + + if data.get("started") is True: + return data + + if time.time() - started_at > timeout: + return None + time.sleep(0.1) + + def get_tray_storage_dir() -> str: """Get tray storage directory. @@ -134,6 +190,7 @@ def get_tray_server_url( validate: Optional[bool] = False, server_url: Optional[str] = None, variant: Optional[str] = None, + timeout: Optional[int] = None ) -> Optional[str]: """Get tray server url. @@ -144,6 +201,7 @@ def get_tray_server_url( variant (Optional[str]): Settings variant. validate (Optional[bool]): Validate if tray is running. By default, does not validate. + timeout (Optional[int]): Timeout for tray start-up. Returns: Optional[str]: Tray server url. @@ -152,6 +210,12 @@ def get_tray_server_url( data = get_tray_file_info(server_url, variant) if data is None: return None + + if data.get("started") is False: + data = _wait_for_starting_tray(server_url, variant, timeout) + if data is None: + return None + url = data.get("url") if not url: return None @@ -291,18 +355,22 @@ def main(): return if state == TrayState.STARTING: - # TODO try to handle stuck tray? - print("Tray is starting.") - return - time.sleep(5) + print("Tray is starting. Waiting for it to start.") + _wait_for_starting_tray() state = get_tray_state() if state == TrayState.RUNNING: + print("Tray started. Exiting.") return + if state == TrayState.STARTING: + print( + "Tray did not start in expected time." + " Killing the process and starting new." + ) file_info = get_tray_file_info() or {} pid = file_info.get("pid") if pid is not None: - os.kill(pid, signal.SIGTERM) + _kill_tray_process(pid) remove_tray_server_url(force=True) # Prepare the file with 'pid' information as soon as possible From f473e987dfa44b0985dbef9380b0a5fbe9d45cf0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 Jul 2024 11:27:29 +0200 Subject: [PATCH 090/144] Fix key name from Settings --- client/ayon_core/hooks/pre_filter_farm_environments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 95ddec990c..cabd705d81 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -64,6 +64,6 @@ class FilterFarmEnvironments(PreLaunchHook): def _skip_environment_variables(self, calculated_env, matching_profile): """Skips list of environment variable names""" - for skip_env in matching_profile["skip_environment"]: + for skip_env in matching_profile["skip_env_keys"]: self.log.info(f"Skipping {skip_env}") calculated_env.pop(skip_env) From 07d0bcc7526ee643b6f8c04e00a254493070b88c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 Jul 2024 12:10:11 +0200 Subject: [PATCH 091/144] Remove empty environment variable --- client/ayon_core/hooks/pre_filter_farm_environments.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index cabd705d81..0f83c0d3e0 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -14,7 +14,6 @@ class FilterFarmEnvironments(PreLaunchHook): - skipping (list) of environment variable keys - removing value in environment variable: - supports regular expression in pattern - - doesn't remove env var if value empty! """ order = 1000 @@ -55,12 +54,16 @@ class FilterFarmEnvironments(PreLaunchHook): def _modify_environment_variables(self, calculated_env, matching_profile): """Modify environment variable values.""" for env_item in matching_profile["replace_in_environment"]: - value = calculated_env.get(env_item["environment_key"]) + key = env_item["environment_key"] + value = calculated_env.get(key) if not value: continue value = re.sub(value, env_item["pattern"], env_item["replacement"]) - calculated_env[env_item["environment_key"]] = value + if value: + calculated_env[key] = value + else: + calculated_env.pop(key) def _skip_environment_variables(self, calculated_env, matching_profile): """Skips list of environment variable names""" From b5336f2e483912b05b884375aee0073f76a1b0cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:51:40 +0200 Subject: [PATCH 092/144] removed upload reviewable from integrate plugin --- client/ayon_core/plugins/publish/integrate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 6837145f5d..69c14465eb 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -349,8 +349,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("{}".format(op_session.to_data())) op_session.commit() - self._upload_reviewable(project_name, version_entity["id"], instance) - # Backwards compatibility used in hero integration. # todo: can we avoid the need to store this? instance.data["published_representations"] = { From df5623e9f6f1d003237cce5e781580088ead3a60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:17:03 +0200 Subject: [PATCH 093/144] added option to trigger tray message --- client/ayon_core/tools/tray/ui/tray.py | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 660c61ac94..6077820fab 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -133,6 +133,7 @@ class TrayManager: kwargs["msecs"] = msecs self.tray_widget.showMessage(*args, **kwargs) + # TODO validate 'self.tray_widget.supportsMessages()' def initialize_addons(self): """Add addons to tray.""" @@ -145,6 +146,9 @@ class TrayManager: self._addons_manager.add_route( "GET", "/tray", self._get_web_tray_info ) + self._addons_manager.add_route( + "POST", "/tray/message", self._web_show_tray_message + ) admin_submenu = ITrayAction.admin_submenu(tray_menu) tray_menu.addMenu(admin_submenu) @@ -285,7 +289,37 @@ class TrayManager: }, "installer_version": os.getenv("AYON_VERSION"), "running_time": time.time() - self._start_time, - })) + }) + + async def _web_show_tray_message(self, request: Request) -> Response: + data = await request.json() + try: + title = data["title"] + message = data["message"] + icon = data.get("icon") + msecs = data.get("msecs") + except KeyError as exc: + return json_response( + { + "error": f"Missing required data. {exc}", + "success": False, + }, + status=400, + ) + + if icon == "information": + icon = QtWidgets.QSystemTrayIconInformation + elif icon == "warning": + icon = QtWidgets.QSystemTrayIconWarning + elif icon == "critical": + icon = QtWidgets.QSystemTrayIcon.Critical + else: + icon = None + + self.execute_in_main_thread( + self.show_tray_message, title, message, icon, msecs + ) + return json_response({"success": True}) def _on_update_check_timer(self): try: From 3156e91e978f4cf6d686a6c12581a95712fc5c16 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:46:44 +0200 Subject: [PATCH 094/144] added helper function to send message to tray --- client/ayon_core/tools/tray/lib.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e13c682ab0..377a844321 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -344,6 +344,38 @@ def is_tray_running( return state != TrayState.NOT_RUNNING +def show_message_in_tray( + title, message, icon=None, msecs=None, tray_url=None +): + """Show message in tray. + + Args: + title (str): Message title. + message (str): Message content. + icon (Optional[str]): Icon for the message. + msecs (Optional[int]): Duration of the message. + tray_url (Optional[str]): Tray server url. + + """ + if not tray_url: + tray_url = get_tray_server_url() + + # TODO handle this case, e.g. raise an error? + if not tray_url: + return + + # TODO handle response, can fail whole request or can fail on status + requests.post( + f"{tray_url}/tray/message", + json={ + "title": title, + "message": message, + "icon": icon, + "msecs": msecs + } + ) + + def main(): from ayon_core.tools.tray.ui import main From dce3a4b6a25205a138d9cf26c7cf78eab98e7383 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:48:33 +0200 Subject: [PATCH 095/144] trigger show message if tray is already running --- client/ayon_core/tools/tray/lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 377a844321..926a0c03cd 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -383,6 +383,10 @@ def main(): state = get_tray_state() if state == TrayState.RUNNING: + show_message_in_tray( + "Tray is already running", + "Your AYON tray application is already running." + ) print("Tray is already running.") return From 947ecfd9182b405a618af1a89e476676cb927e4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:16:39 +0200 Subject: [PATCH 096/144] add username to tray information --- client/ayon_core/tools/tray/ui/tray.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 660c61ac94..aed1fe2139 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -3,12 +3,11 @@ import sys import time import collections import atexit -import json import platform -from aiohttp.web_response import Response import ayon_api from qtpy import QtCore, QtGui, QtWidgets +from aiohttp.web import Response, json_response, Request from ayon_core import resources, style from ayon_core.lib import ( @@ -91,6 +90,10 @@ class TrayManager: self._services_submenu = None self._start_time = time.time() + # Cache AYON username used in process + # - it can change only by changing ayon_api global connection + # should be safe for tray application to cache the value only once + self._cached_username = None self._closing = False try: set_tray_server_url( @@ -143,7 +146,7 @@ class TrayManager: self._addons_manager.initialize(tray_menu) self._addons_manager.add_route( - "GET", "/tray", self._get_web_tray_info + "GET", "/tray", self._web_get_tray_info ) admin_submenu = ITrayAction.admin_submenu(tray_menu) @@ -274,8 +277,12 @@ class TrayManager: return item - async def _get_web_tray_info(self, request): - return Response(text=json.dumps({ + async def _web_get_tray_info(self, _request: Request) -> Response: + if self._cached_username is None: + self._cached_username = ayon_api.get_user()["name"] + + return json_response({ + "username": self._cached_username, "bundle": os.getenv("AYON_BUNDLE_NAME"), "dev_mode": is_dev_mode_enabled(), "staging_mode": is_staging_enabled(), From 90bb6a841be6fbf7a9095f1977f9dfddceee4014 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:49:15 +0200 Subject: [PATCH 097/144] added force option to tray # Conflicts: # client/ayon_core/tools/tray/lib.py --- client/ayon_core/cli.py | 11 +++++++++-- client/ayon_core/cli_commands.py | 6 ------ client/ayon_core/tools/tray/lib.py | 10 +++++++++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index e97b8f1c5a..ee993ecd82 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -59,13 +59,20 @@ def main_cli(ctx): @main_cli.command() -def tray(): +@click.option( + "--force", + is_flag=True, + help="Force to start tray and close any existing one.") +def tray(force): """Launch AYON tray. Default action of AYON command is to launch tray widget to control basic aspects of AYON. See documentation for more information. """ - Commands.launch_tray() + + from ayon_core.tools.tray import main + + main(force) @main_cli.group(help="Run command line arguments of AYON addons") diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 9d871c54b1..8ae1ebb3ba 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -13,12 +13,6 @@ class Commands: Most of its methods are called by :mod:`cli` module. """ - @staticmethod - def launch_tray(): - from ayon_core.tools.tray import main - - main() - @staticmethod def publish( path: str, diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e13c682ab0..752c1ee842 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -344,12 +344,20 @@ def is_tray_running( return state != TrayState.NOT_RUNNING -def main(): +def main(force=False): from ayon_core.tools.tray.ui import main Logger.set_process_name("Tray") state = get_tray_state() + if force and state in (TrayState.RUNNING, TrayState.STARTING): + file_info = get_tray_file_info() or {} + pid = file_info.get("pid") + if pid is not None: + _kill_tray_process(pid) + remove_tray_server_url(force=True) + state = TrayState.NOT_RUNNING + if state == TrayState.RUNNING: print("Tray is already running.") return From d1c85ea2af856063d789e28719641aaa78fd50b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:09:50 +0200 Subject: [PATCH 098/144] added hidden force to main cli --- client/ayon_core/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index ee993ecd82..5936316e2c 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -43,6 +43,7 @@ class AliasedGroup(click.Group): help="Enable debug") @click.option("--verbose", expose_value=False, help=("Change AYON log level (debug - critical or 0-50)")) +@click.option("--force", is_flag=True, expose_value=False, hidden=True) def main_cli(ctx): """AYON is main command serving as entry point to pipeline system. From 9c01ddaf638f19988fefcef76cd6b516d4cfc57c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:47:08 +0200 Subject: [PATCH 099/144] make starting tray check faster --- client/ayon_core/tools/tray/lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e13c682ab0..16a6770d82 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -122,6 +122,10 @@ def _wait_for_starting_tray( if data.get("started") is True: return data + pid = data.get("pid") + if pid and not _is_process_running(pid): + return None + if time.time() - started_at > timeout: return None time.sleep(0.1) From 6bbd48e989cd8251e921fff520a4b513bdb234e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:17:39 +0200 Subject: [PATCH 100/144] fix closing bracket --- client/ayon_core/tools/tray/ui/tray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index aed1fe2139..16e8434302 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -292,7 +292,7 @@ class TrayManager: }, "installer_version": os.getenv("AYON_VERSION"), "running_time": time.time() - self._start_time, - })) + }) def _on_update_check_timer(self): try: From a4de305fde378698ae59cfe4d66472213a7024c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:31:10 +0200 Subject: [PATCH 101/144] remove tray filepath if pid is not running --- client/ayon_core/tools/tray/lib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 16a6770d82..abe8a7a11d 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -278,7 +278,12 @@ def remove_tray_server_url(force: Optional[bool] = False): except BaseException: data = {} - if force or not data or data.get("pid") == os.getpid(): + if ( + force + or not data + or data.get("pid") == os.getpid() + or not _is_process_running(data.get("pid")) + ): os.remove(filepath) From 17e04cd8849216b85557e32293936abce85f7344 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:31:23 +0200 Subject: [PATCH 102/144] call 'remove_tray_server_url' in wait for tray to start --- client/ayon_core/tools/tray/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index abe8a7a11d..2c3a577641 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -124,6 +124,7 @@ def _wait_for_starting_tray( pid = data.get("pid") if pid and not _is_process_running(pid): + remove_tray_server_url() return None if time.time() - started_at > timeout: From 715f547adf1e4539b6841d70774754bb143b28fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:11:00 +0200 Subject: [PATCH 103/144] fix possible encoding issues --- client/ayon_core/tools/tray/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 752c1ee842..20770d5136 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -7,6 +7,7 @@ import subprocess import csv import time import signal +import locale from typing import Optional, Dict, Tuple, Any import ayon_api @@ -50,7 +51,8 @@ def _get_server_and_variant( def _windows_pid_is_running(pid: int) -> bool: args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"] output = subprocess.check_output(args) - csv_content = csv.DictReader(output.decode("utf-8").splitlines()) + encoding = locale.getpreferredencoding() + csv_content = csv.DictReader(output.decode(encoding).splitlines()) # if "PID" not in csv_content.fieldnames: # return False for _ in csv_content: From 5d18e69c7a98d417acfa62ff061905ff46812c39 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:25:20 +0200 Subject: [PATCH 104/144] forward force to tray --- client/ayon_core/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 5936316e2c..0a9bb2aa9c 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -43,8 +43,8 @@ class AliasedGroup(click.Group): help="Enable debug") @click.option("--verbose", expose_value=False, help=("Change AYON log level (debug - critical or 0-50)")) -@click.option("--force", is_flag=True, expose_value=False, hidden=True) -def main_cli(ctx): +@click.option("--force", is_flag=True, hidden=True) +def main_cli(ctx, force): """AYON is main command serving as entry point to pipeline system. It wraps different commands together. @@ -56,7 +56,7 @@ def main_cli(ctx): print(ctx.get_help()) sys.exit(0) else: - ctx.invoke(tray) + ctx.forward(tray) @main_cli.command() From 4511f8db5bb3b44fcac30ba8231e00ebd1c02f1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:32:16 +0200 Subject: [PATCH 105/144] move addons manager to ui --- client/ayon_core/tools/tray/__init__.py | 2 -- client/ayon_core/tools/tray/{ => ui}/addons_manager.py | 0 client/ayon_core/tools/tray/ui/tray.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) rename client/ayon_core/tools/tray/{ => ui}/addons_manager.py (100%) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index 9dbacc54c2..c8fcd7841e 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,5 +1,4 @@ from .webserver import HostMsgAction -from .addons_manager import TrayAddonsManager from .lib import ( TrayState, get_tray_state, @@ -11,7 +10,6 @@ from .lib import ( __all__ = ( "HostMsgAction", - "TrayAddonsManager", "TrayState", "get_tray_state", diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/ui/addons_manager.py similarity index 100% rename from client/ayon_core/tools/tray/addons_manager.py rename to client/ayon_core/tools/tray/ui/addons_manager.py diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 660c61ac94..2a2c79129b 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -28,13 +28,13 @@ from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, ) -from ayon_core.tools.tray import TrayAddonsManager from ayon_core.tools.tray.lib import ( set_tray_server_url, remove_tray_server_url, TrayIsRunningError, ) +from .addons_manager import TrayAddonsManager from .host_console_listener import HostListener from .info_widget import InfoWidget from .dialogs import ( From a4bb042337daf099c1a4adb5a9414da892b603b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:32:45 +0200 Subject: [PATCH 106/144] move structures out of webserver --- client/ayon_core/tools/tray/__init__.py | 2 +- client/ayon_core/tools/tray/{webserver => }/structures.py | 0 client/ayon_core/tools/tray/webserver/__init__.py | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) rename client/ayon_core/tools/tray/{webserver => }/structures.py (100%) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index c8fcd7841e..2490122358 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,4 +1,4 @@ -from .webserver import HostMsgAction +from .structures import HostMsgAction from .lib import ( TrayState, get_tray_state, diff --git a/client/ayon_core/tools/tray/webserver/structures.py b/client/ayon_core/tools/tray/structures.py similarity index 100% rename from client/ayon_core/tools/tray/webserver/structures.py rename to client/ayon_core/tools/tray/structures.py diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index 93bfbd6aee..c40b5b85c3 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,10 +1,8 @@ -from .structures import HostMsgAction from .base_routes import RestApiEndpoint from .server import find_free_port, WebServerManager __all__ = ( - "HostMsgAction", "RestApiEndpoint", "find_free_port", "WebServerManager", From 5bf69857378cb1fe653be7fbb774f543ca8d78a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:33:04 +0200 Subject: [PATCH 107/144] implemented helper function 'make_sure_tray_is_running' to run tray --- client/ayon_core/tools/tray/__init__.py | 2 ++ client/ayon_core/tools/tray/lib.py | 40 ++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index 2490122358..2e179f0620 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -4,6 +4,7 @@ from .lib import ( get_tray_state, is_tray_running, get_tray_server_url, + make_sure_tray_is_running, main, ) @@ -15,5 +16,6 @@ __all__ = ( "get_tray_state", "is_tray_running", "get_tray_server_url", + "make_sure_tray_is_running", "main", ) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 76cf20d3b4..5018dc6620 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -13,7 +13,7 @@ from typing import Optional, Dict, Tuple, Any import ayon_api import requests -from ayon_core.lib import Logger +from ayon_core.lib import Logger, get_ayon_launcher_args, run_detached_process from ayon_core.lib.local_settings import get_ayon_appdirs @@ -356,6 +356,44 @@ def is_tray_running( return state != TrayState.NOT_RUNNING +def make_sure_tray_is_running( + ayon_url: Optional[str] = None, + variant: Optional[str] = None, + env: Optional[Dict[str, str]] = None +): + """Make sure that tray for AYON url and variant is running. + + Args: + ayon_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + env (Optional[Dict[str, str]]): Environment variables for the process. + + """ + state = get_tray_state(ayon_url, variant) + if state == TrayState.RUNNING: + return + + if state == TrayState.STARTING: + _wait_for_starting_tray(ayon_url, variant) + state = get_tray_state(ayon_url, variant) + if state == TrayState.RUNNING: + return + + args = get_ayon_launcher_args("tray", "--force") + if env is None: + env = os.environ.copy() + + if ayon_url: + env["AYON_SERVER_URL"] = ayon_url + + # TODO maybe handle variant in a better way + if variant: + if variant == "staging": + args.append("--use-staging") + + run_detached_process(args, env=env) + + def main(force=False): from ayon_core.tools.tray.ui import main From adc55dee1a844575c3bf8cc46fd4e3ca26174067 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:38:52 +0200 Subject: [PATCH 108/144] unset QT_API --- client/ayon_core/tools/tray/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 5018dc6620..c26f4835b1 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -382,6 +382,9 @@ def make_sure_tray_is_running( args = get_ayon_launcher_args("tray", "--force") if env is None: env = os.environ.copy() + + # Make sure 'QT_API' is not set + env.pop("QT_API", None) if ayon_url: env["AYON_SERVER_URL"] = ayon_url From aa1a3928d3dbfbc0c0f6e8326bd330ea85c53cd2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 24 Jul 2024 22:21:14 +0200 Subject: [PATCH 109/144] Remove newlines, or just write a first chapter book MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > Why empty first line? It is like opening book that starts with 2nd chapter 🙂 Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/ui/product_types_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index ff62ec0bd5..0303f97d09 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -72,7 +72,6 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): self.refreshed.emit() def reset_product_types_filter(self): - project_name = self._controller.get_selected_project_name() product_types_filter = ( self._controller.get_product_types_filter(project_name) @@ -183,7 +182,6 @@ class ProductTypesView(QtWidgets.QListView): super().showEvent(event) def _on_refresh_finished(self): - # Apply product types filter on first show if self._refresh_product_types_filter: self._product_types_model.reset_product_types_filter() From 1d23d076fc49fc4c56bf63a5cb0dc4e4f6b2348a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:40:46 +0200 Subject: [PATCH 110/144] fix import in broker --- client/ayon_core/tools/stdout_broker/broker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/stdout_broker/broker.py b/client/ayon_core/tools/stdout_broker/broker.py index 4f7118e2a8..c449fa7df9 100644 --- a/client/ayon_core/tools/stdout_broker/broker.py +++ b/client/ayon_core/tools/stdout_broker/broker.py @@ -8,7 +8,7 @@ from datetime import datetime import websocket from ayon_core.lib import Logger -from ayon_core.tools.tray.webserver import HostMsgAction +from ayon_core.tools.tray import HostMsgAction log = Logger.get_logger(__name__) From b9067cde3d1f8ca0dab0ec0b07cac6504c9a1ea5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:39:00 +0200 Subject: [PATCH 111/144] remove 'checked' attribute from product type item --- client/ayon_core/tools/loader/abstract.py | 5 +---- client/ayon_core/tools/loader/models/products.py | 4 ++-- client/ayon_core/tools/loader/ui/product_types_widget.py | 5 ----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index dfc83cfc20..c715b9ce99 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -14,19 +14,16 @@ class ProductTypeItem: Args: name (str): Product type name. icon (dict[str, Any]): Product type icon definition. - checked (bool): Is product type checked for filtering. """ - def __init__(self, name, icon, checked): + def __init__(self, name, icon): self.name = name self.icon = icon - self.checked = checked def to_data(self): return { "name": self.name, "icon": self.icon, - "checked": self.checked, } @classmethod diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index c9325c4480..58eab0cabe 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -123,7 +123,7 @@ def product_type_item_from_data(product_type_data): "color": "#0091B2", } # TODO implement checked logic - return ProductTypeItem(product_type_data["name"], icon, True) + return ProductTypeItem(product_type_data["name"], icon) def create_default_product_type_item(product_type): @@ -132,7 +132,7 @@ def create_default_product_type_item(product_type): "name": "fa.folder", "color": "#0091B2", } - return ProductTypeItem(product_type, icon, True) + return ProductTypeItem(product_type, icon) class ProductsModel: diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 0303f97d09..dfccd8f349 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -52,11 +52,6 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): new_items.append(item) self._items_by_name[name] = item - item.setCheckState( - QtCore.Qt.Checked - if product_type_item.checked - else QtCore.Qt.Unchecked - ) icon = get_qt_icon(product_type_item.icon) item.setData(icon, QtCore.Qt.DecorationRole) From c75dbd6c4ed971988a37adaaec314fde0eb19b81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:40:32 +0200 Subject: [PATCH 112/144] receive information only from context data --- client/ayon_core/tools/loader/control.py | 28 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index fa0443d876..b83cb74e76 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -3,9 +3,8 @@ import uuid import ayon_api -from ayon_core.settings import get_current_project_settings +from ayon_core.settings import get_project_settings from ayon_core.pipeline import get_current_host_name -from ayon_core.pipeline.context_tools import get_current_task_entity from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import Anatomy, get_current_context @@ -443,12 +442,10 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): return output context = self.get_current_context() - if ( - not all(context.values()) - or context["project_name"] != project_name - ): + project_name = context.get("project_name") + if not project_name: return output - settings = get_current_project_settings() + settings = get_project_settings(project_name) profiles = ( settings ["core"] @@ -458,13 +455,26 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): ) if not profiles: return output - task_entity = get_current_task_entity(fields={"taskType"}) + + folder_id = context.get("folder_id") + task_name = context.get("task_name") + task_type = None + if folder_id and task_name: + task_entity = ayon_api.get_task_by_name( + project_name, + folder_id, + task_name, + fields={"taskType"} + ) + if task_entity: + task_type = task_entity.get("taskType") + host_name = getattr(self._host, "name", get_current_host_name()) profile = filter_profiles( profiles, { "hosts": host_name, - "task_types": (task_entity or {}).get("taskType") + "task_types": task_type, } ) if profile: From 9af0e6e1cdffae9422eeaed16accb4f6da3505b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:42:37 +0200 Subject: [PATCH 113/144] rename 'is_include' to 'is_allow_list' --- client/ayon_core/tools/loader/abstract.py | 4 ++-- client/ayon_core/tools/loader/control.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index c715b9ce99..14ed831d4b 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -349,9 +349,9 @@ class ProductTypesFilter: Defines the filtering for product types. """ - def __init__(self, product_types: List[str], is_include: bool): + def __init__(self, product_types: List[str], is_allow_list: bool): self.product_types: List[str] = product_types - self.is_include: bool = is_include + self.is_allow_list: bool = is_allow_list class _BaseLoaderController(ABC): diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index b83cb74e76..181e52218f 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -434,7 +434,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_product_types_filter(self, project_name): output = ProductTypesFilter( - is_include=False, + is_allow_list=False, product_types=[] ) # Without host is not determined context @@ -479,7 +479,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): ) if profile: output = ProductTypesFilter( - is_include=profile["is_include"], + is_allow_list=profile["is_include"], product_types=profile["filter_product_types"] ) return output From 1cacc3b723f6862f5dab7ec7a6328f68f8d78a43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:43:12 +0200 Subject: [PATCH 114/144] don't require project name in 'get_product_types_filter' --- client/ayon_core/tools/loader/abstract.py | 5 +---- client/ayon_core/tools/loader/control.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 14ed831d4b..4c8893bf95 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1016,12 +1016,9 @@ class FrontendLoaderController(_BaseLoaderController): pass @abstractmethod - def get_product_types_filter(self, project_name): + def get_product_types_filter(self): """Return product type filter for project name (and current context). - Args: - project_name (str): Project name. - Returns: ProductTypesFilter: Product type filter for current context """ diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 181e52218f..6a809967f7 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -432,7 +432,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def _emit_event(self, topic, data=None): self._event_system.emit(topic, data or {}, "controller") - def get_product_types_filter(self, project_name): + def get_product_types_filter(self): output = ProductTypesFilter( is_allow_list=False, product_types=[] From 4521188ecf6fe3cabf46c01560c113c9dbe35b3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:43:30 +0200 Subject: [PATCH 115/144] modified product types widget to work as expected --- .../tools/loader/ui/product_types_widget.py | 60 ++++++++++++------- client/ayon_core/tools/loader/ui/window.py | 2 + 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index dfccd8f349..9b1bf6326f 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -13,10 +13,17 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): super(ProductTypesQtModel, self).__init__() self._controller = controller + self._reset_filters_on_refresh = True self._refreshing = False self._bulk_change = False + self._last_project = None self._items_by_name = {} + controller.register_event_callback( + "controller.reset.finished", + self._on_controller_reset_finish, + ) + def is_refreshing(self): return self._refreshing @@ -37,14 +44,19 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): self._refreshing = True product_type_items = self._controller.get_product_type_items( project_name) + self._last_project = project_name items_to_remove = set(self._items_by_name.keys()) new_items = [] + items_filter_required = {} for product_type_item in product_type_items: name = product_type_item.name items_to_remove.discard(name) - item = self._items_by_name.get(product_type_item.name) + item = self._items_by_name.get(name) + # Apply filter to new items or if filters reset is requested + filter_required = self._reset_filters_on_refresh if item is None: + filter_required = True item = QtGui.QStandardItem(name) item.setData(name, PRODUCT_TYPE_ROLE) item.setEditable(False) @@ -52,9 +64,26 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): new_items.append(item) self._items_by_name[name] = item + if filter_required: + items_filter_required[name] = item + icon = get_qt_icon(product_type_item.icon) item.setData(icon, QtCore.Qt.DecorationRole) + if items_filter_required: + product_types_filter = self._controller.get_product_types_filter() + for product_type, item in items_filter_required.items(): + matching = ( + int(product_type in product_types_filter.product_types) + + int(product_types_filter.is_allow_list) + ) + state = ( + QtCore.Qt.Checked + if matching % 2 == 0 + else QtCore.Qt.Unchecked + ) + item.setCheckState(state) + root_item = self.invisibleRootItem() if new_items: root_item.appendRows(new_items) @@ -63,22 +92,12 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): item = self._items_by_name.pop(name) root_item.removeRow(item.row()) + self._reset_filters_on_refresh = False self._refreshing = False self.refreshed.emit() - def reset_product_types_filter(self): - project_name = self._controller.get_selected_project_name() - product_types_filter = ( - self._controller.get_product_types_filter(project_name) - ) - if product_types_filter.is_include: - self.change_state_for_all(False) - else: - self.change_state_for_all(True) - self.change_states( - product_types_filter.is_include, - product_types_filter.product_types - ) + def reset_product_types_filter_on_refresh(self): + self._reset_filters_on_refresh = True def setData(self, index, value, role=None): checkstate_changed = False @@ -131,6 +150,9 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): if changed: self.filter_changed.emit() + def _on_controller_reset_finish(self): + self.refresh(self._last_project) + class ProductTypesView(QtWidgets.QListView): filter_changed = QtCore.Signal() @@ -168,19 +190,15 @@ class ProductTypesView(QtWidgets.QListView): def get_filter_info(self): return self._product_types_model.get_filter_info() + def reset_product_types_filter_on_refresh(self): + self._product_types_model.reset_product_types_filter_on_refresh() + def _on_project_change(self, event): project_name = event["project_name"] self._product_types_model.refresh(project_name) - def showEvent(self, event): - self._refresh_product_types_filter = True - super().showEvent(event) - def _on_refresh_finished(self): # Apply product types filter on first show - if self._refresh_product_types_filter: - self._product_types_model.reset_product_types_filter() - self.filter_changed.emit() def _on_filter_change(self): diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 58af6f0b1f..31c9908b23 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -345,6 +345,8 @@ class LoaderWindow(QtWidgets.QWidget): def closeEvent(self, event): super(LoaderWindow, self).closeEvent(event) + self._product_types_widget.reset_product_types_filter_on_refresh() + self._reset_on_show = True def keyPressEvent(self, event): From d812395af99b6b545eef488bb2092fcd661cc6c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:43:44 +0200 Subject: [PATCH 116/144] use full variable names --- client/ayon_core/tools/loader/control.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 6a809967f7..0ea2903544 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -337,11 +337,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name = context.get("project_name") folder_path = context.get("folder_path") if project_name and folder_path: - folder = ayon_api.get_folder_by_path( + folder_entity = ayon_api.get_folder_by_path( project_name, folder_path, fields=["id"] ) - if folder: - folder_id = folder["id"] + if folder_entity: + folder_id = folder_entity["id"] return { "project_name": project_name, "folder_id": folder_id, From 2eecac36da7ac9e7c873bac308d5b7d709e69035 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:53:04 +0200 Subject: [PATCH 117/144] change settings for better readability --- client/ayon_core/tools/loader/control.py | 6 +++++- server/settings/tools.py | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 0ea2903544..2da77337fb 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -478,8 +478,12 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): } ) if profile: + # TODO remove 'is_include' after release '0.4.3' + is_allow_list = profile.get("is_include") + if is_allow_list is None: + is_allow_list = profile["filter_type"] == "is_allow_list" output = ProductTypesFilter( - is_allow_list=profile["is_include"], + is_allow_list=is_allow_list, product_types=profile["filter_product_types"] ) return output diff --git a/server/settings/tools.py b/server/settings/tools.py index 9368e29990..85a66f6a70 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -222,6 +222,13 @@ def _product_types_enum(): ] +def filter_type_enum(): + return [ + {"value": "is_allow_list", "label": "Allow list"}, + {"value": "is_deny_list", "label": "Deny list"}, + ] + + class LoaderProductTypeFilterProfile(BaseSettingsModel): _layout = "expanded" # TODO this should use hosts enum @@ -231,9 +238,15 @@ class LoaderProductTypeFilterProfile(BaseSettingsModel): title="Task types", enum_resolver=task_types_enum ) - is_include: bool = SettingsField(True, title="Exclude / Include") + filter_type: str = SettingsField( + "is_allow_list", + title="Filter type", + section="Product type filter", + enum_resolver=filter_type_enum + ) filter_product_types: list[str] = SettingsField( default_factory=list, + title="Product types", enum_resolver=_product_types_enum ) From ea547ed53974a0c2868c55a20603cfd411d28e9e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 25 Jul 2024 14:14:45 +0200 Subject: [PATCH 118/144] Update client/ayon_core/tools/loader/abstract.py --- client/ayon_core/tools/loader/abstract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 4c8893bf95..0b790dfbbd 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1017,7 +1017,7 @@ class FrontendLoaderController(_BaseLoaderController): @abstractmethod def get_product_types_filter(self): - """Return product type filter for project name (and current context). + """Return product type filter for current context. Returns: ProductTypesFilter: Product type filter for current context From aad7e2902dba233aedb99585c95192882c5b16be Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:33:55 +0200 Subject: [PATCH 119/144] Change name of variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 6596a9ecba..564dd92bd2 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -259,7 +259,7 @@ class CoreSettings(BaseSettingsModel): title="Project environments", section="---" ) - filter_farm_environment: list[FilterFarmEnvironmentModel] = SettingsField( + filter_env_profiles: list[FilterEnvsProfileModel] = SettingsField( default_factory=list, ) From 6b66de7daadd93cae586c8829468a7886f758cac Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:34:22 +0200 Subject: [PATCH 120/144] Change name of variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 564dd92bd2..986a9ed1c5 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -357,5 +357,5 @@ DEFAULT_VALUES = { {}, indent=4 ), - "filter_farm_environment": [], + "filter_env_profiles": [], } From 6a4196c5b48e133fade1aec7bc396691f42f6469 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:34:35 +0200 Subject: [PATCH 121/144] Change name of variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hooks/pre_filter_farm_environments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 0f83c0d3e0..d231acf5e9 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -23,7 +23,7 @@ class FilterFarmEnvironments(PreLaunchHook): data = self.launch_context.data project_settings = data["project_settings"] filter_env_profiles = ( - project_settings["core"]["filter_farm_environment"]) + project_settings["core"]["filter_env_profiles"]) if not filter_env_profiles: self.log.debug("No profiles found for env var filtering") From 2003ae81b40d7c12585c6ef60de250af923eff2b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:35:06 +0200 Subject: [PATCH 122/144] Change name of model Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 986a9ed1c5..0972ccdfb9 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -175,7 +175,7 @@ class EnvironmentReplacementModel(BaseSettingsModel): replacement: str = SettingsField("", title="Replacement") -class FilterFarmEnvironmentModel(BaseSettingsModel): +class FilterEnvsProfileModel(BaseSettingsModel): _layout = "expanded" host_names: list[str] = SettingsField( From f90ac20f463e696b751544f2505297c7e9499d68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:34:25 +0200 Subject: [PATCH 123/144] add Literal to docstring --- client/ayon_core/tools/tray/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 7c80f467f2..ad190482a8 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -364,7 +364,8 @@ def show_message_in_tray( Args: title (str): Message title. message (str): Message content. - icon (Optional[str]): Icon for the message. + icon (Optional[Literal["information", "warning", "critical"]]): Icon + for the message. msecs (Optional[int]): Duration of the message. tray_url (Optional[str]): Tray server url. From a7f56175d6bbdfc3accba9843f1d322e9405997b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:36:41 +0200 Subject: [PATCH 124/144] move some logic from cli_commands.py to cli.py --- client/ayon_core/cli.py | 38 ++++++++++++++++++++------------ client/ayon_core/cli_commands.py | 34 ---------------------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 0a9bb2aa9c..acc7dfb6d4 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -5,6 +5,7 @@ import sys import code import traceback from pathlib import Path +import warnings import click import acre @@ -116,14 +117,25 @@ def extractenvironments( This function is deprecated and will be removed in future. Please use 'addon applications extractenvironments ...' instead. """ - Commands.extractenvironments( - output_json_path, - project, - asset, - task, - app, - envgroup, - ctx.obj["addons_manager"] + warnings.warn( + ( + "Command 'extractenvironments' is deprecated and will be" + " removed in future. Please use" + " 'addon applications extractenvironments ...' instead." + ), + DeprecationWarning + ) + + addons_manager = ctx.obj["addons_manager"] + applications_addon = addons_manager.get_enabled_addon("applications") + if applications_addon is None: + raise RuntimeError( + "Applications addon is not available or enabled." + ) + + # Please ignore the fact this is using private method + applications_addon._cli_extract_environments( + output_json_path, project, asset, task, app, envgroup ) @@ -170,12 +182,10 @@ def contextselection( Context is project name, folder path and task name. The result is stored into json file which path is passed in first argument. """ - Commands.contextselection( - output_path, - project, - folder, - strict - ) + from ayon_core.tools.context_dialog import main + + main(output_path, project, folder, strict) + @main_cli.command( diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 8ae1ebb3ba..085dd5bb04 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -2,7 +2,6 @@ """Implementation of AYON commands.""" import os import sys -import warnings from typing import Optional, List from ayon_core.addon import AddonsManager @@ -136,36 +135,3 @@ class Commands: log.info("Publish finished.") - @staticmethod - def extractenvironments( - output_json_path, project, asset, task, app, env_group, addons_manager - ): - """Produces json file with environment based on project and app. - - Called by Deadline plugin to propagate environment into render jobs. - """ - warnings.warn( - ( - "Command 'extractenvironments' is deprecated and will be" - " removed in future. Please use " - "'addon applications extractenvironments ...' instead." - ), - DeprecationWarning - ) - - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is None: - raise RuntimeError( - "Applications addon is not available or enabled." - ) - - # Please ignore the fact this is using private method - applications_addon._cli_extract_environments( - output_json_path, project, asset, task, app, env_group - ) - - @staticmethod - def contextselection(output_path, project_name, folder_path, strict): - from ayon_core.tools.context_dialog import main - - main(output_path, project_name, folder_path, strict) From 78c278cdde4da37404d79b4008ff7323411b4af2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:47:28 +0200 Subject: [PATCH 125/144] move publish to cli.py --- client/ayon_core/cli.py | 99 +++++++++++++++++++++- client/ayon_core/cli_commands.py | 137 ------------------------------- 2 files changed, 97 insertions(+), 139 deletions(-) delete mode 100644 client/ayon_core/cli_commands.py diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index acc7dfb6d4..b7dad94346 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -19,7 +19,6 @@ from ayon_core.lib import ( Logger, ) -from .cli_commands import Commands class AliasedGroup(click.Group): @@ -152,7 +151,103 @@ def publish(ctx, path, targets, gui): Publish collects json from path provided as an argument. """ - Commands.publish(path, targets, gui, ctx.obj["addons_manager"]) + import ayon_api + import pyblish.util + + from ayon_core.pipeline import ( + install_ayon_plugins, + get_global_context, + ) + + # Register target and host + if not isinstance(path, str): + raise RuntimeError("Path to JSON must be a string.") + + # Fix older jobs + for src_key, dst_key in ( + ("AVALON_PROJECT", "AYON_PROJECT_NAME"), + ("AVALON_ASSET", "AYON_FOLDER_PATH"), + ("AVALON_TASK", "AYON_TASK_NAME"), + ("AVALON_WORKDIR", "AYON_WORKDIR"), + ("AVALON_APP_NAME", "AYON_APP_NAME"), + ("AVALON_APP", "AYON_HOST_NAME"), + ): + if src_key in os.environ and dst_key not in os.environ: + os.environ[dst_key] = os.environ[src_key] + # Remove old keys, so we're sure they're not used + os.environ.pop(src_key, None) + + log = Logger.get_logger("CLI-publish") + + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + con = ayon_api.get_server_api_connection() + try: + con.set_default_service_username(username) + except ValueError: + pass + + install_ayon_plugins() + + addons_manager = ctx.obj["addons_manager"] + + # TODO validate if this has to happen + # - it should happen during 'install_ayon_plugins' + publish_paths = addons_manager.collect_plugin_paths()["publish"] + for plugin_path in publish_paths: + pyblish.api.register_plugin_path(plugin_path) + + applications_addon = addons_manager.get_enabled_addon("applications") + if applications_addon is not None: + context = get_global_context() + env = applications_addon.get_farm_publish_environment_variables( + context["project_name"], + context["folder_path"], + context["task_name"], + ) + os.environ.update(env) + + pyblish.api.register_host("shell") + + if targets: + for target in targets: + print(f"setting target: {target}") + pyblish.api.register_target(target) + else: + pyblish.api.register_target("farm") + + os.environ["AYON_PUBLISH_DATA"] = path + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib + + log.info("Running publish ...") + + plugins = pyblish.api.discover() + print("Using plugins:") + for plugin in plugins: + print(plugin) + + if gui: + from ayon_core.tools.utils.host_tools import show_publish + from ayon_core.tools.utils.lib import qt_app_context + with qt_app_context(): + show_publish() + else: + # Error exit as soon as any error occurs. + error_format = ("Failed {plugin.__name__}: " + "{error} -- {error.traceback}") + + for result in pyblish.util.publish_iter(): + if result["error"]: + log.error(error_format.format(**result)) + # uninstall() + sys.exit(1) + + log.info("Publish finished.") @main_cli.command(context_settings={"ignore_unknown_options": True}) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py deleted file mode 100644 index 085dd5bb04..0000000000 --- a/client/ayon_core/cli_commands.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -"""Implementation of AYON commands.""" -import os -import sys -from typing import Optional, List - -from ayon_core.addon import AddonsManager - - -class Commands: - """Class implementing commands used by AYON. - - Most of its methods are called by :mod:`cli` module. - """ - @staticmethod - def publish( - path: str, - targets: Optional[List[str]] = None, - gui: Optional[bool] = False, - addons_manager: Optional[AddonsManager] = None, - ) -> None: - """Start headless publishing. - - Publish use json from passed path argument. - - Args: - path (str): Path to JSON. - targets (Optional[List[str]]): List of pyblish targets. - gui (Optional[bool]): Show publish UI. - addons_manager (Optional[AddonsManager]): Addons manager instance. - - Raises: - RuntimeError: When there is no path to process. - RuntimeError: When executed with list of JSON paths. - - """ - from ayon_core.lib import Logger - - from ayon_core.addon import AddonsManager - from ayon_core.pipeline import ( - install_ayon_plugins, - get_global_context, - ) - - import ayon_api - import pyblish.util - - # Register target and host - if not isinstance(path, str): - raise RuntimeError("Path to JSON must be a string.") - - # Fix older jobs - for src_key, dst_key in ( - ("AVALON_PROJECT", "AYON_PROJECT_NAME"), - ("AVALON_ASSET", "AYON_FOLDER_PATH"), - ("AVALON_TASK", "AYON_TASK_NAME"), - ("AVALON_WORKDIR", "AYON_WORKDIR"), - ("AVALON_APP_NAME", "AYON_APP_NAME"), - ("AVALON_APP", "AYON_HOST_NAME"), - ): - if src_key in os.environ and dst_key not in os.environ: - os.environ[dst_key] = os.environ[src_key] - # Remove old keys, so we're sure they're not used - os.environ.pop(src_key, None) - - log = Logger.get_logger("CLI-publish") - - # Make public ayon api behave as other user - # - this works only if public ayon api is using service user - username = os.environ.get("AYON_USERNAME") - if username: - # NOTE: ayon-python-api does not have public api function to find - # out if is used service user. So we need to have try > except - # block. - con = ayon_api.get_server_api_connection() - try: - con.set_default_service_username(username) - except ValueError: - pass - - install_ayon_plugins() - - if addons_manager is None: - addons_manager = AddonsManager() - - publish_paths = addons_manager.collect_plugin_paths()["publish"] - - for plugin_path in publish_paths: - pyblish.api.register_plugin_path(plugin_path) - - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is not None: - context = get_global_context() - env = applications_addon.get_farm_publish_environment_variables( - context["project_name"], - context["folder_path"], - context["task_name"], - ) - os.environ.update(env) - - pyblish.api.register_host("shell") - - if targets: - for target in targets: - print(f"setting target: {target}") - pyblish.api.register_target(target) - else: - pyblish.api.register_target("farm") - - os.environ["AYON_PUBLISH_DATA"] = path - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - - log.info("Running publish ...") - - plugins = pyblish.api.discover() - print("Using plugins:") - for plugin in plugins: - print(plugin) - - if gui: - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context - with qt_app_context(): - show_publish() - else: - # Error exit as soon as any error occurs. - error_format = ("Failed {plugin.__name__}: " - "{error} -- {error.traceback}") - - for result in pyblish.util.publish_iter(): - if result["error"]: - log.error(error_format.format(**result)) - # uninstall() - sys.exit(1) - - log.info("Publish finished.") - From 856a30cd5a2e34349a35cdc39ac51a2d9cc87ad8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:48:31 +0200 Subject: [PATCH 126/144] remove gui option --- client/ayon_core/cli.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index b7dad94346..c1b5e5d5fc 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -143,9 +143,7 @@ def extractenvironments( @click.argument("path", required=True) @click.option("-t", "--targets", help="Targets", default=None, multiple=True) -@click.option("-g", "--gui", is_flag=True, - help="Show Publish UI", default=False) -def publish(ctx, path, targets, gui): +def publish(ctx, path, targets): """Start CLI publishing. Publish collects json from path provided as an argument. @@ -231,21 +229,15 @@ def publish(ctx, path, targets, gui): for plugin in plugins: print(plugin) - if gui: - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context - with qt_app_context(): - show_publish() - else: - # Error exit as soon as any error occurs. - error_format = ("Failed {plugin.__name__}: " - "{error} -- {error.traceback}") + # Error exit as soon as any error occurs. + error_format = ("Failed {plugin.__name__}: " + "{error} -- {error.traceback}") - for result in pyblish.util.publish_iter(): - if result["error"]: - log.error(error_format.format(**result)) - # uninstall() - sys.exit(1) + for result in pyblish.util.publish_iter(): + if result["error"]: + log.error(error_format.format(**result)) + # uninstall() + sys.exit(1) log.info("Publish finished.") From f82c420fe499bfd168d0ee3e773bf2dc064c17ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:54:42 +0200 Subject: [PATCH 127/144] create function for cli in publish --- client/ayon_core/cli.py | 92 +------------- client/ayon_core/pipeline/publish/__init__.py | 4 + client/ayon_core/pipeline/publish/lib.py | 114 +++++++++++++++++- 3 files changed, 119 insertions(+), 91 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index c1b5e5d5fc..db6674d88f 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -149,97 +149,9 @@ def publish(ctx, path, targets): Publish collects json from path provided as an argument. """ - import ayon_api - import pyblish.util + from ayon_core.pipeline.publish import main_cli_publish - from ayon_core.pipeline import ( - install_ayon_plugins, - get_global_context, - ) - - # Register target and host - if not isinstance(path, str): - raise RuntimeError("Path to JSON must be a string.") - - # Fix older jobs - for src_key, dst_key in ( - ("AVALON_PROJECT", "AYON_PROJECT_NAME"), - ("AVALON_ASSET", "AYON_FOLDER_PATH"), - ("AVALON_TASK", "AYON_TASK_NAME"), - ("AVALON_WORKDIR", "AYON_WORKDIR"), - ("AVALON_APP_NAME", "AYON_APP_NAME"), - ("AVALON_APP", "AYON_HOST_NAME"), - ): - if src_key in os.environ and dst_key not in os.environ: - os.environ[dst_key] = os.environ[src_key] - # Remove old keys, so we're sure they're not used - os.environ.pop(src_key, None) - - log = Logger.get_logger("CLI-publish") - - # Make public ayon api behave as other user - # - this works only if public ayon api is using service user - username = os.environ.get("AYON_USERNAME") - if username: - # NOTE: ayon-python-api does not have public api function to find - # out if is used service user. So we need to have try > except - # block. - con = ayon_api.get_server_api_connection() - try: - con.set_default_service_username(username) - except ValueError: - pass - - install_ayon_plugins() - - addons_manager = ctx.obj["addons_manager"] - - # TODO validate if this has to happen - # - it should happen during 'install_ayon_plugins' - publish_paths = addons_manager.collect_plugin_paths()["publish"] - for plugin_path in publish_paths: - pyblish.api.register_plugin_path(plugin_path) - - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is not None: - context = get_global_context() - env = applications_addon.get_farm_publish_environment_variables( - context["project_name"], - context["folder_path"], - context["task_name"], - ) - os.environ.update(env) - - pyblish.api.register_host("shell") - - if targets: - for target in targets: - print(f"setting target: {target}") - pyblish.api.register_target(target) - else: - pyblish.api.register_target("farm") - - os.environ["AYON_PUBLISH_DATA"] = path - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - - log.info("Running publish ...") - - plugins = pyblish.api.discover() - print("Using plugins:") - for plugin in plugins: - print(plugin) - - # Error exit as soon as any error occurs. - error_format = ("Failed {plugin.__name__}: " - "{error} -- {error.traceback}") - - for result in pyblish.util.publish_iter(): - if result["error"]: - log.error(error_format.format(**result)) - # uninstall() - sys.exit(1) - - log.info("Publish finished.") + main_cli_publish(path, targets, ctx.obj["addons_manager"]) @main_cli.command(context_settings={"ignore_unknown_options": True}) diff --git a/client/ayon_core/pipeline/publish/__init__.py b/client/ayon_core/pipeline/publish/__init__.py index d507972664..ab19b6e360 100644 --- a/client/ayon_core/pipeline/publish/__init__.py +++ b/client/ayon_core/pipeline/publish/__init__.py @@ -42,6 +42,8 @@ from .lib import ( get_plugin_settings, get_publish_instance_label, get_publish_instance_families, + + main_cli_publish, ) from .abstract_expected_files import ExpectedFiles @@ -92,6 +94,8 @@ __all__ = ( "get_publish_instance_label", "get_publish_instance_families", + "main_cli_publish", + "ExpectedFiles", "RenderInstance", diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index c4e7b2a42c..8b82622e4c 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -4,8 +4,9 @@ import inspect import copy import tempfile import xml.etree.ElementTree -from typing import Optional, Union +from typing import Optional, Union, List +import ayon_api import pyblish.util import pyblish.plugin import pyblish.api @@ -16,6 +17,7 @@ from ayon_core.lib import ( filter_profiles, ) from ayon_core.settings import get_project_settings +from ayon_core.addon import AddonsManager from ayon_core.pipeline import ( tempdir, Anatomy @@ -978,3 +980,113 @@ def get_instance_expected_output_path( path_template_obj = anatomy.get_template_item("publish", "default")["path"] template_filled = path_template_obj.format_strict(template_data) return os.path.normpath(template_filled) + + +def main_cli_publish( + path: str, + targets: Optional[List[str]] = None, + addons_manager: Optional[AddonsManager] = None, +): + """Start headless publishing. + + Publish use json from passed path argument. + + Args: + path (str): Path to JSON. + targets (Optional[List[str]]): List of pyblish targets. + addons_manager (Optional[AddonsManager]): Addons manager instance. + + Raises: + RuntimeError: When there is no path to process or when executed with + list of JSON paths. + + """ + from ayon_core.pipeline import ( + install_ayon_plugins, + get_global_context, + ) + + # Register target and host + if not isinstance(path, str): + raise RuntimeError("Path to JSON must be a string.") + + # Fix older jobs + for src_key, dst_key in ( + ("AVALON_PROJECT", "AYON_PROJECT_NAME"), + ("AVALON_ASSET", "AYON_FOLDER_PATH"), + ("AVALON_TASK", "AYON_TASK_NAME"), + ("AVALON_WORKDIR", "AYON_WORKDIR"), + ("AVALON_APP_NAME", "AYON_APP_NAME"), + ("AVALON_APP", "AYON_HOST_NAME"), + ): + if src_key in os.environ and dst_key not in os.environ: + os.environ[dst_key] = os.environ[src_key] + # Remove old keys, so we're sure they're not used + os.environ.pop(src_key, None) + + log = Logger.get_logger("CLI-publish") + + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + con = ayon_api.get_server_api_connection() + try: + con.set_default_service_username(username) + except ValueError: + pass + + install_ayon_plugins() + + if addons_manager is None: + addons_manager = AddonsManager() + + # TODO validate if this has to happen + # - it should happen during 'install_ayon_plugins' + publish_paths = addons_manager.collect_plugin_paths()["publish"] + for plugin_path in publish_paths: + pyblish.api.register_plugin_path(plugin_path) + + applications_addon = addons_manager.get_enabled_addon("applications") + if applications_addon is not None: + context = get_global_context() + env = applications_addon.get_farm_publish_environment_variables( + context["project_name"], + context["folder_path"], + context["task_name"], + ) + os.environ.update(env) + + pyblish.api.register_host("shell") + + if targets: + for target in targets: + print(f"setting target: {target}") + pyblish.api.register_target(target) + else: + pyblish.api.register_target("farm") + + os.environ["AYON_PUBLISH_DATA"] = path + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib + + log.info("Running publish ...") + + plugins = pyblish.api.discover() + print("Using plugins:") + for plugin in plugins: + print(plugin) + + # Error exit as soon as any error occurs. + error_format = ("Failed {plugin.__name__}: " + "{error} -- {error.traceback}") + + for result in pyblish.util.publish_iter(): + if result["error"]: + log.error(error_format.format(**result)) + # uninstall() + sys.exit(1) + + log.info("Publish finished.") From 34305862f4a7b1b38b1ba8436d6ccd7aad178551 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 29 Jul 2024 23:44:54 +0200 Subject: [PATCH 128/144] Tweak grammar plus make it more understandable what addon needs updating --- client/ayon_core/addon/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 0ffad2045e..08dd2d6bbd 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -235,10 +235,10 @@ def _handle_moved_addons(addon_name, milestone_version, log): "client", ) if not os.path.exists(addon_dir): - log.error(( - "Addon '{}' is not be available." - " Please update applications addon to '{}' or higher." - ).format(addon_name, milestone_version)) + log.error( + f"Addon '{addon_name}' is not available. Please update " + f"{addon_name} addon to '{milestone_version}' or higher." + ) return None log.warning(( From ddd5313a415b67c4d1af4ece9d178932f73a79fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:42:23 +0200 Subject: [PATCH 129/144] add optional output of ensude function --- client/ayon_core/addon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 8b7740c89c..caa6b70a85 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -66,7 +66,7 @@ def ensure_addons_are_process_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, exit_on_failure: bool = True, -): +) -> Optional[Exception]: """Ensure all enabled addons are ready to be used in the given context. Call this method only in AYON launcher process and as first thing From f8e1b6eeb106666c1904eb27418a2c3cc789f605 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:59:28 +0200 Subject: [PATCH 130/144] added one more ensure function for easier approach. --- client/ayon_core/addon/__init__.py | 2 ++ client/ayon_core/addon/utils.py | 33 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index 497987fa22..6a7ce8a3cb 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -17,6 +17,7 @@ from .base import ( ) from .utils import ( + ensure_addons_are_process_context_ready, ensure_addons_are_process_ready, ) @@ -36,5 +37,6 @@ __all__ = ( "AddonsManager", "load_addons", + "ensure_addons_are_process_context_ready", "ensure_addons_are_process_ready", ) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index caa6b70a85..d96809de49 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,7 +62,7 @@ def _handle_error( os.remove(tmp_path) -def ensure_addons_are_process_ready( +def ensure_addons_are_process_context_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, exit_on_failure: bool = True, @@ -134,7 +134,7 @@ def ensure_addons_are_process_ready( if failed: detail = None if use_detail: - # In case stdout was not captured, use the tracebacks + # In case stdout was not captured, use the tracebacks as detail if not output_str: output_str = "\n".join(tracebacks) detail = output_str @@ -143,3 +143,32 @@ def ensure_addons_are_process_ready( if not exit_on_failure: return exception sys.exit(1) + + +def ensure_addons_are_process_ready( + addons_manager: Optional[AddonsManager] = None, + exit_on_failure: bool = True, + **kwargs, +) -> Optional[Exception]: + """Ensure all enabled addons are ready to be used in the given context. + + Call this method only in AYON launcher process and as first thing + to avoid possible clashes with preparation. For example 'QApplication' + should not be created. + + Args: + addons_manager (Optional[AddonsManager]): The addons + manager to use. If not provided, a new one will be created. + exit_on_failure (bool, optional): If True, the process will exit + if an error occurs. Defaults to True. + kwargs: The keyword arguments to pass to the ProcessContext. + + Returns: + Optional[Exception]: The exception that occurred during the + preparation, if any. + + """ + context: ProcessContext = ProcessContext(**kwargs) + return ensure_addons_are_process_context_ready( + context, addons_manager, exit_on_failure + ) From 943245b698557f1bd8fa36aab30d6c2fc8547e3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:05:20 +0200 Subject: [PATCH 131/144] start tray if is not running --- client/ayon_core/addon/utils.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index d96809de49..b6f665b677 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,6 +62,12 @@ def _handle_error( os.remove(tmp_path) +def _start_tray(process_context): + from ayon_core.tools.tray import make_sure_tray_is_running + + make_sure_tray_is_running() + + def ensure_addons_are_process_context_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, @@ -131,18 +137,22 @@ def ensure_addons_are_process_context_ready( output_str = output.getvalue() # Print stdout/stderr to console as it was redirected print(output_str) - if failed: - detail = None - if use_detail: - # In case stdout was not captured, use the tracebacks as detail - if not output_str: - output_str = "\n".join(tracebacks) - detail = output_str + if not failed: + if not process_context.headless: + _start_tray(process_context) + return None - _handle_error(process_context, message, detail) - if not exit_on_failure: - return exception - sys.exit(1) + detail = None + if use_detail: + # In case stdout was not captured, use the tracebacks as detail + if not output_str: + output_str = "\n".join(tracebacks) + detail = output_str + + _handle_error(process_context, message, detail) + if not exit_on_failure: + return exception + sys.exit(1) def ensure_addons_are_process_ready( From fa729cdcdd3b04c2e03fa15b157ec5869f4872d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:29:25 +0200 Subject: [PATCH 132/144] don't require process context to start tray function --- client/ayon_core/addon/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index b6f665b677..ac5ff25984 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,7 +62,7 @@ def _handle_error( os.remove(tmp_path) -def _start_tray(process_context): +def _start_tray(): from ayon_core.tools.tray import make_sure_tray_is_running make_sure_tray_is_running() @@ -139,7 +139,7 @@ def ensure_addons_are_process_context_ready( print(output_str) if not failed: if not process_context.headless: - _start_tray(process_context) + _start_tray() return None detail = None From aebc9a90d65bc1fd1697543ac7d14e2f9fc20f42 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 Jul 2024 15:35:18 +0200 Subject: [PATCH 133/144] Update opencolorio version to ^2.3.2 in pyproject.toml Bump opencolorio version from 2.2.1 to ^2.3.2 in the pyproject.toml file for compatibility and potential enhancements with other dependencies. --- client/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyproject.toml b/client/pyproject.toml index ca88a37125..a0be9605b6 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -15,6 +15,6 @@ qtawesome = "0.7.3" aiohttp-middlewares = "^2.0.0" Click = "^8" OpenTimelineIO = "0.16.0" -opencolorio = "2.2.1" +opencolorio = "^2.3.2" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" From 3b942cefbe384042abc7781fec93e3c5a1a48788 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:46:25 +0200 Subject: [PATCH 134/144] bump version to '0.4.3' --- client/ayon_core/version.py | 2 +- package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a8c42ec80a..85f0814bfe 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.4.3-dev.1" +__version__ = "0.4.3" diff --git a/package.py b/package.py index 4f2d2b16b4..0902df7618 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.4.3-dev.1" +version = "0.4.3" client_dir = "ayon_core" From b6fba133226af8a267eff0aa7028db414c064e7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:47:02 +0200 Subject: [PATCH 135/144] bump version to '0.4.4-dev.1' --- client/ayon_core/version.py | 2 +- package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 85f0814bfe..55a14ba567 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.4.3" +__version__ = "0.4.4-dev.1" diff --git a/package.py b/package.py index 0902df7618..ca4006425d 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.4.3" +version = "0.4.4-dev.1" client_dir = "ayon_core" From 8cd6a2f342431d70afa9433b75d026ec7bb78e0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:26:34 +0200 Subject: [PATCH 136/144] wrap tray info into object --- client/ayon_core/tools/tray/lib.py | 311 +++++++++++++++++++++-------- 1 file changed, 233 insertions(+), 78 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index ad190482a8..e6de884cb5 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -144,17 +144,6 @@ def get_tray_storage_dir() -> str: return get_ayon_appdirs("tray") -def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]: - if not tray_url: - return None - try: - response = requests.get(f"{tray_url}/tray") - response.raise_for_status() - return response.json() - except (requests.HTTPError, requests.ConnectionError): - return None - - def _get_tray_info_filepath( server_url: Optional[str] = None, variant: Optional[str] = None @@ -165,6 +154,51 @@ def _get_tray_info_filepath( return os.path.join(hash_dir, filename) +def _get_tray_file_info( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> Tuple[Optional[Dict[str, Any]], Optional[float]]: + filepath = _get_tray_info_filepath(server_url, variant) + if not os.path.exists(filepath): + return None, None + file_modified = os.path.getmtime(filepath) + try: + with open(filepath, "r") as stream: + data = json.load(stream) + except Exception: + return None, file_modified + + return data, file_modified + + +def _remove_tray_server_url( + server_url: Optional[str], + variant: Optional[str], + file_modified: Optional[float], +): + """Remove tray information file. + + Called from tray logic, do not use on your own. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + file_modified (Optional[float]): File modified timestamp. Is validated + against current state of file. + + """ + filepath = _get_tray_info_filepath(server_url, variant) + if not os.path.exists(filepath): + return + + if ( + file_modified is not None + and os.path.getmtime(filepath) != file_modified + ): + return + os.remove(filepath) + + def get_tray_file_info( server_url: Optional[str] = None, variant: Optional[str] = None @@ -182,15 +216,156 @@ def get_tray_file_info( Optional[Dict[str, Any]]: Tray information. """ - filepath = _get_tray_info_filepath(server_url, variant) - if not os.path.exists(filepath): + file_info, _ = _get_tray_file_info(server_url, variant) + return file_info + + +def _get_tray_rest_information(tray_url: str) -> Optional[Dict[str, Any]]: + if not tray_url: return None try: - with open(filepath, "r") as stream: - data = json.load(stream) - except Exception: + response = requests.get(f"{tray_url}/tray") + response.raise_for_status() + return response.json() + except (requests.HTTPError, requests.ConnectionError): return None - return data + + +class TrayInfo: + def __init__( + self, + server_url: str, + variant: str, + timeout: Optional[int] = None + ): + self.server_url = server_url + self.variant = variant + + if timeout is None: + timeout = 10 + + self._timeout = timeout + + self._file_modified = None + self._file_info = None + self._file_info_cached = False + self._tray_info = None + self._tray_info_cached = False + self._file_state = None + self._state = None + + @classmethod + def new( + cls, + server_url: Optional[str] = None, + variant: Optional[str] = None, + timeout: Optional[int] = None, + wait_to_start: Optional[bool] = True + ) -> "TrayInfo": + server_url, variant = _get_server_and_variant(server_url, variant) + obj = cls(server_url, variant, timeout=timeout) + if wait_to_start: + obj.wait_to_start() + return obj + + def get_pid(self) -> Optional[int]: + file_info = self.get_file_info() + if file_info: + return file_info.get("pid") + return None + + def reset(self): + self._file_modified = None + self._file_info = None + self._file_info_cached = False + self._tray_info = None + self._tray_info_cached = False + self._state = None + self._file_state = None + + def get_file_info(self) -> Optional[Dict[str, Any]]: + if not self._file_info_cached: + file_info, file_modified = _get_tray_file_info( + self.server_url, self.variant + ) + self._file_info = file_info + self._file_modified = file_modified + self._file_info_cached = True + return self._file_info + + def get_file_url(self) -> Optional[str]: + file_info = self.get_file_info() + if file_info: + return file_info.get("url") + return None + + def get_tray_url(self) -> Optional[str]: + info = self.get_tray_info() + if info: + return self.get_file_url() + return None + + def get_tray_info(self) -> Optional[Dict[str, Any]]: + if self._tray_info_cached: + return self._tray_info + + tray_url = self.get_file_url() + tray_info = None + if tray_url: + tray_info = _get_tray_rest_information(tray_url) + + self._tray_info = tray_info + self._tray_info_cached = True + return self._tray_info + + def get_file_state(self) -> int: + if self._file_state is not None: + return self._file_state + + state = TrayState.NOT_RUNNING + file_info = self.get_file_info() + if file_info: + state = TrayState.STARTING + if file_info.get("started") is True: + state = TrayState.RUNNING + self._file_state = state + return self._file_state + + def get_state(self) -> int: + if self._state is not None: + return self._state + + state = self.get_file_state() + if state == TrayState.RUNNING and not self.get_tray_info(): + state = TrayState.NOT_RUNNING + pid = self.pid + if pid: + _kill_tray_process(pid) + # Remove the file as tray is not running anymore and update + # the state of this object. + _remove_tray_server_url( + self.server_url, self.variant, self._file_modified + ) + self.reset() + + self._state = state + return self._state + + def get_ayon_username(self) -> Optional[str]: + tray_info = self.get_tray_info() + if tray_info: + return tray_info.get("username") + return None + + def wait_to_start(self) -> bool: + _wait_for_starting_tray( + self.server_url, self.variant, self._timeout + ) + self.reset() + return self.get_file_state() == TrayState.RUNNING + + pid = property(get_pid) + state = property(get_state) def get_tray_server_url( @@ -214,25 +389,12 @@ def get_tray_server_url( Optional[str]: Tray server url. """ - data = get_tray_file_info(server_url, variant) - if data is None: - return None - - if data.get("started") is False: - data = _wait_for_starting_tray(server_url, variant, timeout) - if data is None: - return None - - url = data.get("url") - if not url: - return None - - if not validate: - return url - - if _get_tray_information(url): - return url - return None + tray_info = TrayInfo.new( + server_url, variant, timeout, wait_to_start=True + ) + if validate: + return tray_info.get_tray_url() + return tray_info.get_file_url() def set_tray_server_url(tray_url: Optional[str], started: bool): @@ -246,10 +408,13 @@ def set_tray_server_url(tray_url: Optional[str], started: bool): that tray is starting up. """ - file_info = get_tray_file_info() - if file_info and file_info["pid"] != os.getpid(): - if not file_info["started"] or _get_tray_information(file_info["url"]): - raise TrayIsRunningError("Tray is already running.") + info = TrayInfo.new(wait_to_start=False) + if ( + info.pid + and info.pid != os.getpid() + and info.state in (TrayState.RUNNING, TrayState.STARTING) + ): + raise TrayIsRunningError("Tray is already running.") filepath = _get_tray_info_filepath() os.makedirs(os.path.dirname(filepath), exist_ok=True) @@ -292,20 +457,21 @@ def remove_tray_server_url(force: Optional[bool] = False): def get_tray_information( server_url: Optional[str] = None, - variant: Optional[str] = None -) -> Optional[Dict[str, Any]]: + variant: Optional[str] = None, + timeout: Optional[int] = None, +) -> TrayInfo: """Get information about tray. Args: server_url (Optional[str]): AYON server url. variant (Optional[str]): Settings variant. + timeout (Optional[int]): Timeout for tray start-up. Returns: - Optional[Dict[str, Any]]: Tray information. + TrayInfo: Tray information. """ - tray_url = get_tray_server_url(server_url, variant) - return _get_tray_information(tray_url) + return TrayInfo.new(server_url, variant, timeout) def get_tray_state( @@ -322,20 +488,8 @@ def get_tray_state( int: Tray state. """ - file_info = get_tray_file_info(server_url, variant) - if file_info is None: - return TrayState.NOT_RUNNING - - if file_info.get("started") is False: - return TrayState.STARTING - - tray_url = file_info.get("url") - info = _get_tray_information(tray_url) - if not info: - # Remove the information as the tray is not running - remove_tray_server_url(force=True) - return TrayState.NOT_RUNNING - return TrayState.RUNNING + tray_info = get_tray_information(server_url, variant) + return tray_info.state def is_tray_running( @@ -435,38 +589,39 @@ def main(force=False): Logger.set_process_name("Tray") - state = get_tray_state() - if force and state in (TrayState.RUNNING, TrayState.STARTING): - file_info = get_tray_file_info() or {} - pid = file_info.get("pid") + tray_info = TrayInfo.new(wait_to_start=False) + file_state = tray_info.get_file_state() + if force and file_state in (TrayState.RUNNING, TrayState.STARTING): + pid = tray_info.pid if pid is not None: _kill_tray_process(pid) remove_tray_server_url(force=True) - state = TrayState.NOT_RUNNING + file_state = TrayState.NOT_RUNNING - if state == TrayState.RUNNING: - show_message_in_tray( - "Tray is already running", - "Your AYON tray application is already running." - ) - print("Tray is already running.") - return + if file_state == TrayState.RUNNING: + if tray_info.get_state() == TrayState.RUNNING: + show_message_in_tray( + "Tray is already running", + "Your AYON tray application is already running." + ) + print("Tray is already running.") + return + file_state = tray_info.get_file_state() - if state == TrayState.STARTING: + if file_state == TrayState.STARTING: print("Tray is starting. Waiting for it to start.") - _wait_for_starting_tray() - state = get_tray_state() - if state == TrayState.RUNNING: + tray_info.wait_to_start() + file_state = tray_info.get_file_state() + if file_state == TrayState.RUNNING: print("Tray started. Exiting.") return - if state == TrayState.STARTING: + if file_state == TrayState.STARTING: print( "Tray did not start in expected time." " Killing the process and starting new." ) - file_info = get_tray_file_info() or {} - pid = file_info.get("pid") + pid = tray_info.pid if pid is not None: _kill_tray_process(pid) remove_tray_server_url(force=True) From 041b93d09a264800c6fc34f4a32c6b15036dd4df Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:26:16 +0200 Subject: [PATCH 137/144] use power of sets --- client/ayon_core/lib/execute.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index f61d892324..1c73a97731 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -210,11 +210,12 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): env = clean_envs_for_ayon_process(os.environ) if add_sys_paths: - new_pythonpath = list(sys.path) - for path in env.get("PYTHONPATH", "").split(os.pathsep): - if not path or path in new_pythonpath: - continue - new_pythonpath.append(path) + pp_set = frozenset(sys.path) + new_pythonpath = list(pp_set) + pythonpath = env.get("PYTHONPATH") or "" + for path in frozenset(pythonpath.split(os.pathsep)): + if path and path not in pp_set: + new_pythonpath.append(path) env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) return run_subprocess(args, env=env, **kwargs) From 3e0d422ed5b7d9243fc85b33825e816d03d0e3e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:03:58 +0200 Subject: [PATCH 138/144] fix paths ordering --- client/ayon_core/lib/execute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 1c73a97731..23da34b6fa 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -210,8 +210,8 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): env = clean_envs_for_ayon_process(os.environ) if add_sys_paths: - pp_set = frozenset(sys.path) - new_pythonpath = list(pp_set) + new_pythonpath = list(sys.path) + pp_set = set(new_pythonpath) pythonpath = env.get("PYTHONPATH") or "" for path in frozenset(pythonpath.split(os.pathsep)): if path and path not in pp_set: From a90c7a0f9029a7587d9bc35182615ce31028bda5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:05:48 +0200 Subject: [PATCH 139/144] update lookup set --- client/ayon_core/lib/execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 23da34b6fa..bc55c27bd8 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -211,11 +211,11 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): if add_sys_paths: new_pythonpath = list(sys.path) - pp_set = set(new_pythonpath) - pythonpath = env.get("PYTHONPATH") or "" - for path in frozenset(pythonpath.split(os.pathsep)): - if path and path not in pp_set: + lookup_set = set(new_pythonpath) + for path in (env.get("PYTHONPATH") or "").split(os.pathsep): + if path and path not in lookup_set: new_pythonpath.append(path) + lookup_set.add(path) env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) return run_subprocess(args, env=env, **kwargs) From c63f50197c90128334cf79afe77d029c1d95abed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:03:56 +0200 Subject: [PATCH 140/144] use outdated color if container is not from latest version --- client/ayon_core/tools/sceneinventory/model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index a40d110476..b7f79986ac 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -217,10 +217,7 @@ class InventoryModel(QtGui.QStandardItemModel): version_item = version_items[repre_info.version_id] version_label = format_version(version_item.version) is_hero = version_item.version < 0 - is_latest = version_item.is_latest - # TODO maybe use different colors for last approved and last - # version? Or don't care about color at all? - if not is_latest and not version_item.is_last_approved: + if not version_item.is_latest: version_color = self.OUTDATED_COLOR status_name = version_item.status From e10a78d761210ae8f9484c653b049d02a16444bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:22:49 +0200 Subject: [PATCH 141/144] shutdown existing tray if is running under different user --- client/ayon_core/tools/tray/lib.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e6de884cb5..f09f400dfa 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -13,7 +13,12 @@ from typing import Optional, Dict, Tuple, Any import ayon_api import requests -from ayon_core.lib import Logger, get_ayon_launcher_args, run_detached_process +from ayon_core.lib import ( + Logger, + get_ayon_launcher_args, + run_detached_process, + get_ayon_username, +) from ayon_core.lib.local_settings import get_ayon_appdirs @@ -590,6 +595,7 @@ def main(force=False): Logger.set_process_name("Tray") tray_info = TrayInfo.new(wait_to_start=False) + file_state = tray_info.get_file_state() if force and file_state in (TrayState.RUNNING, TrayState.STARTING): pid = tray_info.pid @@ -598,6 +604,17 @@ def main(force=False): remove_tray_server_url(force=True) file_state = TrayState.NOT_RUNNING + if file_state in (TrayState.RUNNING, TrayState.STARTING): + expected_username = get_ayon_username() + username = tray_info.get_ayon_username() + # TODO probably show some message to the user??? + if expected_username != username: + pid = tray_info.pid + if pid is not None: + _kill_tray_process(pid) + remove_tray_server_url(force=True) + file_state = TrayState.NOT_RUNNING + if file_state == TrayState.RUNNING: if tray_info.get_state() == TrayState.RUNNING: show_message_in_tray( From d7ab1328cfeab813e55ca0c0c5dd5f590e03e6cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:42:22 +0200 Subject: [PATCH 142/144] 'make_sure_tray_is_running' is also handling username --- client/ayon_core/tools/tray/lib.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index f09f400dfa..0d0ead85d4 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -551,6 +551,7 @@ def show_message_in_tray( def make_sure_tray_is_running( ayon_url: Optional[str] = None, variant: Optional[str] = None, + username: Optional[str] = None, env: Optional[Dict[str, str]] = None ): """Make sure that tray for AYON url and variant is running. @@ -558,19 +559,26 @@ def make_sure_tray_is_running( Args: ayon_url (Optional[str]): AYON server url. variant (Optional[str]): Settings variant. + username (Optional[str]): Username under which should be tray running. env (Optional[Dict[str, str]]): Environment variables for the process. """ - state = get_tray_state(ayon_url, variant) - if state == TrayState.RUNNING: - return + tray_info = TrayInfo.new( + ayon_url, variant, wait_to_start=False + ) + if tray_info.state == TrayState.STARTING: + tray_info.wait_to_start() - if state == TrayState.STARTING: - _wait_for_starting_tray(ayon_url, variant) - state = get_tray_state(ayon_url, variant) - if state == TrayState.RUNNING: + if tray_info.state == TrayState.RUNNING: + if not username: + username = get_ayon_username() + if tray_info.get_ayon_username() == username: return + pid = tray_info.pid + if pid is not None: + _kill_tray_process(pid) + args = get_ayon_launcher_args("tray", "--force") if env is None: env = os.environ.copy() From 0a05731413b411a0fa554b727288e74b13f08aeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:43:15 +0200 Subject: [PATCH 143/144] don't kill tray in make sure function --- client/ayon_core/tools/tray/lib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 0d0ead85d4..7fec47ee56 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -575,10 +575,6 @@ def make_sure_tray_is_running( if tray_info.get_ayon_username() == username: return - pid = tray_info.pid - if pid is not None: - _kill_tray_process(pid) - args = get_ayon_launcher_args("tray", "--force") if env is None: env = os.environ.copy() From 60c919cd3386de4b7b79261806a29ca972e52745 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:24:18 +0200 Subject: [PATCH 144/144] use 'get_default_settings_variant' from utils --- client/ayon_core/tools/tray/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 7fec47ee56..fd84a9bd10 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -10,8 +10,8 @@ import signal import locale from typing import Optional, Dict, Tuple, Any -import ayon_api import requests +from ayon_api.utils import get_default_settings_variant from ayon_core.lib import ( Logger, @@ -39,7 +39,7 @@ def _get_default_server_url() -> str: def _get_default_variant() -> str: """Get default settings variant.""" - return ayon_api.get_default_settings_variant() + return get_default_settings_variant() def _get_server_and_variant(