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 01/40] 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 02/40] 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 03/40] 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 04/40] 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 05/40] 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 06/40] 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 07/40] 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 08/40] 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 09/40] 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 10/40] 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 11/40] 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 12/40] 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 13/40] 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 14/40] 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 15/40] 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 16/40] 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 17/40] 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 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 18/40] 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 19/40] 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 20/40] 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 21/40] 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 22/40] 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 23/40] 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 24/40] 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 25/40] 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 26/40] 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 27/40] 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 28/40] 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 29/40] 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 30/40] 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 31/40] 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 32/40] 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 33/40] 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 34/40] 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 35/40] 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 36/40] 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 37/40] 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 38/40] 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 39/40] '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 40/40] 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