From 7bef86ac79a246487437e50e6c2f2252491f7bf4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 15 Jul 2024 12:55:57 +0200 Subject: [PATCH 01/92] 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 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 02/92] 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 03/92] 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 04/92] 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 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 05/92] 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 06/92] 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 07/92] 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 08/92] 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 09/92] 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 10/92] 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 11/92] 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 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 12/92] 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 13/92] 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 14/92] 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 15/92] 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 16/92] 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 17/92] 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 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 18/92] 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 2af09665865b33b6104ceb23fa4c415d3fe9a4ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:02:21 +0200 Subject: [PATCH 19/92] 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 20/92] 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 21/92] 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 22/92] 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 becf14ed68a5aa2d60a9dca712972baef0194325 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:34:37 +0200 Subject: [PATCH 23/92] 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 24/92] 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 25/92] 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 26/92] 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 eda080d86d987775697728949973019d8d2c1a1d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 15:59:16 +0200 Subject: [PATCH 27/92] 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 28/92] 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 29/92] 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 30/92] 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 31/92] 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 32/92] 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 33/92] 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 34/92] 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 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 35/92] 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 36/92] 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 37/92] 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 38/92] 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 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 39/92] 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 40/92] 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 41/92] 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 42/92] 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 43/92] 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 44/92] 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 45/92] 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 46/92] 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 47/92] 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 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 48/92] 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 49/92] 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 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 50/92] 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 51/92] 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 52/92] 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 53/92] '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 54/92] 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 55/92] 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 56/92] 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 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 57/92] 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 58/92] 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 59/92] 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 60/92] 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 61/92] 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 62/92] 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 63/92] 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 64/92] 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 65/92] 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 66/92] 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 67/92] 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 68/92] 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 69/92] 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 70/92] 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 71/92] 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 72/92] 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 73/92] 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 74/92] 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 75/92] 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 76/92] 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 77/92] 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 78/92] 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 79/92] 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 80/92] 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 81/92] 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 82/92] 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 83/92] 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 84/92] 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 85/92] 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 86/92] 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 87/92] 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 88/92] 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 89/92] 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 90/92] 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 91/92] 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 92/92] 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((