Merge branch 'develop' into enhancement/AY-4919_Move-Resolve-client-code

This commit is contained in:
Jakub Trllo 2024-05-31 11:52:27 +02:00 committed by GitHub
commit 2b95bb5e23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 303 additions and 40 deletions

View file

@ -55,6 +55,7 @@ MOVED_ADDON_MILESTONE_VERSIONS = {
"clockify": VersionInfo(0, 2, 0),
"flame": VersionInfo(0, 2, 0),
"max": VersionInfo(0, 2, 0),
"photoshop": VersionInfo(0, 2, 0),
"traypublisher": VersionInfo(0, 2, 0),
"tvpaint": VersionInfo(0, 2, 0),
"maya": VersionInfo(0, 2, 0),

View file

@ -1,3 +1,4 @@
from .version import __version__
from .addon import (
PHOTOSHOP_ADDON_ROOT,
PhotoshopAddon,
@ -6,6 +7,8 @@ from .addon import (
__all__ = (
"__version__",
"PHOTOSHOP_ADDON_ROOT",
"PhotoshopAddon",
"get_launch_script_path",

View file

@ -1,11 +1,14 @@
import os
from ayon_core.addon import AYONAddon, IHostAddon
from .version import __version__
PHOTOSHOP_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class PhotoshopAddon(AYONAddon, IHostAddon):
name = "photoshop"
version = __version__
host_name = "photoshop"
def add_implementation_envs(self, env, _app):
@ -33,4 +36,3 @@ def get_launch_script_path():
return os.path.join(
PHOTOSHOP_ADDON_ROOT, "api", "launch_script.py"
)

View file

@ -17,7 +17,7 @@ ExManCmd /install {path to addon}/api/extension.zxp
The easiest way to get the server and Photoshop launch is with:
```
python -c ^"import ayon_core.hosts.photoshop;ayon_core.hosts.photoshop.launch(""C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe"")^"
python -c ^"import ayon_photoshop;ayon_photoshop.launch(""C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe"")^"
```
`avalon.photoshop.launch` launches the application and server, and also closes the server when Photoshop exists.
@ -128,7 +128,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
import os
from ayon_core.pipeline import publish
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class ExtractImage(publish.Extractor):

View file

@ -22,9 +22,9 @@ from ayon_core.pipeline.workfile import (
)
from ayon_core.pipeline.template_data import get_template_data_with_names
from ayon_core.tools.utils import host_tools
from ayon_core.tools.adobe_webserver.app import WebServerTool
from ayon_core.pipeline.context_tools import change_current_context
from .webserver import WebServerTool
from .ws_stub import PhotoshopServerStub
log = Logger.get_logger(__name__)

View file

@ -8,7 +8,7 @@ workfile or others.
import os
import sys
from ayon_core.hosts.photoshop.api.lib import main as host_main
from ayon_photoshop.api.lib import main as host_main
# Get current file to locate start point of sys.argv
CURRENT_FILE = os.path.abspath(__file__)

View file

@ -19,7 +19,7 @@ def safe_excepthook(*args):
def main(*subprocess_args):
from ayon_core.hosts.photoshop.api import PhotoshopHost
from ayon_photoshop.api import PhotoshopHost
host = PhotoshopHost()
install_host(host)

View file

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -21,8 +21,8 @@ from ayon_core.host import (
)
from ayon_core.pipeline.load import any_outdated_containers
from ayon_core.hosts.photoshop import PHOTOSHOP_ADDON_ROOT
from ayon_core.tools.utils import get_ayon_qt_app
from ayon_photoshop import PHOTOSHOP_ADDON_ROOT
from . import lib

View file

@ -0,0 +1,241 @@
"""Webserver for communication with photoshop.
Aiohttp (Asyncio) based websocket server used for communication with host
application.
This webserver is started in spawned Python process that opens DCC during
its launch, waits for connection from DCC and handles communication going
forward. Server is closed before Python process is killed.
"""
import os
import logging
import urllib
import threading
import asyncio
import socket
from aiohttp import web
from wsrpc_aiohttp import WSRPCClient
from ayon_core.pipeline import get_global_context
log = logging.getLogger(__name__)
class WebServerTool:
"""
Basic POC implementation of asychronic websocket RPC server.
Uses class in external_app_1.py to mimic implementation for single
external application.
'test_client' folder contains two test implementations of client
"""
_instance = None
def __init__(self):
WebServerTool._instance = self
self.client = None
self.handlers = {}
self.on_stop_callbacks = []
port = None
host_name = "localhost"
websocket_url = os.getenv("WEBSOCKET_URL")
if websocket_url:
parsed = urllib.parse.urlparse(websocket_url)
port = parsed.port
host_name = parsed.netloc.split(":")[0]
if not port:
port = 8098 # fallback
self.port = port
self.host_name = host_name
self.app = web.Application()
# add route with multiple methods for single "external app"
self.webserver_thread = WebServerThread(self, self.port)
def add_route(self, *args, **kwargs):
self.app.router.add_route(*args, **kwargs)
def add_static(self, *args, **kwargs):
self.app.router.add_static(*args, **kwargs)
def start_server(self):
if self.webserver_thread and not self.webserver_thread.is_alive():
self.webserver_thread.start()
def stop_server(self):
self.stop()
async def send_context_change(self, host):
"""
Calls running webserver to inform about context change
Used when new PS/AE should be triggered,
but one already running, without
this publish would point to old context.
"""
client = WSRPCClient(os.getenv("WEBSOCKET_URL"),
loop=asyncio.get_event_loop())
await client.connect()
context = get_global_context()
project_name = context["project_name"]
folder_path = context["folder_path"]
task_name = context["task_name"]
log.info("Sending context change to {}{}/{}".format(
project_name, folder_path, task_name
))
await client.call(
'{}.set_context'.format(host),
project=project_name,
folder=folder_path,
task=task_name
)
await client.close()
def port_occupied(self, host_name, port):
"""
Check if 'url' is already occupied.
This could mean, that app is already running and we are trying open it
again. In that case, use existing running webserver.
Check here is easier than capturing exception from thread.
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as con:
result = con.connect_ex((host_name, port)) == 0
if result:
print(f"Port {port} is already in use")
return result
def call(self, func):
log.debug("websocket.call {}".format(func))
future = asyncio.run_coroutine_threadsafe(
func,
self.webserver_thread.loop
)
result = future.result()
return result
@staticmethod
def get_instance():
if WebServerTool._instance is None:
WebServerTool()
return WebServerTool._instance
@property
def is_running(self):
if not self.webserver_thread:
return False
return self.webserver_thread.is_running
def stop(self):
if not self.is_running:
return
try:
log.debug("Stopping websocket server")
self.webserver_thread.is_running = False
self.webserver_thread.stop()
except Exception:
log.warning(
"Error has happened during Killing websocket server",
exc_info=True
)
def thread_stopped(self):
for callback in self.on_stop_callbacks:
callback()
class WebServerThread(threading.Thread):
""" Listener for websocket rpc requests.
It would be probably better to "attach" this to main thread (as for
example Harmony needs to run something on main thread), but currently
it creates separate thread and separate asyncio event loop
"""
def __init__(self, module, port):
super(WebServerThread, self).__init__()
self.is_running = False
self.port = port
self.module = module
self.loop = None
self.runner = None
self.site = None
self.tasks = []
def run(self):
self.is_running = True
try:
log.info("Starting web server")
self.loop = asyncio.new_event_loop() # create new loop for thread
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.start_server())
websocket_url = "ws://localhost:{}/ws".format(self.port)
log.debug(
"Running Websocket server on URL: \"{}\"".format(websocket_url)
)
asyncio.ensure_future(self.check_shutdown(), loop=self.loop)
self.loop.run_forever()
except Exception:
self.is_running = False
log.warning(
"Websocket Server service has failed", exc_info=True
)
raise
finally:
self.loop.close() # optional
self.is_running = False
self.module.thread_stopped()
log.info("Websocket server stopped")
async def start_server(self):
""" Starts runner and TCPsite """
self.runner = web.AppRunner(self.module.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, 'localhost', self.port)
await self.site.start()
def stop(self):
"""Sets is_running flag to false, 'check_shutdown' shuts server down"""
self.is_running = False
async def check_shutdown(self):
""" Future that is running and checks if server should be running
periodically.
"""
while self.is_running:
while self.tasks:
task = self.tasks.pop(0)
log.debug("waiting for task {}".format(task))
await task
log.debug("returned value {}".format(task.result))
await asyncio.sleep(0.5)
log.debug("Starting shutdown")
await self.site.stop()
log.debug("Site stopped")
await self.runner.cleanup()
log.debug("Runner stopped")
tasks = [task for task in asyncio.all_tasks() if
task is not asyncio.current_task()]
list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks
results = await asyncio.gather(*tasks, return_exceptions=True)
log.debug(f'Finished awaiting cancelled tasks, results: {results}...')
await self.loop.shutdown_asyncgens()
# to really make sure everything else has time to stop
await asyncio.sleep(0.07)
self.loop.stop()

View file

@ -6,7 +6,7 @@ import json
import attr
from wsrpc_aiohttp import WebSocketAsync
from ayon_core.tools.adobe_webserver.app import WebServerTool
from .webserver import WebServerTool
@attr.s

View file

@ -7,7 +7,7 @@ from ayon_core.lib import (
is_using_ayon_console,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.photoshop import get_launch_script_path
from ayon_photoshop import get_launch_script_path
def get_launch_kwargs(kwargs):

View file

@ -2,13 +2,13 @@ import re
import ayon_api
import ayon_core.hosts.photoshop.api as api
from ayon_core.lib import prepare_template_data
from ayon_core.pipeline import (
AutoCreator,
CreatedInstance
)
from ayon_core.hosts.photoshop.api.pipeline import cache_and_get_instances
from ayon_photoshop import api
from ayon_photoshop.api.pipeline import cache_and_get_instances
class PSAutoCreator(AutoCreator):

View file

@ -1,7 +1,7 @@
import ayon_api
import ayon_core.hosts.photoshop.api as api
from ayon_core.hosts.photoshop.lib import PSAutoCreator, clean_product_name
from ayon_photoshop import api
from ayon_photoshop.lib import PSAutoCreator, clean_product_name
from ayon_core.lib import BoolDef, prepare_template_data
from ayon_core.pipeline.create import get_product_name, CreatedInstance

View file

@ -1,6 +1,5 @@
import re
from ayon_core.hosts.photoshop import api
from ayon_core.lib import BoolDef
from ayon_core.pipeline import (
Creator,
@ -9,8 +8,9 @@ from ayon_core.pipeline import (
)
from ayon_core.lib import prepare_template_data
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
from ayon_core.hosts.photoshop.api.pipeline import cache_and_get_instances
from ayon_core.hosts.photoshop.lib import clean_product_name
from ayon_photoshop import api
from ayon_photoshop.api.pipeline import cache_and_get_instances
from ayon_photoshop.lib import clean_product_name
class ImageCreator(Creator):

View file

@ -1,4 +1,4 @@
from ayon_core.hosts.photoshop.lib import PSAutoCreator
from ayon_photoshop.lib import PSAutoCreator
class ReviewCreator(PSAutoCreator):

View file

@ -1,4 +1,4 @@
from ayon_core.hosts.photoshop.lib import PSAutoCreator
from ayon_photoshop.lib import PSAutoCreator
class WorkfileCreator(PSAutoCreator):

View file

@ -1,8 +1,8 @@
import re
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.photoshop import api as photoshop
from ayon_core.hosts.photoshop.api import get_unique_layer_name
from ayon_photoshop import api as photoshop
from ayon_photoshop.api import get_unique_layer_name
class ImageLoader(photoshop.PhotoshopLoader):

View file

@ -2,8 +2,8 @@ import os
import qargparse
from ayon_core.hosts.photoshop import api as photoshop
from ayon_core.hosts.photoshop.api import get_unique_layer_name
from ayon_photoshop import api as photoshop
from ayon_photoshop.api import get_unique_layer_name
class ImageFromSequenceLoader(photoshop.PhotoshopLoader):

View file

@ -1,8 +1,8 @@
import re
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.photoshop import api as photoshop
from ayon_core.hosts.photoshop.api import get_unique_layer_name
from ayon_photoshop import api as photoshop
from ayon_photoshop.api import get_unique_layer_name
class ReferenceLoader(photoshop.PhotoshopLoader):

View file

@ -2,7 +2,7 @@
"""Close PS after publish. For Webpublishing only."""
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class ClosePS(pyblish.api.ContextPlugin):

View file

@ -1,6 +1,6 @@
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
from ayon_core.pipeline.create import get_product_name

View file

@ -1,6 +1,6 @@
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class CollectAutoImageRefresh(pyblish.api.ContextPlugin):

View file

@ -7,7 +7,7 @@ Provides:
"""
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
from ayon_core.pipeline.create import get_product_name

View file

@ -1,7 +1,7 @@
import os
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
from ayon_core.pipeline.create import get_product_name

View file

@ -4,8 +4,8 @@ import re
import pyblish.api
from ayon_core.lib import prepare_template_data, is_in_tests
from ayon_core.hosts.photoshop import api as photoshop
from ayon_core.settings import get_project_settings
from ayon_photoshop import api as photoshop
class CollectColorCodedInstances(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class CollectCurrentFile(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import re
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class CollectExtensionVersion(pyblish.api.ContextPlugin):

View file

@ -1,6 +1,6 @@
import pyblish.api
from ayon_core.hosts.photoshop import api
from ayon_photoshop import api
class CollectImage(pyblish.api.InstancePlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class ExtractImage(pyblish.api.ContextPlugin):

View file

@ -7,7 +7,7 @@ from ayon_core.lib import (
get_ffmpeg_tool_args,
)
from ayon_core.pipeline import publish
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class ExtractReview(publish.Extractor):

View file

@ -1,5 +1,5 @@
from ayon_core.pipeline import publish
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class ExtractSaveScene(publish.Extractor):

View file

@ -3,7 +3,7 @@ import pyblish.api
from ayon_core.pipeline.publish import get_errored_plugins_from_context
from ayon_core.lib import version_up
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class IncrementWorkfile(pyblish.api.InstancePlugin):

View file

@ -6,7 +6,7 @@ from ayon_core.pipeline.publish import (
PublishXmlValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
class ValidateInstanceFolderRepair(pyblish.api.Action):

View file

@ -2,7 +2,7 @@ import re
import pyblish.api
from ayon_core.hosts.photoshop import api as photoshop
from ayon_photoshop import api as photoshop
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
from ayon_core.pipeline.publish import (
ValidateContentsOrder,

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'photoshop' version."""
__version__ = "0.2.0"

View file

@ -0,0 +1,6 @@
[project]
name="photoshop"
description="AYON Phostoshop addon."
[ayon.runtimeDependencies]
wsrpc_aiohttp = "^3.1.1" # websocket server

View file

@ -1,3 +1,10 @@
name = "photoshop"
title = "Photoshop"
version = "0.1.3"
version = "0.2.0"
client_dir = "ayon_photoshop"
ayon_required_addons = {
"core": ">0.3.2",
}
ayon_compatible_addons = {}