diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index e97b8f1c5a..0a9bb2aa9c 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -43,7 +43,8 @@ class AliasedGroup(click.Group): help="Enable debug") @click.option("--verbose", expose_value=False, help=("Change AYON log level (debug - critical or 0-50)")) -def main_cli(ctx): +@click.option("--force", is_flag=True, hidden=True) +def main_cli(ctx, force): """AYON is main command serving as entry point to pipeline system. It wraps different commands together. @@ -55,17 +56,24 @@ def main_cli(ctx): print(ctx.get_help()) sys.exit(0) else: - ctx.invoke(tray) + ctx.forward(tray) @main_cli.command() -def tray(): +@click.option( + "--force", + is_flag=True, + help="Force to start tray and close any existing one.") +def tray(force): """Launch AYON tray. Default action of AYON command is to launch tray widget to control basic aspects of AYON. See documentation for more information. """ - Commands.launch_tray() + + from ayon_core.tools.tray import main + + main(force) @main_cli.group(help="Run command line arguments of AYON addons") diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 9d871c54b1..8ae1ebb3ba 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -13,12 +13,6 @@ class Commands: Most of its methods are called by :mod:`cli` module. """ - @staticmethod - def launch_tray(): - from ayon_core.tools.tray import main - - main() - @staticmethod def publish( path: str, diff --git a/client/ayon_core/tools/stdout_broker/broker.py b/client/ayon_core/tools/stdout_broker/broker.py index 4f7118e2a8..c449fa7df9 100644 --- a/client/ayon_core/tools/stdout_broker/broker.py +++ b/client/ayon_core/tools/stdout_broker/broker.py @@ -8,7 +8,7 @@ from datetime import datetime import websocket from ayon_core.lib import Logger -from ayon_core.tools.tray.webserver import HostMsgAction +from ayon_core.tools.tray import HostMsgAction log = Logger.get_logger(__name__) diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index 9dbacc54c2..2e179f0620 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,21 +1,21 @@ -from .webserver import HostMsgAction -from .addons_manager import TrayAddonsManager +from .structures import HostMsgAction from .lib import ( TrayState, get_tray_state, is_tray_running, get_tray_server_url, + make_sure_tray_is_running, main, ) __all__ = ( "HostMsgAction", - "TrayAddonsManager", "TrayState", "get_tray_state", "is_tray_running", "get_tray_server_url", + "make_sure_tray_is_running", "main", ) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e13c682ab0..c26f4835b1 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -7,12 +7,13 @@ import subprocess import csv import time import signal +import locale from typing import Optional, Dict, Tuple, Any import ayon_api import requests -from ayon_core.lib import Logger +from ayon_core.lib import Logger, get_ayon_launcher_args, run_detached_process from ayon_core.lib.local_settings import get_ayon_appdirs @@ -50,7 +51,8 @@ def _get_server_and_variant( def _windows_pid_is_running(pid: int) -> bool: args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"] output = subprocess.check_output(args) - csv_content = csv.DictReader(output.decode("utf-8").splitlines()) + encoding = locale.getpreferredencoding() + csv_content = csv.DictReader(output.decode(encoding).splitlines()) # if "PID" not in csv_content.fieldnames: # return False for _ in csv_content: @@ -122,6 +124,11 @@ def _wait_for_starting_tray( if data.get("started") is True: return data + pid = data.get("pid") + if pid and not _is_process_running(pid): + remove_tray_server_url() + return None + if time.time() - started_at > timeout: return None time.sleep(0.1) @@ -274,7 +281,12 @@ def remove_tray_server_url(force: Optional[bool] = False): except BaseException: data = {} - if force or not data or data.get("pid") == os.getpid(): + if ( + force + or not data + or data.get("pid") == os.getpid() + or not _is_process_running(data.get("pid")) + ): os.remove(filepath) @@ -344,12 +356,61 @@ def is_tray_running( return state != TrayState.NOT_RUNNING -def main(): +def make_sure_tray_is_running( + ayon_url: Optional[str] = None, + variant: Optional[str] = None, + env: Optional[Dict[str, str]] = None +): + """Make sure that tray for AYON url and variant is running. + + Args: + ayon_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + env (Optional[Dict[str, str]]): Environment variables for the process. + + """ + state = get_tray_state(ayon_url, variant) + if state == TrayState.RUNNING: + return + + if state == TrayState.STARTING: + _wait_for_starting_tray(ayon_url, variant) + state = get_tray_state(ayon_url, variant) + if state == TrayState.RUNNING: + return + + args = get_ayon_launcher_args("tray", "--force") + if env is None: + env = os.environ.copy() + + # Make sure 'QT_API' is not set + env.pop("QT_API", None) + + if ayon_url: + env["AYON_SERVER_URL"] = ayon_url + + # TODO maybe handle variant in a better way + if variant: + if variant == "staging": + args.append("--use-staging") + + run_detached_process(args, env=env) + + +def main(force=False): from ayon_core.tools.tray.ui import main Logger.set_process_name("Tray") state = get_tray_state() + if force and state in (TrayState.RUNNING, TrayState.STARTING): + file_info = get_tray_file_info() or {} + pid = file_info.get("pid") + if pid is not None: + _kill_tray_process(pid) + remove_tray_server_url(force=True) + state = TrayState.NOT_RUNNING + if state == TrayState.RUNNING: print("Tray is already running.") return diff --git a/client/ayon_core/tools/tray/webserver/structures.py b/client/ayon_core/tools/tray/structures.py similarity index 100% rename from client/ayon_core/tools/tray/webserver/structures.py rename to client/ayon_core/tools/tray/structures.py diff --git a/client/ayon_core/tools/tray/addons_manager.py b/client/ayon_core/tools/tray/ui/addons_manager.py similarity index 100% rename from client/ayon_core/tools/tray/addons_manager.py rename to client/ayon_core/tools/tray/ui/addons_manager.py diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 660c61ac94..60c0a63cdb 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -3,12 +3,11 @@ import sys import time import collections import atexit -import json import platform -from aiohttp.web_response import Response import ayon_api from qtpy import QtCore, QtGui, QtWidgets +from aiohttp.web import Response, json_response, Request from ayon_core import resources, style from ayon_core.lib import ( @@ -28,13 +27,13 @@ from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, ) -from ayon_core.tools.tray import TrayAddonsManager from ayon_core.tools.tray.lib import ( set_tray_server_url, remove_tray_server_url, TrayIsRunningError, ) +from .addons_manager import TrayAddonsManager from .host_console_listener import HostListener from .info_widget import InfoWidget from .dialogs import ( @@ -91,6 +90,10 @@ class TrayManager: self._services_submenu = None self._start_time = time.time() + # Cache AYON username used in process + # - it can change only by changing ayon_api global connection + # should be safe for tray application to cache the value only once + self._cached_username = None self._closing = False try: set_tray_server_url( @@ -143,7 +146,7 @@ class TrayManager: self._addons_manager.initialize(tray_menu) self._addons_manager.add_route( - "GET", "/tray", self._get_web_tray_info + "GET", "/tray", self._web_get_tray_info ) admin_submenu = ITrayAction.admin_submenu(tray_menu) @@ -274,8 +277,12 @@ class TrayManager: return item - async def _get_web_tray_info(self, request): - return Response(text=json.dumps({ + async def _web_get_tray_info(self, _request: Request) -> Response: + if self._cached_username is None: + self._cached_username = ayon_api.get_user()["name"] + + return json_response({ + "username": self._cached_username, "bundle": os.getenv("AYON_BUNDLE_NAME"), "dev_mode": is_dev_mode_enabled(), "staging_mode": is_staging_enabled(), @@ -285,7 +292,7 @@ class TrayManager: }, "installer_version": os.getenv("AYON_VERSION"), "running_time": time.time() - self._start_time, - })) + }) def _on_update_check_timer(self): try: diff --git a/client/ayon_core/tools/tray/webserver/__init__.py b/client/ayon_core/tools/tray/webserver/__init__.py index 93bfbd6aee..c40b5b85c3 100644 --- a/client/ayon_core/tools/tray/webserver/__init__.py +++ b/client/ayon_core/tools/tray/webserver/__init__.py @@ -1,10 +1,8 @@ -from .structures import HostMsgAction from .base_routes import RestApiEndpoint from .server import find_free_port, WebServerManager __all__ = ( - "HostMsgAction", "RestApiEndpoint", "find_free_port", "WebServerManager", diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 36bb3f7340..1ca487969f 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -964,7 +964,8 @@ DEFAULT_PUBLISH_VALUES = { "nuke", "harmony", "photoshop", - "aftereffects" + "aftereffects", + "fusion" ], "enabled": True, "optional": True,