From ec419f556906c4e3a804b02b1c139cb82e1b91ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:12:26 +0200 Subject: [PATCH 01/17] added 'ensure_is_process_ready' method to addon base class --- client/ayon_core/addon/base.py | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index b9ecff4233..c85f38b32a 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -67,6 +67,56 @@ MOVED_ADDON_MILESTONE_VERSIONS = { } +class ProcessPreparationError(Exception): + """Exception that can be used when process preparation failed. + + The message is showed to user (either as UI dialog or printed). If + different error is raised a "generic" error message is showed to user + with option to copy error message to clipboard. + + """ + pass + + +class ProcessContext: + """Context of child process. + + Notes: + This class is used to pass context to child process. It can be used + to use different behavior of addon based on information in + the context. + The context can be enhanced in future versions. + + Args: + addon_name (Optional[str]): Addon name which triggered process. + addon_version (Optional[str]): Addon version which triggered process. + project_name (Optional[str]): Project name. Can be filled in case + process is triggered for specific project. Some addons can have + different behavior based on project. + headless (Optional[bool]): Is process running in headless mode. + + """ + def __init__( + self, + addon_name: Optional[str] = None, + addon_version: Optional[str] = None, + project_name: Optional[str] = None, + headless: Optional[bool] = None, + **kwargs, + ): + if headless is None: + # TODO use lib function to get headless mode + headless = os.getenv("AYON_HEADLESS_MODE") == "1" + self.addon_name: Optional[str] = addon_name + self.addon_version: Optional[str] = addon_version + self.project_name: Optional[str] = project_name + self.headless: bool = headless + + if kwargs: + unknown_keys = ", ".join([f'"{key}"' for key in kwargs.keys()]) + print(f"Unknown keys in ProcessContext: {unknown_keys}") + + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing AYON addons. @@ -588,7 +638,29 @@ class AYONAddon(object): Args: enabled_addons (list[AYONAddon]): Addons that are enabled. """ + pass + def ensure_is_process_ready( + self, process_context: ProcessContext + ): + """Make sure addon is prepared for a process. + + This method is called when some action makes sure that addon has set + necessary data. For example if user should be logged in + and filled credentials in environment variables this method should + ask user for credentials. + + Implementation of this method is optional. + + Note: + The logic can be similar to logic in tray, but in tray not require + to be logged in. + + Args: + process_context (ProcessContext): Context of child + process. + + """ pass def get_global_environments(self): From 9de1f236f16166f51fdfed836284cdf1fd0ed3ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:12:52 +0200 Subject: [PATCH 02/17] prepare utils to run process preparation --- client/ayon_core/addon/__init__.py | 8 +++ client/ayon_core/addon/utils.py | 112 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 client/ayon_core/addon/utils.py diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index fe8865c730..27af2955ed 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -9,12 +9,17 @@ from .interfaces import ( ) from .base import ( + ProcessPreparationError, AYONAddon, AddonsManager, TrayAddonsManager, load_addons, ) +from .utils import ( + ensure_addons_are_process_ready, +) + __all__ = ( "click_wrap", @@ -25,8 +30,11 @@ __all__ = ( "ITrayService", "IHostAddon", + "ProcessPreparationError", "AYONAddon", "AddonsManager", "TrayAddonsManager", "load_addons", + + "ensure_addons_are_process_ready", ) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py new file mode 100644 index 0000000000..092ce502af --- /dev/null +++ b/client/ayon_core/addon/utils.py @@ -0,0 +1,112 @@ +import os +import sys +import contextlib +import tempfile +import json +from io import StringIO +from typing import Optional + +from ayon_core.lib import run_ayon_launcher_process + +from .base import AddonsManager, ProcessContext, ProcessPreparationError + + +def _handle_error( + process_context: ProcessContext, + message: str, + detail: Optional[str], +): + """Handle error in process ready preparation. + + Shows UI to inform user about the error, or prints the message + to stdout if running in headless mode. + + Args: + process_context (ProcessContext): The context in which the + error occurred. + message (str): The message to show. + detail (Optional[str]): The detail message to show (usually + traceback). + + """ + if process_context.headless: + print(detail) + print(f"{10*'*'}\n{message}\n{10*'*'}") + return + + current_dir = os.path.dirname(os.path.abspath(__file__)) + script_path = os.path.join(current_dir, "ui", "process_ready_error.py") + with tempfile.NamedTemporaryFile("w", delete=False) as tmp: + tmp_path = tmp.name + + try: + with open(tmp_path, "w") as stream: + json.dump( + {"message": message, "detail": detail}, + stream + ) + + run_ayon_launcher_process( + "run", script_path, tmp_path + ) + + finally: + os.remove(tmp_path) + + +def ensure_addons_are_process_ready( + process_context: ProcessContext, + addons_manager: Optional[AddonsManager] = None, + exit_on_failure: bool = True, +): + """Ensure all enabled addons are ready to be used in the given context. + + Call this method only in AYON launcher process and as first thing + to avoid possible clashes with preparation. For example 'QApplication' + should not be created. + + Args: + process_context (ProcessContext): The context in which the + addons should be prepared. + addons_manager (Optional[AddonsManager]): The addons + manager to use. If not provided, a new one will be created. + exit_on_failure (bool, optional): If True, the process will exit + if an error occurs. Defaults to True. + + Returns: + Optional[Exception]: The exception that occurred during the + preparation, if any. + + """ + if addons_manager is None: + addons_manager = AddonsManager() + + exc = None + message = None + failed = False + use_detail = False + with contextlib.redirect_stdout(StringIO()) as stdout: + for addon in addons_manager.get_enabled_addons(): + failed = True + try: + addon.ensure_is_process_ready(process_context) + failed = False + except ProcessPreparationError as exc: + message = str(exc) + + except BaseException as exc: + message = "An unexpected error occurred." + use_detail = True + + if failed: + break + + if failed: + detail = None + if use_detail: + detail = stdout.get_value() + + _handle_error(process_context, message, detail) + if not exit_on_failure: + return exc + sys.exit(1) From 01983de888db8118437c6feb104b576cbffc362f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:13:31 +0200 Subject: [PATCH 03/17] implemented dialog showing error --- .../ayon_core/addon/ui/process_ready_error.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 client/ayon_core/addon/ui/process_ready_error.py diff --git a/client/ayon_core/addon/ui/process_ready_error.py b/client/ayon_core/addon/ui/process_ready_error.py new file mode 100644 index 0000000000..78e2b9b9bb --- /dev/null +++ b/client/ayon_core/addon/ui/process_ready_error.py @@ -0,0 +1,132 @@ +import sys +import json +from typing import Optional + +from qtpy import QtWidgets, QtCore + +from ayon_core.style import load_stylesheet +from ayon_core.tools.utils import get_ayon_qt_app + + +class DetailDialog(QtWidgets.QDialog): + def __init__(self, detail, parent): + super().__init__(parent) + + self.setWindowTitle("Detail") + + detail_input = QtWidgets.QPlainTextEdit(self) + detail_input.setPlainText(detail) + detail_input.setReadOnly(True) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(detail_input, 1) + + def showEvent(self, event): + self.resize(600, 400) + super().showEvent(event) + + +class ErrorDialog(QtWidgets.QDialog): + def __init__( + self, + message: str, + detail: Optional[str], + parent: Optional[QtWidgets.QWidget] = None + ): + super().__init__(parent) + + self.setWindowTitle("Preparation failed") + self.setWindowFlags( + self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint + ) + + message_label = QtWidgets.QLabel(self) + + detail_wrapper = QtWidgets.QWidget(self) + + detail_label = QtWidgets.QLabel(detail_wrapper) + + detail_layout = QtWidgets.QVBoxLayout(detail_wrapper) + detail_layout.setContentsMargins(0, 0, 0, 0) + detail_layout.addWidget(detail_label) + + btns_wrapper = QtWidgets.QWidget(self) + + copy_detail_btn = QtWidgets.QPushButton("Copy detail", btns_wrapper) + show_detail_btn = QtWidgets.QPushButton("Show detail", btns_wrapper) + confirm_btn = QtWidgets.QPushButton("Close", btns_wrapper) + + btns_layout = QtWidgets.QHBoxLayout(btns_wrapper) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(copy_detail_btn, 0) + btns_layout.addWidget(show_detail_btn, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(detail_wrapper, 1) + layout.addWidget(btns_wrapper, 0) + + copy_detail_btn.clicked.connect(self._on_copy_clicked) + show_detail_btn.clicked.connect(self._on_show_detail_clicked) + confirm_btn.clicked.connect(self._on_confirm_clicked) + + self._message_label = message_label + self._detail_wrapper = detail_wrapper + self._detail_label = detail_label + + self._copy_detail_btn = copy_detail_btn + self._show_detail_btn = show_detail_btn + self._confirm_btn = confirm_btn + + self._detail_dialog = None + + self._detail = detail + + self.set_message(message, detail) + + def showEvent(self, event): + self.setStyleSheet(load_stylesheet()) + self.resize(320, 140) + super().showEvent(event) + + def set_message(self, message, detail): + self._message_label.setText(message) + self._detail = detail + + for widget in ( + self._copy_detail_btn, + self._show_detail_btn, + ): + widget.setVisible(bool(detail)) + + def _on_copy_clicked(self): + if self._detail: + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self._detail) + + def _on_show_detail_clicked(self): + if self._detail_dialog is None: + self._detail_dialog = DetailDialog(self._detail, self) + self._detail_dialog.show() + + def _on_confirm_clicked(self): + self.accept() + + +def main(): + json_path = sys.argv[-1] + with open(json_path, "r") as stream: + data = json.load(stream) + + message = data["message"] + detail = data["detail"] + app = get_ayon_qt_app() + dialog = ErrorDialog(message, detail) + dialog.show() + app.exec_() + + +if __name__ == "__main__": + main() From 03317229fa0073f8624357ac04ad1706bcff574d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:10:35 +0200 Subject: [PATCH 04/17] add missing import --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index c85f38b32a..4c27cca9d7 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -10,6 +10,7 @@ import threading import collections from uuid import uuid4 from abc import ABCMeta, abstractmethod +from typing import Optional import six import appdirs From c86613922f035162ed801c9dd95c8698022112a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:15:05 +0200 Subject: [PATCH 05/17] better handling of the error --- client/ayon_core/addon/__init__.py | 2 + client/ayon_core/addon/utils.py | 63 ++++++++++++++++++------------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index 27af2955ed..44f495b2a1 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -10,6 +10,7 @@ from .interfaces import ( from .base import ( ProcessPreparationError, + ProcessContext, AYONAddon, AddonsManager, TrayAddonsManager, @@ -31,6 +32,7 @@ __all__ = ( "IHostAddon", "ProcessPreparationError", + "ProcessContext", "AYONAddon", "AddonsManager", "TrayAddonsManager", diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 092ce502af..77dc5a26f6 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -3,6 +3,7 @@ import sys import contextlib import tempfile import json +import traceback from io import StringIO from typing import Optional @@ -21,6 +22,10 @@ def _handle_error( Shows UI to inform user about the error, or prints the message to stdout if running in headless mode. + Todos: + Make this functionality with the dialog as unified function, so it can + be used elsewhere. + Args: process_context (ProcessContext): The context in which the error occurred. @@ -30,7 +35,8 @@ def _handle_error( """ if process_context.headless: - print(detail) + if detail: + print(detail) print(f"{10*'*'}\n{message}\n{10*'*'}") return @@ -38,16 +44,15 @@ def _handle_error( script_path = os.path.join(current_dir, "ui", "process_ready_error.py") with tempfile.NamedTemporaryFile("w", delete=False) as tmp: tmp_path = tmp.name + json.dump( + {"message": message, "detail": detail}, + tmp.file + ) try: - with open(tmp_path, "w") as stream: - json.dump( - {"message": message, "detail": detail}, - stream - ) - run_ayon_launcher_process( - "run", script_path, tmp_path + "run", script_path, tmp_path, + creationflags=0 ) finally: @@ -85,27 +90,35 @@ def ensure_addons_are_process_ready( message = None failed = False use_detail = False - with contextlib.redirect_stdout(StringIO()) as stdout: - for addon in addons_manager.get_enabled_addons(): - failed = True - try: - addon.ensure_is_process_ready(process_context) - failed = False - except ProcessPreparationError as exc: - message = str(exc) + output = StringIO() + with contextlib.redirect_stdout(output): + with contextlib.redirect_stderr(output): + for addon in addons_manager.get_enabled_addons(): + failed = True + try: + addon.ensure_is_process_ready(process_context) + failed = False + except ProcessPreparationError as exc: + message = str(exc) + print(f"Addon preparation failed: '{addon.name}'") + print(message) - except BaseException as exc: - message = "An unexpected error occurred." - use_detail = True + except BaseException as exc: + message = "An unexpected error occurred." + print(f"Addon preparation failed: '{addon.name}'") + print(message) + # Print the traceback so it is in the output + traceback.print_exception(*sys.exc_info()) + use_detail = True - if failed: - break + if failed: + break + output_str = output.getvalue() + # Print stdout/stderr to console as it was redirected + print(output_str) if failed: - detail = None - if use_detail: - detail = stdout.get_value() - + detail = output_str if use_detail else None _handle_error(process_context, message, detail) if not exit_on_failure: return exc From 54f22696a964154c9a1a113cc24bd486ed7ec58d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:27:39 +0200 Subject: [PATCH 06/17] 'run_ayon_launcher_process' can add sys path to python path --- client/ayon_core/lib/execute.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index e89c8f22ee..f61d892324 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -179,7 +179,7 @@ def clean_envs_for_ayon_process(env=None): return env -def run_ayon_launcher_process(*args, **kwargs): +def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): """Execute AYON process with passed arguments and wait. Wrapper for 'run_process' which prepends AYON executable arguments @@ -209,6 +209,14 @@ def run_ayon_launcher_process(*args, **kwargs): # - fill more if you find more env = clean_envs_for_ayon_process(os.environ) + if add_sys_paths: + new_pythonpath = list(sys.path) + for path in env.get("PYTHONPATH", "").split(os.pathsep): + if not path or path in new_pythonpath: + continue + new_pythonpath.append(path) + env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) + return run_subprocess(args, env=env, **kwargs) From 909e88baa799dc6f226c8bca746fffa697a0fa1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:28:09 +0200 Subject: [PATCH 07/17] run the UI by adding sys path and skipping bootstrap --- client/ayon_core/addon/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 77dc5a26f6..3ad112b562 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -51,8 +51,11 @@ def _handle_error( try: run_ayon_launcher_process( - "run", script_path, tmp_path, - creationflags=0 + "--skip-bootstrap", + script_path, + tmp_path, + add_sys_paths=True, + creationflags=0, ) finally: From 4fefd1e8e5270e77b9a66778373873e99513c526 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:53:50 +0200 Subject: [PATCH 08/17] Fix grammar Co-authored-by: Roy Nieterau --- client/ayon_core/addon/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 4c27cca9d7..0c173ee6f6 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -71,8 +71,8 @@ MOVED_ADDON_MILESTONE_VERSIONS = { class ProcessPreparationError(Exception): """Exception that can be used when process preparation failed. - The message is showed to user (either as UI dialog or printed). If - different error is raised a "generic" error message is showed to user + The message is shown to user (either as UI dialog or printed). If + different error is raised a "generic" error message is shown to user with option to copy error message to clipboard. """ @@ -654,7 +654,7 @@ class AYONAddon(object): Implementation of this method is optional. Note: - The logic can be similar to logic in tray, but in tray not require + The logic can be similar to logic in tray, but tray does not require to be logged in. Args: From e7f816795314a931e80f28a26c6a5eb4efa70d31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:55:18 +0200 Subject: [PATCH 09/17] store tracebacks in case stdout is not available --- client/ayon_core/addon/utils.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 3ad112b562..66c2d81a79 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -93,35 +93,50 @@ def ensure_addons_are_process_ready( message = None failed = False use_detail = False + # Wrap the output in StringIO to capture it for details on fail + # - but in case stdout was invalid on start of process also store + # the tracebacks + tracebacks = [] output = StringIO() with contextlib.redirect_stdout(output): with contextlib.redirect_stderr(output): for addon in addons_manager.get_enabled_addons(): - failed = True + addon_failed = True try: addon.ensure_is_process_ready(process_context) - failed = False + addon_failed = False except ProcessPreparationError as exc: message = str(exc) print(f"Addon preparation failed: '{addon.name}'") print(message) except BaseException as exc: + use_detail = True message = "An unexpected error occurred." + formatted_traceback = "".join(traceback.format_exception( + *sys.exc_info() + )) + tracebacks.append(formatted_traceback) print(f"Addon preparation failed: '{addon.name}'") print(message) - # Print the traceback so it is in the output - traceback.print_exception(*sys.exc_info()) - use_detail = True + # Print the traceback so it is in the stdout + print(formatted_traceback) - if failed: + if addon_failed: + failed = True break output_str = output.getvalue() # Print stdout/stderr to console as it was redirected print(output_str) if failed: - detail = output_str if use_detail else None + detail = None + if use_detail: + # In case stdout was not captured, use the tracebacks + if not output_str: + output_str = "\n".join(tracebacks) + detail = output_str + _handle_error(process_context, message, detail) if not exit_on_failure: return exc From e15a2b275f4373025a18c6d9b7a9fe72d2a81edd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:00:30 +0200 Subject: [PATCH 10/17] fix ruff linting --- client/ayon_core/addon/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 66c2d81a79..8b7740c89c 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -89,7 +89,7 @@ def ensure_addons_are_process_ready( if addons_manager is None: addons_manager = AddonsManager() - exc = None + exception = None message = None failed = False use_detail = False @@ -106,11 +106,13 @@ def ensure_addons_are_process_ready( addon.ensure_is_process_ready(process_context) addon_failed = False except ProcessPreparationError as exc: + exception = exc message = str(exc) print(f"Addon preparation failed: '{addon.name}'") print(message) except BaseException as exc: + exception = exc use_detail = True message = "An unexpected error occurred." formatted_traceback = "".join(traceback.format_exception( @@ -139,5 +141,5 @@ def ensure_addons_are_process_ready( _handle_error(process_context, message, detail) if not exit_on_failure: - return exc + return exception sys.exit(1) From ddd5313a415b67c4d1af4ece9d178932f73a79fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:42:23 +0200 Subject: [PATCH 11/17] add optional output of ensude function --- client/ayon_core/addon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 8b7740c89c..caa6b70a85 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -66,7 +66,7 @@ def ensure_addons_are_process_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, exit_on_failure: bool = True, -): +) -> Optional[Exception]: """Ensure all enabled addons are ready to be used in the given context. Call this method only in AYON launcher process and as first thing From f8e1b6eeb106666c1904eb27418a2c3cc789f605 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:59:28 +0200 Subject: [PATCH 12/17] added one more ensure function for easier approach. --- client/ayon_core/addon/__init__.py | 2 ++ client/ayon_core/addon/utils.py | 33 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index 497987fa22..6a7ce8a3cb 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -17,6 +17,7 @@ from .base import ( ) from .utils import ( + ensure_addons_are_process_context_ready, ensure_addons_are_process_ready, ) @@ -36,5 +37,6 @@ __all__ = ( "AddonsManager", "load_addons", + "ensure_addons_are_process_context_ready", "ensure_addons_are_process_ready", ) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index caa6b70a85..d96809de49 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,7 +62,7 @@ def _handle_error( os.remove(tmp_path) -def ensure_addons_are_process_ready( +def ensure_addons_are_process_context_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, exit_on_failure: bool = True, @@ -134,7 +134,7 @@ def ensure_addons_are_process_ready( if failed: detail = None if use_detail: - # In case stdout was not captured, use the tracebacks + # In case stdout was not captured, use the tracebacks as detail if not output_str: output_str = "\n".join(tracebacks) detail = output_str @@ -143,3 +143,32 @@ def ensure_addons_are_process_ready( if not exit_on_failure: return exception sys.exit(1) + + +def ensure_addons_are_process_ready( + addons_manager: Optional[AddonsManager] = None, + exit_on_failure: bool = True, + **kwargs, +) -> Optional[Exception]: + """Ensure all enabled addons are ready to be used in the given context. + + Call this method only in AYON launcher process and as first thing + to avoid possible clashes with preparation. For example 'QApplication' + should not be created. + + Args: + addons_manager (Optional[AddonsManager]): The addons + manager to use. If not provided, a new one will be created. + exit_on_failure (bool, optional): If True, the process will exit + if an error occurs. Defaults to True. + kwargs: The keyword arguments to pass to the ProcessContext. + + Returns: + Optional[Exception]: The exception that occurred during the + preparation, if any. + + """ + context: ProcessContext = ProcessContext(**kwargs) + return ensure_addons_are_process_context_ready( + context, addons_manager, exit_on_failure + ) From 943245b698557f1bd8fa36aab30d6c2fc8547e3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:05:20 +0200 Subject: [PATCH 13/17] start tray if is not running --- client/ayon_core/addon/utils.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index d96809de49..b6f665b677 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,6 +62,12 @@ def _handle_error( os.remove(tmp_path) +def _start_tray(process_context): + from ayon_core.tools.tray import make_sure_tray_is_running + + make_sure_tray_is_running() + + def ensure_addons_are_process_context_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, @@ -131,18 +137,22 @@ def ensure_addons_are_process_context_ready( output_str = output.getvalue() # Print stdout/stderr to console as it was redirected print(output_str) - if failed: - detail = None - if use_detail: - # In case stdout was not captured, use the tracebacks as detail - if not output_str: - output_str = "\n".join(tracebacks) - detail = output_str + if not failed: + if not process_context.headless: + _start_tray(process_context) + return None - _handle_error(process_context, message, detail) - if not exit_on_failure: - return exception - sys.exit(1) + detail = None + if use_detail: + # In case stdout was not captured, use the tracebacks as detail + if not output_str: + output_str = "\n".join(tracebacks) + detail = output_str + + _handle_error(process_context, message, detail) + if not exit_on_failure: + return exception + sys.exit(1) def ensure_addons_are_process_ready( From fa729cdcdd3b04c2e03fa15b157ec5869f4872d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:29:25 +0200 Subject: [PATCH 14/17] don't require process context to start tray function --- client/ayon_core/addon/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index b6f665b677..ac5ff25984 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,7 +62,7 @@ def _handle_error( os.remove(tmp_path) -def _start_tray(process_context): +def _start_tray(): from ayon_core.tools.tray import make_sure_tray_is_running make_sure_tray_is_running() @@ -139,7 +139,7 @@ def ensure_addons_are_process_context_ready( print(output_str) if not failed: if not process_context.headless: - _start_tray(process_context) + _start_tray() return None detail = None From 041b93d09a264800c6fc34f4a32c6b15036dd4df Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:26:16 +0200 Subject: [PATCH 15/17] use power of sets --- client/ayon_core/lib/execute.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index f61d892324..1c73a97731 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -210,11 +210,12 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): env = clean_envs_for_ayon_process(os.environ) if add_sys_paths: - new_pythonpath = list(sys.path) - for path in env.get("PYTHONPATH", "").split(os.pathsep): - if not path or path in new_pythonpath: - continue - new_pythonpath.append(path) + pp_set = frozenset(sys.path) + new_pythonpath = list(pp_set) + pythonpath = env.get("PYTHONPATH") or "" + for path in frozenset(pythonpath.split(os.pathsep)): + if path and path not in pp_set: + new_pythonpath.append(path) env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) return run_subprocess(args, env=env, **kwargs) From 3e0d422ed5b7d9243fc85b33825e816d03d0e3e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:03:58 +0200 Subject: [PATCH 16/17] fix paths ordering --- client/ayon_core/lib/execute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 1c73a97731..23da34b6fa 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -210,8 +210,8 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): env = clean_envs_for_ayon_process(os.environ) if add_sys_paths: - pp_set = frozenset(sys.path) - new_pythonpath = list(pp_set) + new_pythonpath = list(sys.path) + pp_set = set(new_pythonpath) pythonpath = env.get("PYTHONPATH") or "" for path in frozenset(pythonpath.split(os.pathsep)): if path and path not in pp_set: From a90c7a0f9029a7587d9bc35182615ce31028bda5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:05:48 +0200 Subject: [PATCH 17/17] update lookup set --- client/ayon_core/lib/execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 23da34b6fa..bc55c27bd8 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -211,11 +211,11 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): if add_sys_paths: new_pythonpath = list(sys.path) - pp_set = set(new_pythonpath) - pythonpath = env.get("PYTHONPATH") or "" - for path in frozenset(pythonpath.split(os.pathsep)): - if path and path not in pp_set: + lookup_set = set(new_pythonpath) + for path in (env.get("PYTHONPATH") or "").split(os.pathsep): + if path and path not in lookup_set: new_pythonpath.append(path) + lookup_set.add(path) env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) return run_subprocess(args, env=env, **kwargs)