Merge branch 'develop' into feature/AY-5998_DL-Search--replace-in-environment-values-requires-a-Value

This commit is contained in:
Jakub Trllo 2024-07-24 14:02:25 +02:00 committed by GitHub
commit a82118a3bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1000 additions and 572 deletions

View file

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

View file

@ -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",
)

View file

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

View file

@ -12,7 +12,11 @@ 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
@ -64,7 +68,6 @@ def tray():
Commands.launch_tray()
@Commands.add_addons
@main_cli.group(help="Run command line arguments of AYON addons")
@click.pass_context
def addon(ctx):
@ -80,6 +83,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 +92,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.
@ -103,23 +109,30 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup):
'addon applications extractenvironments ...' instead.
"""
Commands.extractenvironments(
output_json_path, project, asset, task, app, envgroup
output_json_path,
project,
asset,
task,
app,
envgroup,
ctx.obj["addons_manager"]
)
@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, gui):
"""Start CLI publishing.
Publish collects json from path provided as an argument.
S
"""
Commands.publish(path, targets, gui)
Commands.publish(path, targets, gui, ctx.obj["addons_manager"])
@main_cli.command(context_settings={"ignore_unknown_options": True})
@ -245,11 +258,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 +269,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 +307,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:")

View file

@ -3,6 +3,9 @@
import os
import sys
import warnings
from typing import Optional, List
from ayon_core.addon import AddonsManager
class Commands:
@ -12,44 +15,26 @@ class Commands:
"""
@staticmethod
def launch_tray():
from ayon_core.lib import Logger
from ayon_core.tools import tray
from ayon_core.tools.tray import main
Logger.set_process_name("Tray")
tray.main()
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:
def publish(
path: str,
targets: Optional[List[str]] = None,
gui: Optional[bool] = False,
addons_manager: Optional[AddonsManager] = None,
) -> 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.
targets (Optional[List[str]]): List of pyblish targets.
gui (Optional[bool]): Show publish UI.
addons_manager (Optional[AddonsManager]): Addons manager instance.
Raises:
RuntimeError: When there is no path to process.
@ -102,14 +87,15 @@ class Commands:
install_ayon_plugins()
manager = AddonsManager()
if addons_manager is None:
addons_manager = AddonsManager()
publish_paths = manager.collect_plugin_paths()["publish"]
publish_paths = addons_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")
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(
@ -158,15 +144,12 @@ class Commands:
@staticmethod
def extractenvironments(
output_json_path, project, asset, task, app, env_group
output_json_path, project, asset, task, app, env_group, addons_manager
):
"""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"
@ -176,7 +159,6 @@ class Commands:
DeprecationWarning
)
addons_manager = AddonsManager()
applications_addon = addons_manager.get_enabled_addon("applications")
if applications_addon is None:
raise RuntimeError(

View file

@ -17,7 +17,6 @@ from .base import (
load_modules,
ModulesManager,
TrayModulesManager,
)
@ -38,5 +37,4 @@ __all__ = (
"load_modules",
"ModulesManager",
"TrayModulesManager",
)

View file

@ -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",
)

View file

@ -1,13 +0,0 @@
from .version import __version__
from .structures import HostMsgAction
from .webserver_module import (
WebServerAddon
)
__all__ = (
"__version__",
"HostMsgAction",
"WebServerAddon",
)

View file

@ -1 +0,0 @@
__version__ = "1.0.0"

View file

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

View file

@ -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.webserver import HostMsgAction
log = Logger.get_logger(__name__)

View file

@ -1,6 +1,21 @@
from .tray import main
from .webserver import HostMsgAction
from .addons_manager import TrayAddonsManager
from .lib import (
TrayState,
get_tray_state,
is_tray_running,
get_tray_server_url,
main,
)
__all__ = (
"HostMsgAction",
"TrayAddonsManager",
"TrayState",
"get_tray_state",
"is_tray_running",
"get_tray_server_url",
"main",
)

View 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

View file

@ -0,0 +1,384 @@
import os
import sys
import json
import hashlib
import platform
import subprocess
import csv
import time
import signal
from typing import Optional, Dict, Tuple, Any
import ayon_api
import requests
from ayon_core.lib import Logger
from ayon_core.lib.local_settings import get_ayon_appdirs
class TrayState:
NOT_RUNNING = 0
STARTING = 1
RUNNING = 2
class TrayIsRunningError(Exception):
pass
def _get_default_server_url() -> str:
"""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)
csv_content = csv.DictReader(output.decode("utf-8").splitlines())
# if "PID" not in csv_content.fieldnames:
# return False
for _ in csv_content:
return True
return False
def _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
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():
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 main():
from ayon_core.tools.tray.ui import main
Logger.set_process_name("Tray")
state = get_tray_state()
if state == TrayState.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()

View file

@ -0,0 +1,6 @@
from .tray import main
__all__ = (
"main",
)

View file

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

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Before After
Before After

View file

@ -1,10 +1,12 @@
import os
import sys
import time
import collections
import atexit
import json
import platform
from aiohttp.web_response import Response
import ayon_api
from qtpy import QtCore, QtGui, QtWidgets
@ -21,13 +23,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 import TrayAddonsManager
from ayon_core.tools.tray.lib import (
set_tray_server_url,
remove_tray_server_url,
TrayIsRunningError,
)
from .host_console_listener import HostListener
from .info_widget import InfoWidget
from .dialogs import (
UpdateDialog,
@ -54,25 +62,51 @@ 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()
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."""
@ -102,50 +136,64 @@ class TrayManager:
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._get_web_tray_info
)
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 +255,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 +274,19 @@ class TrayManager:
return item
async def _get_web_tray_info(self, request):
return Response(text=json.dumps({
"bundle": os.getenv("AYON_BUNDLE_NAME"),
"dev_mode": is_dev_mode_enabled(),
"staging_mode": is_staging_enabled(),
"addons": {
addon.name: addon.version
for addon in self._addons_manager.get_enabled_addons()
},
"installer_version": os.getenv("AYON_VERSION"),
"running_time": time.time() - self._start_time,
}))
def _on_update_check_timer(self):
try:
bundles = ayon_api.get_bundles()
@ -298,20 +363,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 +388,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 +403,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 +494,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 +530,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 +541,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 +551,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 +565,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 +584,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 +609,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()

View file

@ -0,0 +1,11 @@
from .structures import HostMsgAction
from .base_routes import RestApiEndpoint
from .server import find_free_port, WebServerManager
__all__ = (
"HostMsgAction",
"RestApiEndpoint",
"find_free_port",
"WebServerManager",
)

View file

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

View file

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