mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' into enhancement/AY-6299_Scene-inventory-mark-all-outdated-containers
This commit is contained in:
commit
5f3a82cb64
6 changed files with 675 additions and 89 deletions
|
|
@ -9,11 +9,18 @@ from .interfaces import (
|
|||
)
|
||||
|
||||
from .base import (
|
||||
ProcessPreparationError,
|
||||
ProcessContext,
|
||||
AYONAddon,
|
||||
AddonsManager,
|
||||
load_addons,
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
ensure_addons_are_process_context_ready,
|
||||
ensure_addons_are_process_ready,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"click_wrap",
|
||||
|
|
@ -24,7 +31,12 @@ __all__ = (
|
|||
"ITrayService",
|
||||
"IHostAddon",
|
||||
|
||||
"ProcessPreparationError",
|
||||
"ProcessContext",
|
||||
"AYONAddon",
|
||||
"AddonsManager",
|
||||
"load_addons",
|
||||
|
||||
"ensure_addons_are_process_context_ready",
|
||||
"ensure_addons_are_process_ready",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import threading
|
|||
import collections
|
||||
from uuid import uuid4
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
import appdirs
|
||||
import ayon_api
|
||||
|
|
@ -64,6 +65,56 @@ MOVED_ADDON_MILESTONE_VERSIONS = {
|
|||
}
|
||||
|
||||
|
||||
class ProcessPreparationError(Exception):
|
||||
"""Exception that can be used when process preparation failed.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
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.
|
||||
|
|
@ -584,7 +635,29 @@ class AYONAddon(ABC):
|
|||
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 tray does not require
|
||||
to be logged in.
|
||||
|
||||
Args:
|
||||
process_context (ProcessContext): Context of child
|
||||
process.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_global_environments(self):
|
||||
|
|
|
|||
132
client/ayon_core/addon/ui/process_ready_error.py
Normal file
132
client/ayon_core/addon/ui/process_ready_error.py
Normal file
|
|
@ -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()
|
||||
184
client/ayon_core/addon/utils.py
Normal file
184
client/ayon_core/addon/utils.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import os
|
||||
import sys
|
||||
import contextlib
|
||||
import tempfile
|
||||
import json
|
||||
import traceback
|
||||
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.
|
||||
|
||||
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.
|
||||
message (str): The message to show.
|
||||
detail (Optional[str]): The detail message to show (usually
|
||||
traceback).
|
||||
|
||||
"""
|
||||
if process_context.headless:
|
||||
if detail:
|
||||
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
|
||||
json.dump(
|
||||
{"message": message, "detail": detail},
|
||||
tmp.file
|
||||
)
|
||||
|
||||
try:
|
||||
run_ayon_launcher_process(
|
||||
"--skip-bootstrap",
|
||||
script_path,
|
||||
tmp_path,
|
||||
add_sys_paths=True,
|
||||
creationflags=0,
|
||||
)
|
||||
|
||||
finally:
|
||||
os.remove(tmp_path)
|
||||
|
||||
|
||||
def _start_tray():
|
||||
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,
|
||||
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
|
||||
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()
|
||||
|
||||
exception = None
|
||||
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():
|
||||
addon_failed = True
|
||||
try:
|
||||
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(
|
||||
*sys.exc_info()
|
||||
))
|
||||
tracebacks.append(formatted_traceback)
|
||||
print(f"Addon preparation failed: '{addon.name}'")
|
||||
print(message)
|
||||
# Print the traceback so it is in the stdout
|
||||
print(formatted_traceback)
|
||||
|
||||
if addon_failed:
|
||||
failed = True
|
||||
break
|
||||
|
||||
output_str = output.getvalue()
|
||||
# Print stdout/stderr to console as it was redirected
|
||||
print(output_str)
|
||||
if not failed:
|
||||
if not process_context.headless:
|
||||
_start_tray()
|
||||
return None
|
||||
|
||||
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(
|
||||
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
|
||||
)
|
||||
|
|
@ -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,15 @@ 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)
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ import signal
|
|||
import locale
|
||||
from typing import Optional, Dict, Tuple, Any
|
||||
|
||||
import ayon_api
|
||||
import requests
|
||||
from ayon_api.utils import get_default_settings_variant
|
||||
|
||||
from ayon_core.lib import Logger, get_ayon_launcher_args, run_detached_process
|
||||
from ayon_core.lib import (
|
||||
Logger,
|
||||
get_ayon_launcher_args,
|
||||
run_detached_process,
|
||||
get_ayon_username,
|
||||
)
|
||||
from ayon_core.lib.local_settings import get_ayon_appdirs
|
||||
|
||||
|
||||
|
|
@ -34,7 +39,7 @@ def _get_default_server_url() -> str:
|
|||
|
||||
def _get_default_variant() -> str:
|
||||
"""Get default settings variant."""
|
||||
return ayon_api.get_default_settings_variant()
|
||||
return get_default_settings_variant()
|
||||
|
||||
|
||||
def _get_server_and_variant(
|
||||
|
|
@ -144,17 +149,6 @@ def get_tray_storage_dir() -> str:
|
|||
return get_ayon_appdirs("tray")
|
||||
|
||||
|
||||
def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]:
|
||||
if not tray_url:
|
||||
return None
|
||||
try:
|
||||
response = requests.get(f"{tray_url}/tray")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except (requests.HTTPError, requests.ConnectionError):
|
||||
return None
|
||||
|
||||
|
||||
def _get_tray_info_filepath(
|
||||
server_url: Optional[str] = None,
|
||||
variant: Optional[str] = None
|
||||
|
|
@ -165,6 +159,51 @@ def _get_tray_info_filepath(
|
|||
return os.path.join(hash_dir, filename)
|
||||
|
||||
|
||||
def _get_tray_file_info(
|
||||
server_url: Optional[str] = None,
|
||||
variant: Optional[str] = None
|
||||
) -> Tuple[Optional[Dict[str, Any]], Optional[float]]:
|
||||
filepath = _get_tray_info_filepath(server_url, variant)
|
||||
if not os.path.exists(filepath):
|
||||
return None, None
|
||||
file_modified = os.path.getmtime(filepath)
|
||||
try:
|
||||
with open(filepath, "r") as stream:
|
||||
data = json.load(stream)
|
||||
except Exception:
|
||||
return None, file_modified
|
||||
|
||||
return data, file_modified
|
||||
|
||||
|
||||
def _remove_tray_server_url(
|
||||
server_url: Optional[str],
|
||||
variant: Optional[str],
|
||||
file_modified: Optional[float],
|
||||
):
|
||||
"""Remove tray information file.
|
||||
|
||||
Called from tray logic, do not use on your own.
|
||||
|
||||
Args:
|
||||
server_url (Optional[str]): AYON server url.
|
||||
variant (Optional[str]): Settings variant.
|
||||
file_modified (Optional[float]): File modified timestamp. Is validated
|
||||
against current state of file.
|
||||
|
||||
"""
|
||||
filepath = _get_tray_info_filepath(server_url, variant)
|
||||
if not os.path.exists(filepath):
|
||||
return
|
||||
|
||||
if (
|
||||
file_modified is not None
|
||||
and os.path.getmtime(filepath) != file_modified
|
||||
):
|
||||
return
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
def get_tray_file_info(
|
||||
server_url: Optional[str] = None,
|
||||
variant: Optional[str] = None
|
||||
|
|
@ -182,15 +221,156 @@ def get_tray_file_info(
|
|||
Optional[Dict[str, Any]]: Tray information.
|
||||
|
||||
"""
|
||||
filepath = _get_tray_info_filepath(server_url, variant)
|
||||
if not os.path.exists(filepath):
|
||||
file_info, _ = _get_tray_file_info(server_url, variant)
|
||||
return file_info
|
||||
|
||||
|
||||
def _get_tray_rest_information(tray_url: str) -> Optional[Dict[str, Any]]:
|
||||
if not tray_url:
|
||||
return None
|
||||
try:
|
||||
with open(filepath, "r") as stream:
|
||||
data = json.load(stream)
|
||||
except Exception:
|
||||
response = requests.get(f"{tray_url}/tray")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except (requests.HTTPError, requests.ConnectionError):
|
||||
return None
|
||||
return data
|
||||
|
||||
|
||||
class TrayInfo:
|
||||
def __init__(
|
||||
self,
|
||||
server_url: str,
|
||||
variant: str,
|
||||
timeout: Optional[int] = None
|
||||
):
|
||||
self.server_url = server_url
|
||||
self.variant = variant
|
||||
|
||||
if timeout is None:
|
||||
timeout = 10
|
||||
|
||||
self._timeout = timeout
|
||||
|
||||
self._file_modified = None
|
||||
self._file_info = None
|
||||
self._file_info_cached = False
|
||||
self._tray_info = None
|
||||
self._tray_info_cached = False
|
||||
self._file_state = None
|
||||
self._state = None
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
server_url: Optional[str] = None,
|
||||
variant: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
wait_to_start: Optional[bool] = True
|
||||
) -> "TrayInfo":
|
||||
server_url, variant = _get_server_and_variant(server_url, variant)
|
||||
obj = cls(server_url, variant, timeout=timeout)
|
||||
if wait_to_start:
|
||||
obj.wait_to_start()
|
||||
return obj
|
||||
|
||||
def get_pid(self) -> Optional[int]:
|
||||
file_info = self.get_file_info()
|
||||
if file_info:
|
||||
return file_info.get("pid")
|
||||
return None
|
||||
|
||||
def reset(self):
|
||||
self._file_modified = None
|
||||
self._file_info = None
|
||||
self._file_info_cached = False
|
||||
self._tray_info = None
|
||||
self._tray_info_cached = False
|
||||
self._state = None
|
||||
self._file_state = None
|
||||
|
||||
def get_file_info(self) -> Optional[Dict[str, Any]]:
|
||||
if not self._file_info_cached:
|
||||
file_info, file_modified = _get_tray_file_info(
|
||||
self.server_url, self.variant
|
||||
)
|
||||
self._file_info = file_info
|
||||
self._file_modified = file_modified
|
||||
self._file_info_cached = True
|
||||
return self._file_info
|
||||
|
||||
def get_file_url(self) -> Optional[str]:
|
||||
file_info = self.get_file_info()
|
||||
if file_info:
|
||||
return file_info.get("url")
|
||||
return None
|
||||
|
||||
def get_tray_url(self) -> Optional[str]:
|
||||
info = self.get_tray_info()
|
||||
if info:
|
||||
return self.get_file_url()
|
||||
return None
|
||||
|
||||
def get_tray_info(self) -> Optional[Dict[str, Any]]:
|
||||
if self._tray_info_cached:
|
||||
return self._tray_info
|
||||
|
||||
tray_url = self.get_file_url()
|
||||
tray_info = None
|
||||
if tray_url:
|
||||
tray_info = _get_tray_rest_information(tray_url)
|
||||
|
||||
self._tray_info = tray_info
|
||||
self._tray_info_cached = True
|
||||
return self._tray_info
|
||||
|
||||
def get_file_state(self) -> int:
|
||||
if self._file_state is not None:
|
||||
return self._file_state
|
||||
|
||||
state = TrayState.NOT_RUNNING
|
||||
file_info = self.get_file_info()
|
||||
if file_info:
|
||||
state = TrayState.STARTING
|
||||
if file_info.get("started") is True:
|
||||
state = TrayState.RUNNING
|
||||
self._file_state = state
|
||||
return self._file_state
|
||||
|
||||
def get_state(self) -> int:
|
||||
if self._state is not None:
|
||||
return self._state
|
||||
|
||||
state = self.get_file_state()
|
||||
if state == TrayState.RUNNING and not self.get_tray_info():
|
||||
state = TrayState.NOT_RUNNING
|
||||
pid = self.pid
|
||||
if pid:
|
||||
_kill_tray_process(pid)
|
||||
# Remove the file as tray is not running anymore and update
|
||||
# the state of this object.
|
||||
_remove_tray_server_url(
|
||||
self.server_url, self.variant, self._file_modified
|
||||
)
|
||||
self.reset()
|
||||
|
||||
self._state = state
|
||||
return self._state
|
||||
|
||||
def get_ayon_username(self) -> Optional[str]:
|
||||
tray_info = self.get_tray_info()
|
||||
if tray_info:
|
||||
return tray_info.get("username")
|
||||
return None
|
||||
|
||||
def wait_to_start(self) -> bool:
|
||||
_wait_for_starting_tray(
|
||||
self.server_url, self.variant, self._timeout
|
||||
)
|
||||
self.reset()
|
||||
return self.get_file_state() == TrayState.RUNNING
|
||||
|
||||
pid = property(get_pid)
|
||||
state = property(get_state)
|
||||
|
||||
|
||||
def get_tray_server_url(
|
||||
|
|
@ -214,25 +394,12 @@ def get_tray_server_url(
|
|||
Optional[str]: Tray server url.
|
||||
|
||||
"""
|
||||
data = get_tray_file_info(server_url, variant)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if data.get("started") is False:
|
||||
data = _wait_for_starting_tray(server_url, variant, timeout)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
url = data.get("url")
|
||||
if not url:
|
||||
return None
|
||||
|
||||
if not validate:
|
||||
return url
|
||||
|
||||
if _get_tray_information(url):
|
||||
return url
|
||||
return None
|
||||
tray_info = TrayInfo.new(
|
||||
server_url, variant, timeout, wait_to_start=True
|
||||
)
|
||||
if validate:
|
||||
return tray_info.get_tray_url()
|
||||
return tray_info.get_file_url()
|
||||
|
||||
|
||||
def set_tray_server_url(tray_url: Optional[str], started: bool):
|
||||
|
|
@ -246,10 +413,13 @@ def set_tray_server_url(tray_url: Optional[str], started: bool):
|
|||
that tray is starting up.
|
||||
|
||||
"""
|
||||
file_info = get_tray_file_info()
|
||||
if file_info and file_info["pid"] != os.getpid():
|
||||
if not file_info["started"] or _get_tray_information(file_info["url"]):
|
||||
raise TrayIsRunningError("Tray is already running.")
|
||||
info = TrayInfo.new(wait_to_start=False)
|
||||
if (
|
||||
info.pid
|
||||
and info.pid != os.getpid()
|
||||
and info.state in (TrayState.RUNNING, TrayState.STARTING)
|
||||
):
|
||||
raise TrayIsRunningError("Tray is already running.")
|
||||
|
||||
filepath = _get_tray_info_filepath()
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
|
|
@ -292,20 +462,21 @@ def remove_tray_server_url(force: Optional[bool] = False):
|
|||
|
||||
def get_tray_information(
|
||||
server_url: Optional[str] = None,
|
||||
variant: Optional[str] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
variant: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
) -> TrayInfo:
|
||||
"""Get information about tray.
|
||||
|
||||
Args:
|
||||
server_url (Optional[str]): AYON server url.
|
||||
variant (Optional[str]): Settings variant.
|
||||
timeout (Optional[int]): Timeout for tray start-up.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Tray information.
|
||||
TrayInfo: Tray information.
|
||||
|
||||
"""
|
||||
tray_url = get_tray_server_url(server_url, variant)
|
||||
return _get_tray_information(tray_url)
|
||||
return TrayInfo.new(server_url, variant, timeout)
|
||||
|
||||
|
||||
def get_tray_state(
|
||||
|
|
@ -322,20 +493,8 @@ def get_tray_state(
|
|||
int: Tray state.
|
||||
|
||||
"""
|
||||
file_info = get_tray_file_info(server_url, variant)
|
||||
if file_info is None:
|
||||
return TrayState.NOT_RUNNING
|
||||
|
||||
if file_info.get("started") is False:
|
||||
return TrayState.STARTING
|
||||
|
||||
tray_url = file_info.get("url")
|
||||
info = _get_tray_information(tray_url)
|
||||
if not info:
|
||||
# Remove the information as the tray is not running
|
||||
remove_tray_server_url(force=True)
|
||||
return TrayState.NOT_RUNNING
|
||||
return TrayState.RUNNING
|
||||
tray_info = get_tray_information(server_url, variant)
|
||||
return tray_info.state
|
||||
|
||||
|
||||
def is_tray_running(
|
||||
|
|
@ -392,6 +551,7 @@ def show_message_in_tray(
|
|||
def make_sure_tray_is_running(
|
||||
ayon_url: Optional[str] = None,
|
||||
variant: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
env: Optional[Dict[str, str]] = None
|
||||
):
|
||||
"""Make sure that tray for AYON url and variant is running.
|
||||
|
|
@ -399,17 +559,20 @@ def make_sure_tray_is_running(
|
|||
Args:
|
||||
ayon_url (Optional[str]): AYON server url.
|
||||
variant (Optional[str]): Settings variant.
|
||||
username (Optional[str]): Username under which should be tray running.
|
||||
env (Optional[Dict[str, str]]): Environment variables for the process.
|
||||
|
||||
"""
|
||||
state = get_tray_state(ayon_url, variant)
|
||||
if state == TrayState.RUNNING:
|
||||
return
|
||||
tray_info = TrayInfo.new(
|
||||
ayon_url, variant, wait_to_start=False
|
||||
)
|
||||
if tray_info.state == TrayState.STARTING:
|
||||
tray_info.wait_to_start()
|
||||
|
||||
if state == TrayState.STARTING:
|
||||
_wait_for_starting_tray(ayon_url, variant)
|
||||
state = get_tray_state(ayon_url, variant)
|
||||
if state == TrayState.RUNNING:
|
||||
if tray_info.state == TrayState.RUNNING:
|
||||
if not username:
|
||||
username = get_ayon_username()
|
||||
if tray_info.get_ayon_username() == username:
|
||||
return
|
||||
|
||||
args = get_ayon_launcher_args("tray", "--force")
|
||||
|
|
@ -435,38 +598,51 @@ def main(force=False):
|
|||
|
||||
Logger.set_process_name("Tray")
|
||||
|
||||
state = get_tray_state()
|
||||
if force and state in (TrayState.RUNNING, TrayState.STARTING):
|
||||
file_info = get_tray_file_info() or {}
|
||||
pid = file_info.get("pid")
|
||||
tray_info = TrayInfo.new(wait_to_start=False)
|
||||
|
||||
file_state = tray_info.get_file_state()
|
||||
if force and file_state in (TrayState.RUNNING, TrayState.STARTING):
|
||||
pid = tray_info.pid
|
||||
if pid is not None:
|
||||
_kill_tray_process(pid)
|
||||
remove_tray_server_url(force=True)
|
||||
state = TrayState.NOT_RUNNING
|
||||
file_state = TrayState.NOT_RUNNING
|
||||
|
||||
if state == TrayState.RUNNING:
|
||||
show_message_in_tray(
|
||||
"Tray is already running",
|
||||
"Your AYON tray application is already running."
|
||||
)
|
||||
print("Tray is already running.")
|
||||
return
|
||||
if file_state in (TrayState.RUNNING, TrayState.STARTING):
|
||||
expected_username = get_ayon_username()
|
||||
username = tray_info.get_ayon_username()
|
||||
# TODO probably show some message to the user???
|
||||
if expected_username != username:
|
||||
pid = tray_info.pid
|
||||
if pid is not None:
|
||||
_kill_tray_process(pid)
|
||||
remove_tray_server_url(force=True)
|
||||
file_state = TrayState.NOT_RUNNING
|
||||
|
||||
if state == TrayState.STARTING:
|
||||
if file_state == TrayState.RUNNING:
|
||||
if tray_info.get_state() == TrayState.RUNNING:
|
||||
show_message_in_tray(
|
||||
"Tray is already running",
|
||||
"Your AYON tray application is already running."
|
||||
)
|
||||
print("Tray is already running.")
|
||||
return
|
||||
file_state = tray_info.get_file_state()
|
||||
|
||||
if file_state == TrayState.STARTING:
|
||||
print("Tray is starting. Waiting for it to start.")
|
||||
_wait_for_starting_tray()
|
||||
state = get_tray_state()
|
||||
if state == TrayState.RUNNING:
|
||||
tray_info.wait_to_start()
|
||||
file_state = tray_info.get_file_state()
|
||||
if file_state == TrayState.RUNNING:
|
||||
print("Tray started. Exiting.")
|
||||
return
|
||||
|
||||
if state == TrayState.STARTING:
|
||||
if file_state == TrayState.STARTING:
|
||||
print(
|
||||
"Tray did not start in expected time."
|
||||
" Killing the process and starting new."
|
||||
)
|
||||
file_info = get_tray_file_info() or {}
|
||||
pid = file_info.get("pid")
|
||||
pid = tray_info.pid
|
||||
if pid is not None:
|
||||
_kill_tray_process(pid)
|
||||
remove_tray_server_url(force=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue