mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into feature/integrate-reviewables-to-ayon
This commit is contained in:
commit
cb3d002ac0
37 changed files with 1539 additions and 777 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -237,10 +235,10 @@ def _handle_moved_addons(addon_name, milestone_version, log):
|
|||
"client",
|
||||
)
|
||||
if not os.path.exists(addon_dir):
|
||||
log.error((
|
||||
"Addon '{}' is not be available."
|
||||
" Please update applications addon to '{}' or higher."
|
||||
).format(addon_name, milestone_version))
|
||||
log.error(
|
||||
f"Addon '{addon_name}' is not available. Please update "
|
||||
f"{addon_name} addon to '{milestone_version}' or higher."
|
||||
)
|
||||
return None
|
||||
|
||||
log.warning((
|
||||
|
|
@ -923,20 +921,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 +943,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:
|
||||
|
|
@ -1338,185 +1336,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()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import sys
|
|||
import code
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
import click
|
||||
import acre
|
||||
|
|
@ -12,9 +13,12 @@ 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
|
||||
|
||||
|
||||
class AliasedGroup(click.Group):
|
||||
|
|
@ -39,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.
|
||||
|
|
@ -51,20 +56,26 @@ 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)
|
||||
|
||||
|
||||
@Commands.add_addons
|
||||
@main_cli.group(help="Run command line arguments of AYON addons")
|
||||
@click.pass_context
|
||||
def addon(ctx):
|
||||
|
|
@ -80,6 +91,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)
|
||||
|
|
@ -88,7 +100,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.
|
||||
|
|
@ -102,24 +116,42 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup):
|
|||
This function is deprecated and will be removed in future. Please use
|
||||
'addon applications extractenvironments ...' instead.
|
||||
"""
|
||||
Commands.extractenvironments(
|
||||
warnings.warn(
|
||||
(
|
||||
"Command 'extractenvironments' is deprecated and will be"
|
||||
" removed in future. Please use"
|
||||
" 'addon applications extractenvironments ...' instead."
|
||||
),
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
addons_manager = ctx.obj["addons_manager"]
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
if applications_addon is None:
|
||||
raise RuntimeError(
|
||||
"Applications addon is not available or enabled."
|
||||
)
|
||||
|
||||
# Please ignore the fact this is using private method
|
||||
applications_addon._cli_extract_environments(
|
||||
output_json_path, project, asset, task, app, envgroup
|
||||
)
|
||||
|
||||
|
||||
@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):
|
||||
"""Start CLI publishing.
|
||||
|
||||
Publish collects json from path provided as an argument.
|
||||
S
|
||||
|
||||
"""
|
||||
Commands.publish(path, targets, gui)
|
||||
from ayon_core.pipeline.publish import main_cli_publish
|
||||
|
||||
main_cli_publish(path, targets, ctx.obj["addons_manager"])
|
||||
|
||||
|
||||
@main_cli.command(context_settings={"ignore_unknown_options": True})
|
||||
|
|
@ -149,12 +181,10 @@ def contextselection(
|
|||
Context is project name, folder path and task name. The result is stored
|
||||
into json file which path is passed in first argument.
|
||||
"""
|
||||
Commands.contextselection(
|
||||
output_path,
|
||||
project,
|
||||
folder,
|
||||
strict
|
||||
)
|
||||
from ayon_core.tools.context_dialog import main
|
||||
|
||||
main(output_path, project, folder, strict)
|
||||
|
||||
|
||||
|
||||
@main_cli.command(
|
||||
|
|
@ -245,11 +275,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 +286,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-AddAddons")
|
||||
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,10 +324,14 @@ 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")
|
||||
main_cli(
|
||||
prog_name="ayon",
|
||||
obj={"addons_manager": addons_manager},
|
||||
)
|
||||
except Exception: # noqa
|
||||
exc_info = sys.exc_info()
|
||||
print("!!! AYON crashed:")
|
||||
|
|
|
|||
|
|
@ -1,195 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Implementation of AYON commands."""
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
|
||||
class Commands:
|
||||
"""Class implementing commands used by AYON.
|
||||
|
||||
Most of its methods are called by :mod:`cli` module.
|
||||
"""
|
||||
@staticmethod
|
||||
def launch_tray():
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.tools import tray
|
||||
|
||||
Logger.set_process_name("Tray")
|
||||
|
||||
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.
|
||||
|
||||
Publish use json from passed path argument.
|
||||
|
||||
Args:
|
||||
path (str): Path to JSON.
|
||||
targets (list of str): List of pyblish targets.
|
||||
gui (bool): Show publish UI.
|
||||
|
||||
Raises:
|
||||
RuntimeError: When there is no path to process.
|
||||
RuntimeError: When executed with list of JSON paths.
|
||||
|
||||
"""
|
||||
from ayon_core.lib import Logger
|
||||
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline import (
|
||||
install_ayon_plugins,
|
||||
get_global_context,
|
||||
)
|
||||
|
||||
import ayon_api
|
||||
import pyblish.util
|
||||
|
||||
# Register target and host
|
||||
if not isinstance(path, str):
|
||||
raise RuntimeError("Path to JSON must be a string.")
|
||||
|
||||
# Fix older jobs
|
||||
for src_key, dst_key in (
|
||||
("AVALON_PROJECT", "AYON_PROJECT_NAME"),
|
||||
("AVALON_ASSET", "AYON_FOLDER_PATH"),
|
||||
("AVALON_TASK", "AYON_TASK_NAME"),
|
||||
("AVALON_WORKDIR", "AYON_WORKDIR"),
|
||||
("AVALON_APP_NAME", "AYON_APP_NAME"),
|
||||
("AVALON_APP", "AYON_HOST_NAME"),
|
||||
):
|
||||
if src_key in os.environ and dst_key not in os.environ:
|
||||
os.environ[dst_key] = os.environ[src_key]
|
||||
# Remove old keys, so we're sure they're not used
|
||||
os.environ.pop(src_key, None)
|
||||
|
||||
log = Logger.get_logger("CLI-publish")
|
||||
|
||||
# Make public ayon api behave as other user
|
||||
# - this works only if public ayon api is using service user
|
||||
username = os.environ.get("AYON_USERNAME")
|
||||
if username:
|
||||
# NOTE: ayon-python-api does not have public api function to find
|
||||
# out if is used service user. So we need to have try > except
|
||||
# block.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
try:
|
||||
con.set_default_service_username(username)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
install_ayon_plugins()
|
||||
|
||||
manager = AddonsManager()
|
||||
|
||||
publish_paths = 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")
|
||||
if applications_addon is not None:
|
||||
context = get_global_context()
|
||||
env = applications_addon.get_farm_publish_environment_variables(
|
||||
context["project_name"],
|
||||
context["folder_path"],
|
||||
context["task_name"],
|
||||
)
|
||||
os.environ.update(env)
|
||||
|
||||
pyblish.api.register_host("shell")
|
||||
|
||||
if targets:
|
||||
for target in targets:
|
||||
print(f"setting target: {target}")
|
||||
pyblish.api.register_target(target)
|
||||
else:
|
||||
pyblish.api.register_target("farm")
|
||||
|
||||
os.environ["AYON_PUBLISH_DATA"] = path
|
||||
os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
|
||||
|
||||
log.info("Running publish ...")
|
||||
|
||||
plugins = pyblish.api.discover()
|
||||
print("Using plugins:")
|
||||
for plugin in plugins:
|
||||
print(plugin)
|
||||
|
||||
if gui:
|
||||
from ayon_core.tools.utils.host_tools import show_publish
|
||||
from ayon_core.tools.utils.lib import qt_app_context
|
||||
with qt_app_context():
|
||||
show_publish()
|
||||
else:
|
||||
# Error exit as soon as any error occurs.
|
||||
error_format = ("Failed {plugin.__name__}: "
|
||||
"{error} -- {error.traceback}")
|
||||
|
||||
for result in pyblish.util.publish_iter():
|
||||
if result["error"]:
|
||||
log.error(error_format.format(**result))
|
||||
# uninstall()
|
||||
sys.exit(1)
|
||||
|
||||
log.info("Publish finished.")
|
||||
|
||||
@staticmethod
|
||||
def extractenvironments(
|
||||
output_json_path, project, asset, task, app, env_group
|
||||
):
|
||||
"""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"
|
||||
" removed in future. Please use "
|
||||
"'addon applications extractenvironments ...' instead."
|
||||
),
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
addons_manager = AddonsManager()
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
if applications_addon is None:
|
||||
raise RuntimeError(
|
||||
"Applications addon is not available or enabled."
|
||||
)
|
||||
|
||||
# Please ignore the fact this is using private method
|
||||
applications_addon._cli_extract_environments(
|
||||
output_json_path, project, asset, task, app, env_group
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def contextselection(output_path, project_name, folder_path, strict):
|
||||
from ayon_core.tools.context_dialog import main
|
||||
|
||||
main(output_path, project_name, folder_path, strict)
|
||||
72
client/ayon_core/hooks/pre_filter_farm_environments.py
Normal file
72
client/ayon_core/hooks/pre_filter_farm_environments.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import re
|
||||
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.lib import filter_profiles
|
||||
|
||||
|
||||
class FilterFarmEnvironments(PreLaunchHook):
|
||||
"""Filter or modify calculated environment variables for farm rendering.
|
||||
|
||||
This hook must run last, only after all other hooks are finished to get
|
||||
correct environment for launch context.
|
||||
|
||||
Implemented modifications to self.launch_context.env:
|
||||
- skipping (list) of environment variable keys
|
||||
- removing value in environment variable:
|
||||
- supports regular expression in pattern
|
||||
"""
|
||||
order = 1000
|
||||
|
||||
launch_types = {LaunchTypes.farm_publish}
|
||||
|
||||
def execute(self):
|
||||
data = self.launch_context.data
|
||||
project_settings = data["project_settings"]
|
||||
filter_env_profiles = (
|
||||
project_settings["core"]["filter_env_profiles"])
|
||||
|
||||
if not filter_env_profiles:
|
||||
self.log.debug("No profiles found for env var filtering")
|
||||
return
|
||||
|
||||
task_entity = data["task_entity"]
|
||||
|
||||
filter_data = {
|
||||
"host_names": self.host_name,
|
||||
"task_types": task_entity["taskType"],
|
||||
"task_names": task_entity["name"],
|
||||
"folder_paths": data["folder_path"]
|
||||
}
|
||||
matching_profile = filter_profiles(
|
||||
filter_env_profiles, filter_data, logger=self.log
|
||||
)
|
||||
if not matching_profile:
|
||||
self.log.debug("No matching profile found for env var filtering "
|
||||
f"for {filter_data}")
|
||||
return
|
||||
|
||||
self._skip_environment_variables(
|
||||
self.launch_context.env, matching_profile)
|
||||
|
||||
self._modify_environment_variables(
|
||||
self.launch_context.env, matching_profile)
|
||||
|
||||
def _modify_environment_variables(self, calculated_env, matching_profile):
|
||||
"""Modify environment variable values."""
|
||||
for env_item in matching_profile["replace_in_environment"]:
|
||||
key = env_item["environment_key"]
|
||||
value = calculated_env.get(key)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
value = re.sub(value, env_item["pattern"], env_item["replacement"])
|
||||
if value:
|
||||
calculated_env[key] = value
|
||||
else:
|
||||
calculated_env.pop(key)
|
||||
|
||||
def _skip_environment_variables(self, calculated_env, matching_profile):
|
||||
"""Skips list of environment variable names"""
|
||||
for skip_env in matching_profile["skip_env_keys"]:
|
||||
self.log.info(f"Skipping {skip_env}")
|
||||
calculated_env.pop(skip_env)
|
||||
|
|
@ -17,7 +17,6 @@ from .base import (
|
|||
load_modules,
|
||||
|
||||
ModulesManager,
|
||||
TrayModulesManager,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -38,5 +37,4 @@ __all__ = (
|
|||
"load_modules",
|
||||
|
||||
"ModulesManager",
|
||||
"TrayModulesManager",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
from .version import __version__
|
||||
from .structures import HostMsgAction
|
||||
from .webserver_module import (
|
||||
WebServerAddon
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"HostMsgAction",
|
||||
"WebServerAddon",
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "1.0.0"
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
"""WebServerAddon 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.
|
||||
|
||||
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 .version import __version__
|
||||
|
||||
|
||||
class WebServerAddon(AYONAddon, ITrayService):
|
||||
name = "webserver"
|
||||
version = __version__
|
||||
label = "WebServer"
|
||||
|
||||
webserver_url_env = "AYON_WEBSERVER_URL"
|
||||
|
||||
def initialize(self, settings):
|
||||
self._server_manager = None
|
||||
self._host_listener = None
|
||||
|
||||
self._port = self.find_free_port()
|
||||
self._webserver_url = None
|
||||
|
||||
@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_modules):
|
||||
if not self._server_manager:
|
||||
return
|
||||
|
||||
for module in enabled_modules:
|
||||
if not hasattr(module, "webserver_initialization"):
|
||||
continue
|
||||
|
||||
try:
|
||||
module.webserver_initialization(self._server_manager)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
(
|
||||
"Failed to connect module \"{}\" to webserver."
|
||||
).format(module.name),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def tray_init(self):
|
||||
self.create_server_manager()
|
||||
self._add_resources_statics()
|
||||
self._add_listeners()
|
||||
|
||||
def tray_start(self):
|
||||
self.start_server()
|
||||
|
||||
def tray_exit(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 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
|
||||
):
|
||||
"""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
|
||||
|
||||
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
|
||||
)
|
||||
|
|
@ -42,6 +42,8 @@ from .lib import (
|
|||
get_plugin_settings,
|
||||
get_publish_instance_label,
|
||||
get_publish_instance_families,
|
||||
|
||||
main_cli_publish,
|
||||
)
|
||||
|
||||
from .abstract_expected_files import ExpectedFiles
|
||||
|
|
@ -92,6 +94,8 @@ __all__ = (
|
|||
"get_publish_instance_label",
|
||||
"get_publish_instance_families",
|
||||
|
||||
"main_cli_publish",
|
||||
|
||||
"ExpectedFiles",
|
||||
|
||||
"RenderInstance",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import inspect
|
|||
import copy
|
||||
import tempfile
|
||||
import xml.etree.ElementTree
|
||||
from typing import Optional, Union
|
||||
from typing import Optional, Union, List
|
||||
|
||||
import ayon_api
|
||||
import pyblish.util
|
||||
import pyblish.plugin
|
||||
import pyblish.api
|
||||
|
|
@ -16,6 +17,7 @@ from ayon_core.lib import (
|
|||
filter_profiles,
|
||||
)
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline import (
|
||||
tempdir,
|
||||
Anatomy
|
||||
|
|
@ -978,3 +980,113 @@ def get_instance_expected_output_path(
|
|||
path_template_obj = anatomy.get_template_item("publish", "default")["path"]
|
||||
template_filled = path_template_obj.format_strict(template_data)
|
||||
return os.path.normpath(template_filled)
|
||||
|
||||
|
||||
def main_cli_publish(
|
||||
path: str,
|
||||
targets: Optional[List[str]] = None,
|
||||
addons_manager: Optional[AddonsManager] = None,
|
||||
):
|
||||
"""Start headless publishing.
|
||||
|
||||
Publish use json from passed path argument.
|
||||
|
||||
Args:
|
||||
path (str): Path to JSON.
|
||||
targets (Optional[List[str]]): List of pyblish targets.
|
||||
addons_manager (Optional[AddonsManager]): Addons manager instance.
|
||||
|
||||
Raises:
|
||||
RuntimeError: When there is no path to process or when executed with
|
||||
list of JSON paths.
|
||||
|
||||
"""
|
||||
from ayon_core.pipeline import (
|
||||
install_ayon_plugins,
|
||||
get_global_context,
|
||||
)
|
||||
|
||||
# Register target and host
|
||||
if not isinstance(path, str):
|
||||
raise RuntimeError("Path to JSON must be a string.")
|
||||
|
||||
# Fix older jobs
|
||||
for src_key, dst_key in (
|
||||
("AVALON_PROJECT", "AYON_PROJECT_NAME"),
|
||||
("AVALON_ASSET", "AYON_FOLDER_PATH"),
|
||||
("AVALON_TASK", "AYON_TASK_NAME"),
|
||||
("AVALON_WORKDIR", "AYON_WORKDIR"),
|
||||
("AVALON_APP_NAME", "AYON_APP_NAME"),
|
||||
("AVALON_APP", "AYON_HOST_NAME"),
|
||||
):
|
||||
if src_key in os.environ and dst_key not in os.environ:
|
||||
os.environ[dst_key] = os.environ[src_key]
|
||||
# Remove old keys, so we're sure they're not used
|
||||
os.environ.pop(src_key, None)
|
||||
|
||||
log = Logger.get_logger("CLI-publish")
|
||||
|
||||
# Make public ayon api behave as other user
|
||||
# - this works only if public ayon api is using service user
|
||||
username = os.environ.get("AYON_USERNAME")
|
||||
if username:
|
||||
# NOTE: ayon-python-api does not have public api function to find
|
||||
# out if is used service user. So we need to have try > except
|
||||
# block.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
try:
|
||||
con.set_default_service_username(username)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
install_ayon_plugins()
|
||||
|
||||
if addons_manager is None:
|
||||
addons_manager = AddonsManager()
|
||||
|
||||
# TODO validate if this has to happen
|
||||
# - it should happen during 'install_ayon_plugins'
|
||||
publish_paths = addons_manager.collect_plugin_paths()["publish"]
|
||||
for plugin_path in publish_paths:
|
||||
pyblish.api.register_plugin_path(plugin_path)
|
||||
|
||||
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(
|
||||
context["project_name"],
|
||||
context["folder_path"],
|
||||
context["task_name"],
|
||||
)
|
||||
os.environ.update(env)
|
||||
|
||||
pyblish.api.register_host("shell")
|
||||
|
||||
if targets:
|
||||
for target in targets:
|
||||
print(f"setting target: {target}")
|
||||
pyblish.api.register_target(target)
|
||||
else:
|
||||
pyblish.api.register_target("farm")
|
||||
|
||||
os.environ["AYON_PUBLISH_DATA"] = path
|
||||
os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
|
||||
|
||||
log.info("Running publish ...")
|
||||
|
||||
plugins = pyblish.api.discover()
|
||||
print("Using plugins:")
|
||||
for plugin in plugins:
|
||||
print(plugin)
|
||||
|
||||
# Error exit as soon as any error occurs.
|
||||
error_format = ("Failed {plugin.__name__}: "
|
||||
"{error} -- {error.traceback}")
|
||||
|
||||
for result in pyblish.util.publish_iter():
|
||||
if result["error"]:
|
||||
log.error(error_format.format(**result))
|
||||
# uninstall()
|
||||
sys.exit(1)
|
||||
|
||||
log.info("Publish finished.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
AbstractAttrDef,
|
||||
|
|
@ -13,19 +14,16 @@ class ProductTypeItem:
|
|||
Args:
|
||||
name (str): Product type name.
|
||||
icon (dict[str, Any]): Product type icon definition.
|
||||
checked (bool): Is product type checked for filtering.
|
||||
"""
|
||||
|
||||
def __init__(self, name, icon, checked):
|
||||
def __init__(self, name, icon):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.checked = checked
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"icon": self.icon,
|
||||
"checked": self.checked,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -346,6 +344,16 @@ class ActionItem:
|
|||
return cls(**data)
|
||||
|
||||
|
||||
class ProductTypesFilter:
|
||||
"""Product types filter.
|
||||
|
||||
Defines the filtering for product types.
|
||||
"""
|
||||
def __init__(self, product_types: List[str], is_allow_list: bool):
|
||||
self.product_types: List[str] = product_types
|
||||
self.is_allow_list: bool = is_allow_list
|
||||
|
||||
|
||||
class _BaseLoaderController(ABC):
|
||||
"""Base loader controller abstraction.
|
||||
|
||||
|
|
@ -1006,3 +1014,13 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_types_filter(self):
|
||||
"""Return product type filter for current context.
|
||||
|
||||
Returns:
|
||||
ProductTypesFilter: Product type filter for current context
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import uuid
|
|||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib import NestedCacheItem, CacheItem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import get_current_host_name
|
||||
from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.pipeline import Anatomy, get_current_context
|
||||
from ayon_core.host import ILoadHost
|
||||
|
|
@ -13,7 +15,11 @@ from ayon_core.tools.common_models import (
|
|||
ThumbnailsModel,
|
||||
)
|
||||
|
||||
from .abstract import BackendLoaderController, FrontendLoaderController
|
||||
from .abstract import (
|
||||
BackendLoaderController,
|
||||
FrontendLoaderController,
|
||||
ProductTypesFilter
|
||||
)
|
||||
from .models import (
|
||||
SelectionModel,
|
||||
ProductsModel,
|
||||
|
|
@ -331,11 +337,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
project_name = context.get("project_name")
|
||||
folder_path = context.get("folder_path")
|
||||
if project_name and folder_path:
|
||||
folder = ayon_api.get_folder_by_path(
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path, fields=["id"]
|
||||
)
|
||||
if folder:
|
||||
folder_id = folder["id"]
|
||||
if folder_entity:
|
||||
folder_id = folder_entity["id"]
|
||||
return {
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
|
|
@ -425,3 +431,59 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self._event_system.emit(topic, data or {}, "controller")
|
||||
|
||||
def get_product_types_filter(self):
|
||||
output = ProductTypesFilter(
|
||||
is_allow_list=False,
|
||||
product_types=[]
|
||||
)
|
||||
# Without host is not determined context
|
||||
if self._host is None:
|
||||
return output
|
||||
|
||||
context = self.get_current_context()
|
||||
project_name = context.get("project_name")
|
||||
if not project_name:
|
||||
return output
|
||||
settings = get_project_settings(project_name)
|
||||
profiles = (
|
||||
settings
|
||||
["core"]
|
||||
["tools"]
|
||||
["loader"]
|
||||
["product_type_filter_profiles"]
|
||||
)
|
||||
if not profiles:
|
||||
return output
|
||||
|
||||
folder_id = context.get("folder_id")
|
||||
task_name = context.get("task_name")
|
||||
task_type = None
|
||||
if folder_id and task_name:
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name,
|
||||
folder_id,
|
||||
task_name,
|
||||
fields={"taskType"}
|
||||
)
|
||||
if task_entity:
|
||||
task_type = task_entity.get("taskType")
|
||||
|
||||
host_name = getattr(self._host, "name", get_current_host_name())
|
||||
profile = filter_profiles(
|
||||
profiles,
|
||||
{
|
||||
"hosts": host_name,
|
||||
"task_types": task_type,
|
||||
}
|
||||
)
|
||||
if profile:
|
||||
# TODO remove 'is_include' after release '0.4.3'
|
||||
is_allow_list = profile.get("is_include")
|
||||
if is_allow_list is None:
|
||||
is_allow_list = profile["filter_type"] == "is_allow_list"
|
||||
output = ProductTypesFilter(
|
||||
is_allow_list=is_allow_list,
|
||||
product_types=profile["filter_product_types"]
|
||||
)
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ def product_type_item_from_data(product_type_data):
|
|||
"color": "#0091B2",
|
||||
}
|
||||
# TODO implement checked logic
|
||||
return ProductTypeItem(product_type_data["name"], icon, True)
|
||||
return ProductTypeItem(product_type_data["name"], icon)
|
||||
|
||||
|
||||
def create_default_product_type_item(product_type):
|
||||
|
|
@ -132,7 +132,7 @@ def create_default_product_type_item(product_type):
|
|||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
return ProductTypeItem(product_type, icon, True)
|
||||
return ProductTypeItem(product_type, icon)
|
||||
|
||||
|
||||
class ProductsModel:
|
||||
|
|
|
|||
|
|
@ -13,10 +13,17 @@ class ProductTypesQtModel(QtGui.QStandardItemModel):
|
|||
super(ProductTypesQtModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._reset_filters_on_refresh = True
|
||||
self._refreshing = False
|
||||
self._bulk_change = False
|
||||
self._last_project = None
|
||||
self._items_by_name = {}
|
||||
|
||||
controller.register_event_callback(
|
||||
"controller.reset.finished",
|
||||
self._on_controller_reset_finish,
|
||||
)
|
||||
|
||||
def is_refreshing(self):
|
||||
return self._refreshing
|
||||
|
||||
|
|
@ -37,14 +44,19 @@ class ProductTypesQtModel(QtGui.QStandardItemModel):
|
|||
self._refreshing = True
|
||||
product_type_items = self._controller.get_product_type_items(
|
||||
project_name)
|
||||
self._last_project = project_name
|
||||
|
||||
items_to_remove = set(self._items_by_name.keys())
|
||||
new_items = []
|
||||
items_filter_required = {}
|
||||
for product_type_item in product_type_items:
|
||||
name = product_type_item.name
|
||||
items_to_remove.discard(name)
|
||||
item = self._items_by_name.get(product_type_item.name)
|
||||
item = self._items_by_name.get(name)
|
||||
# Apply filter to new items or if filters reset is requested
|
||||
filter_required = self._reset_filters_on_refresh
|
||||
if item is None:
|
||||
filter_required = True
|
||||
item = QtGui.QStandardItem(name)
|
||||
item.setData(name, PRODUCT_TYPE_ROLE)
|
||||
item.setEditable(False)
|
||||
|
|
@ -52,14 +64,26 @@ class ProductTypesQtModel(QtGui.QStandardItemModel):
|
|||
new_items.append(item)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
item.setCheckState(
|
||||
QtCore.Qt.Checked
|
||||
if product_type_item.checked
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
if filter_required:
|
||||
items_filter_required[name] = item
|
||||
|
||||
icon = get_qt_icon(product_type_item.icon)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
if items_filter_required:
|
||||
product_types_filter = self._controller.get_product_types_filter()
|
||||
for product_type, item in items_filter_required.items():
|
||||
matching = (
|
||||
int(product_type in product_types_filter.product_types)
|
||||
+ int(product_types_filter.is_allow_list)
|
||||
)
|
||||
state = (
|
||||
QtCore.Qt.Checked
|
||||
if matching % 2 == 0
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
item.setCheckState(state)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
|
@ -68,9 +92,13 @@ class ProductTypesQtModel(QtGui.QStandardItemModel):
|
|||
item = self._items_by_name.pop(name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
self._reset_filters_on_refresh = False
|
||||
self._refreshing = False
|
||||
self.refreshed.emit()
|
||||
|
||||
def reset_product_types_filter_on_refresh(self):
|
||||
self._reset_filters_on_refresh = True
|
||||
|
||||
def setData(self, index, value, role=None):
|
||||
checkstate_changed = False
|
||||
if role is None:
|
||||
|
|
@ -122,6 +150,9 @@ class ProductTypesQtModel(QtGui.QStandardItemModel):
|
|||
if changed:
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _on_controller_reset_finish(self):
|
||||
self.refresh(self._last_project)
|
||||
|
||||
|
||||
class ProductTypesView(QtWidgets.QListView):
|
||||
filter_changed = QtCore.Signal()
|
||||
|
|
@ -151,6 +182,7 @@ class ProductTypesView(QtWidgets.QListView):
|
|||
)
|
||||
|
||||
self._controller = controller
|
||||
self._refresh_product_types_filter = False
|
||||
|
||||
self._product_types_model = product_types_model
|
||||
self._product_types_proxy_model = product_types_proxy_model
|
||||
|
|
@ -158,11 +190,15 @@ class ProductTypesView(QtWidgets.QListView):
|
|||
def get_filter_info(self):
|
||||
return self._product_types_model.get_filter_info()
|
||||
|
||||
def reset_product_types_filter_on_refresh(self):
|
||||
self._product_types_model.reset_product_types_filter_on_refresh()
|
||||
|
||||
def _on_project_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._product_types_model.refresh(project_name)
|
||||
|
||||
def _on_refresh_finished(self):
|
||||
# Apply product types filter on first show
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _on_filter_change(self):
|
||||
|
|
|
|||
|
|
@ -345,6 +345,8 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
def closeEvent(self, event):
|
||||
super(LoaderWindow, self).closeEvent(event)
|
||||
|
||||
self._product_types_widget.reset_product_types_filter_on_refresh()
|
||||
|
||||
self._reset_on_show = True
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
|
|
|
|||
|
|
@ -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 import HostMsgAction
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
from .tray import main
|
||||
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",
|
||||
|
||||
"TrayState",
|
||||
"get_tray_state",
|
||||
"is_tray_running",
|
||||
"get_tray_server_url",
|
||||
"make_sure_tray_is_running",
|
||||
"main",
|
||||
)
|
||||
|
|
|
|||
482
client/ayon_core/tools/tray/lib.py
Normal file
482
client/ayon_core/tools/tray/lib.py
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import platform
|
||||
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, get_ayon_launcher_args, run_detached_process
|
||||
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:
|
||||
"""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()
|
||||
|
||||
|
||||
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 _windows_pid_is_running(pid: int) -> bool:
|
||||
args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"]
|
||||
output = subprocess.check_output(args)
|
||||
encoding = locale.getpreferredencoding()
|
||||
csv_content = csv.DictReader(output.decode(encoding).splitlines())
|
||||
# if "PID" not in csv_content.fieldnames:
|
||||
# return False
|
||||
for _ in csv_content:
|
||||
return True
|
||||
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.
|
||||
|
||||
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 _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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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]]:
|
||||
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
|
||||
) -> 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]]:
|
||||
"""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
|
||||
try:
|
||||
with open(filepath, "r") as stream:
|
||||
data = json.load(stream)
|
||||
except Exception:
|
||||
return None
|
||||
return data
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
timeout (Optional[int]): Timeout for tray start-up.
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
|
||||
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 (Optional[str]): Webserver url with port.
|
||||
started (bool): If tray is started. When set to 'False' it means
|
||||
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.")
|
||||
|
||||
filepath = _get_tray_info_filepath()
|
||||
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(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
|
||||
|
||||
try:
|
||||
with open(filepath, "r") as stream:
|
||||
data = json.load(stream)
|
||||
except BaseException:
|
||||
data = {}
|
||||
|
||||
if (
|
||||
force
|
||||
or not data
|
||||
or data.get("pid") == os.getpid()
|
||||
or not _is_process_running(data.get("pid"))
|
||||
):
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def show_message_in_tray(
|
||||
title, message, icon=None, msecs=None, tray_url=None
|
||||
):
|
||||
"""Show message in tray.
|
||||
|
||||
Args:
|
||||
title (str): Message title.
|
||||
message (str): Message content.
|
||||
icon (Optional[Literal["information", "warning", "critical"]]): Icon
|
||||
for the message.
|
||||
msecs (Optional[int]): Duration of the message.
|
||||
tray_url (Optional[str]): Tray server url.
|
||||
|
||||
"""
|
||||
if not tray_url:
|
||||
tray_url = get_tray_server_url()
|
||||
|
||||
# TODO handle this case, e.g. raise an error?
|
||||
if not tray_url:
|
||||
return
|
||||
|
||||
# TODO handle response, can fail whole request or can fail on status
|
||||
requests.post(
|
||||
f"{tray_url}/tray/message",
|
||||
json={
|
||||
"title": title,
|
||||
"message": message,
|
||||
"icon": icon,
|
||||
"msecs": msecs
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
show_message_in_tray(
|
||||
"Tray is already running",
|
||||
"Your AYON tray application is already running."
|
||||
)
|
||||
print("Tray is already running.")
|
||||
return
|
||||
|
||||
if state == TrayState.STARTING:
|
||||
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:
|
||||
_kill_tray_process(pid)
|
||||
remove_tray_server_url(force=True)
|
||||
|
||||
# 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()
|
||||
|
||||
6
client/ayon_core/tools/tray/ui/__init__.py
Normal file
6
client/ayon_core/tools/tray/ui/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .tray import main
|
||||
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
247
client/ayon_core/tools/tray/ui/addons_manager.py
Normal file
247
client/ayon_core/tools/tray/ui/addons_manager.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import os
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from ayon_core.addon import AddonsManager, ITrayAddon, ITrayService
|
||||
from ayon_core.tools.tray.webserver import (
|
||||
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 = (
|
||||
"ftrack",
|
||||
"kitsu",
|
||||
"launcher_tool",
|
||||
"clockify",
|
||||
)
|
||||
|
||||
def __init__(self, tray_manager):
|
||||
super().__init__(initialize=False)
|
||||
|
||||
self._tray_manager = tray_manager
|
||||
|
||||
self._webserver_manager = WebServerManager(find_free_port(), None)
|
||||
|
||||
self.doubleclick_callbacks = {}
|
||||
self.doubleclick_callback = None
|
||||
|
||||
@property
|
||||
def webserver_url(self):
|
||||
return self._webserver_manager.url
|
||||
|
||||
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.
|
||||
|
||||
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_menu):
|
||||
self.initialize_addons()
|
||||
self.tray_init()
|
||||
self.connect_addons()
|
||||
self.tray_menu(tray_menu)
|
||||
|
||||
def add_route(self, request_method: str, path: str, handler: Callable):
|
||||
self._webserver_manager.add_route(request_method, path, handler)
|
||||
|
||||
def add_static(self, prefix: str, path: str):
|
||||
self._webserver_manager.add_static(prefix, path)
|
||||
|
||||
def add_addon_route(
|
||||
self,
|
||||
addon_name: str,
|
||||
path: str,
|
||||
request_method: str,
|
||||
handler: Callable
|
||||
) -> str:
|
||||
return self._webserver_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._webserver_manager.add_addon_static(
|
||||
addon_name,
|
||||
prefix,
|
||||
path
|
||||
)
|
||||
|
||||
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):
|
||||
self._init_tray_webserver()
|
||||
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 connect_addons(self):
|
||||
self._webserver_manager.connect_with_addons(
|
||||
self.get_enabled_addons()
|
||||
)
|
||||
super().connect_addons()
|
||||
|
||||
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):
|
||||
self._webserver_manager.start_server()
|
||||
|
||||
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):
|
||||
self._webserver_manager.stop_server()
|
||||
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
|
||||
)
|
||||
|
||||
def get_tray_webserver(self):
|
||||
# TODO rename/remove method
|
||||
return self._webserver_manager
|
||||
|
||||
def _init_tray_webserver(self):
|
||||
webserver_url = self.webserver_url
|
||||
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"] = statics_url
|
||||
|
||||
# Deprecated
|
||||
os.environ["OPENPYPE_WEBSERVER_URL"] = webserver_url
|
||||
os.environ["OPENPYPE_STATICS_SERVER"] = statics_url
|
||||
|
|
@ -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__)
|
||||
|
||||
|
|
@ -22,18 +22,19 @@ class IconType:
|
|||
|
||||
|
||||
class HostListener:
|
||||
def __init__(self, webserver, module):
|
||||
self._window_per_id = {}
|
||||
self.module = module
|
||||
self.webserver = webserver
|
||||
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. """
|
||||
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)
|
||||
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
|
@ -1,12 +1,13 @@
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
import collections
|
||||
import atexit
|
||||
|
||||
import platform
|
||||
|
||||
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 (
|
||||
|
|
@ -21,13 +22,19 @@ 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.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 (
|
||||
UpdateDialog,
|
||||
|
|
@ -54,25 +61,55 @@ class TrayManager:
|
|||
)
|
||||
if update_check_interval is None:
|
||||
update_check_interval = 5
|
||||
self._update_check_interval = update_check_interval * 60 * 1000
|
||||
|
||||
self._addons_manager = TrayAddonsManager()
|
||||
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._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(
|
||||
self._addons_manager.webserver_url, False
|
||||
)
|
||||
except TrayIsRunningError:
|
||||
self.log.error("Tray is already running.")
|
||||
self._closing = True
|
||||
|
||||
def is_closing(self):
|
||||
return self._closing
|
||||
|
||||
@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."""
|
||||
|
|
@ -99,53 +136,71 @@ class TrayManager:
|
|||
kwargs["msecs"] = msecs
|
||||
|
||||
self.tray_widget.showMessage(*args, **kwargs)
|
||||
# TODO validate 'self.tray_widget.supportsMessages()'
|
||||
|
||||
def initialize_addons(self):
|
||||
"""Add addons to tray."""
|
||||
if self._closing:
|
||||
return
|
||||
|
||||
self._addons_manager.initialize(self, 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)
|
||||
self._addons_manager.add_route(
|
||||
"GET", "/tray", self._web_get_tray_info
|
||||
)
|
||||
self._addons_manager.add_route(
|
||||
"POST", "/tray/message", self._web_show_tray_message
|
||||
)
|
||||
|
||||
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()
|
||||
# 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()
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
def restart(self):
|
||||
"""Restart Tray tool.
|
||||
|
|
@ -207,9 +262,13 @@ 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()
|
||||
self._addons_manager.on_exit()
|
||||
|
||||
def execute_in_main_thread(self, callback, *args, **kwargs):
|
||||
|
|
@ -222,6 +281,53 @@ class TrayManager:
|
|||
|
||||
return item
|
||||
|
||||
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(),
|
||||
"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,
|
||||
})
|
||||
|
||||
async def _web_show_tray_message(self, request: Request) -> Response:
|
||||
data = await request.json()
|
||||
try:
|
||||
title = data["title"]
|
||||
message = data["message"]
|
||||
icon = data.get("icon")
|
||||
msecs = data.get("msecs")
|
||||
except KeyError as exc:
|
||||
return json_response(
|
||||
{
|
||||
"error": f"Missing required data. {exc}",
|
||||
"success": False,
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if icon == "information":
|
||||
icon = QtWidgets.QSystemTrayIconInformation
|
||||
elif icon == "warning":
|
||||
icon = QtWidgets.QSystemTrayIconWarning
|
||||
elif icon == "critical":
|
||||
icon = QtWidgets.QSystemTrayIcon.Critical
|
||||
else:
|
||||
icon = None
|
||||
|
||||
self.execute_in_main_thread(
|
||||
self.show_tray_message, title, message, icon, msecs
|
||||
)
|
||||
return json_response({"success": True})
|
||||
|
||||
def _on_update_check_timer(self):
|
||||
try:
|
||||
bundles = ayon_api.get_bundles()
|
||||
|
|
@ -298,20 +404,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."""
|
||||
|
|
@ -319,9 +429,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 +444,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
|
||||
|
||||
|
|
@ -424,19 +535,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)
|
||||
|
|
@ -456,10 +571,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):
|
||||
|
|
@ -468,7 +582,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
|
||||
|
||||
|
|
@ -478,7 +592,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()
|
||||
|
||||
|
|
@ -492,7 +606,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:
|
||||
|
|
@ -511,7 +625,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
self._exited = True
|
||||
|
||||
self.hide()
|
||||
self.tray_man.on_exit()
|
||||
self._tray_manager.on_exit()
|
||||
QtCore.QCoreApplication.exit()
|
||||
|
||||
|
||||
|
|
@ -536,6 +650,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()
|
||||
9
client/ayon_core/tools/tray/webserver/__init__.py
Normal file
9
client/ayon_core/tools/tray/webserver/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .base_routes import RestApiEndpoint
|
||||
from .server import find_free_port, WebServerManager
|
||||
|
||||
|
||||
__all__ = (
|
||||
"RestApiEndpoint",
|
||||
"find_free_port",
|
||||
"WebServerManager",
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -1,24 +1,85 @@
|
|||
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."""
|
||||
|
||||
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
|
||||
self.host = host or "localhost"
|
||||
|
||||
self.client = None
|
||||
self.handlers = {}
|
||||
self.on_stop_callbacks = []
|
||||
|
||||
self.app = web.Application(
|
||||
|
|
@ -30,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:
|
||||
|
|
@ -40,14 +102,46 @@ 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 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 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():
|
||||
|
|
@ -68,7 +162,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
|
||||
|
|
@ -169,6 +169,46 @@ class VersionStartCategoryModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class EnvironmentReplacementModel(BaseSettingsModel):
|
||||
environment_key: str = SettingsField("", title="Enviroment variable")
|
||||
pattern: str = SettingsField("", title="Pattern")
|
||||
replacement: str = SettingsField("", title="Replacement")
|
||||
|
||||
|
||||
class FilterEnvsProfileModel(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
|
||||
host_names: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Host names"
|
||||
)
|
||||
|
||||
task_types: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Task types",
|
||||
enum_resolver=task_types_enum
|
||||
)
|
||||
|
||||
task_names: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Task names"
|
||||
)
|
||||
|
||||
folder_paths: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Folder paths"
|
||||
)
|
||||
|
||||
skip_env_keys: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Skip environment variables"
|
||||
)
|
||||
replace_in_environment: list[EnvironmentReplacementModel] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Replace values in environment"
|
||||
)
|
||||
|
||||
|
||||
class CoreSettings(BaseSettingsModel):
|
||||
studio_name: str = SettingsField("", title="Studio name", scope=["studio"])
|
||||
studio_code: str = SettingsField("", title="Studio code", scope=["studio"])
|
||||
|
|
@ -219,6 +259,9 @@ class CoreSettings(BaseSettingsModel):
|
|||
title="Project environments",
|
||||
section="---"
|
||||
)
|
||||
filter_env_profiles: list[FilterEnvsProfileModel] = SettingsField(
|
||||
default_factory=list,
|
||||
)
|
||||
|
||||
@validator(
|
||||
"environments",
|
||||
|
|
@ -313,5 +356,6 @@ DEFAULT_VALUES = {
|
|||
"project_environments": json.dumps(
|
||||
{},
|
||||
indent=4
|
||||
)
|
||||
),
|
||||
"filter_env_profiles": [],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -964,7 +964,8 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"nuke",
|
||||
"harmony",
|
||||
"photoshop",
|
||||
"aftereffects"
|
||||
"aftereffects",
|
||||
"fusion"
|
||||
],
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ def _product_types_enum():
|
|||
"editorial",
|
||||
"gizmo",
|
||||
"image",
|
||||
"imagesequence",
|
||||
"layout",
|
||||
"look",
|
||||
"matchmove",
|
||||
|
|
@ -212,7 +213,6 @@ def _product_types_enum():
|
|||
"setdress",
|
||||
"take",
|
||||
"usd",
|
||||
"usdShade",
|
||||
"vdbcache",
|
||||
"vrayproxy",
|
||||
"workfile",
|
||||
|
|
@ -222,6 +222,13 @@ def _product_types_enum():
|
|||
]
|
||||
|
||||
|
||||
def filter_type_enum():
|
||||
return [
|
||||
{"value": "is_allow_list", "label": "Allow list"},
|
||||
{"value": "is_deny_list", "label": "Deny list"},
|
||||
]
|
||||
|
||||
|
||||
class LoaderProductTypeFilterProfile(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
# TODO this should use hosts enum
|
||||
|
|
@ -231,9 +238,15 @@ class LoaderProductTypeFilterProfile(BaseSettingsModel):
|
|||
title="Task types",
|
||||
enum_resolver=task_types_enum
|
||||
)
|
||||
is_include: bool = SettingsField(True, title="Exclude / Include")
|
||||
filter_type: str = SettingsField(
|
||||
"is_allow_list",
|
||||
title="Filter type",
|
||||
section="Product type filter",
|
||||
enum_resolver=filter_type_enum
|
||||
)
|
||||
filter_product_types: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Product types",
|
||||
enum_resolver=_product_types_enum
|
||||
)
|
||||
|
||||
|
|
@ -499,14 +512,7 @@ DEFAULT_TOOLS_VALUES = {
|
|||
"workfile_lock_profiles": []
|
||||
},
|
||||
"loader": {
|
||||
"product_type_filter_profiles": [
|
||||
{
|
||||
"hosts": [],
|
||||
"task_types": [],
|
||||
"is_include": True,
|
||||
"filter_product_types": []
|
||||
}
|
||||
]
|
||||
"product_type_filter_profiles": []
|
||||
},
|
||||
"publish": {
|
||||
"template_name_profiles": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue