tray is somewhat capable of hangling single tray running

This commit is contained in:
Jakub Trllo 2024-07-18 19:02:46 +02:00
parent 996998d53c
commit 5498bccf85
9 changed files with 254 additions and 33 deletions

View file

@ -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()

View file

@ -1,6 +1,11 @@
from .addons_manager import TrayAddonsManager
from .lib import (
is_tray_running,
main,
)
__all__ = (
"TrayAddonsManager",
"main",
)

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -1,8 +1,10 @@
from .structures import HostMsgAction
from .base_routes import RestApiEndpoint
from .webserver import TrayWebserver
__all__ = (
"HostMsgAction",
"RestApiEndpoint",
"TrayWebserver",
)

View file

@ -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

View file

@ -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()

View file

@ -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):