diff --git a/.run/custom_pre.run.xml b/.run/custom_pre.run.xml new file mode 100644 index 0000000000..1d5cbd9f04 --- /dev/null +++ b/.run/custom_pre.run.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/.run/prod.run.xml b/.run/prod.run.xml new file mode 100644 index 0000000000..067135fdac --- /dev/null +++ b/.run/prod.run.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/.run/prod_pre.run.xml b/.run/prod_pre.run.xml new file mode 100644 index 0000000000..357d7c2aed --- /dev/null +++ b/.run/prod_pre.run.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/src/quadpype/events/handler.py b/src/quadpype/events/handler.py index 0302b9ea51..c47475f318 100644 --- a/src/quadpype/events/handler.py +++ b/src/quadpype/events/handler.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- """Module storing the class and logic of the QuadPype Events Handler.""" import os -import time +import json import functools +from time import sleep from typing import Optional from datetime import datetime, timezone, timedelta import pymongo +import pymongo.errors import requests from qtpy import QtCore, QtWidgets @@ -21,12 +23,11 @@ _EVENT_HANDLER = None -CHECK_NEW_EVENTS_INTERVAL_SECONDS = 120 -MINIMUM_EVENT_EXPIRE_IN_SECONDS = CHECK_NEW_EVENTS_INTERVAL_SECONDS + 10 + +DEFAULT_RESPONSES_WAITING_TIME_SECS = 3 class EventHandlerWorker(QtCore.QThread): - task_completed = QtCore.Signal(int) def __init__(self, manager, parent=None): """Initialize the worker thread.""" @@ -39,6 +40,9 @@ def __init__(self, manager, parent=None): self._webserver_url = self._manager.webserver.url + # Store the amount of time an error occurred + self._error_count = 0 + # Get info about the timestamp of the last handled event self._last_handled_event_timestamp = self._manager.app_registry.get_item( "last_handled_event_timestamp", fallback=0 @@ -53,58 +57,106 @@ def __init__(self, manager, parent=None): self._last_handled_event_timestamp ) + def _respond_to_event(self, event_doc, response): + curr_user_id = self._manager.user_profile["user_id"] + response_data = { + "user_id": curr_user_id, + "content": "" + } + + if response.text: + response_content = json.loads(response.text) + if "content" in response_content: + response_data["content"] = response_content["content"] + else: + response_data["content"] = response_content + + try: + # Only push the response if the user_id hasn't already sent a response + self._manager.collection.update_one( + {"_id": event_doc["_id"], "responses.user_id": {"$ne": curr_user_id}}, + {"$push": {"responses": response_data}} + ) + except Exception as e: + print(f"Error updating event: {e}") + + def _update_last_handled_event_timestamp(self): + # Update the local app registry + # To keep track and properly retrieve the next events after that + self._manager.app_registry.set_item( + "last_handled_event_timestamp", + get_timestamp_str(self._last_handled_event_timestamp)) + + def _process_event(self, event_doc): + current_timestamp = datetime.now() + + if (event_doc["target_users"] and self._manager.user_profile["user_id"] not in event_doc["target_users"]) or \ + (event_doc["target_groups"] and self._manager.user_profile["role"] not in event_doc["target_groups"]) or \ + ("expire_at" in event_doc and current_timestamp > event_doc["expire_at"]): + # User is not targeted by this event OR + # This event is expired, so we skip it + self._last_handled_event_timestamp = event_doc["timestamp"] + return + + route = event_doc["route"] + if not route.startswith("/"): + route = "/" + route + + url_with_route = self._webserver_url + route + + # Send the event to the webserver API + funct = getattr(requests, event_doc["type"]) + if not event_doc["content"]: + response_obj = funct(url_with_route) + else: + response_obj = funct(url_with_route, **event_doc["content"]) + + if response_obj.status_code == 200 and "responses" in event_doc: + # A response is expected + self._respond_to_event(event_doc, response_obj) + + # Update the last handled timestamp + self._last_handled_event_timestamp = event_doc["timestamp"] + def run(self): - start_time = time.time() + # Reset the error count + self._error_count = 0 - # Retrieve the new events to process + # Retrieve the new events that were added since the last connection db_cursor = self._manager.collection.find({ "timestamp": { "$gt": self._last_handled_event_timestamp } }).sort("timestamp", 1) - # Process the events - current_timestamp = datetime.now() - for doc in db_cursor: + # Process these events + for event_doc in db_cursor: if not self._manager.is_running: - break - if "expire_at" in doc and doc["expire_at"] > current_timestamp: - # This event is expired, skip - self._last_handled_event_timestamp = doc["timestamp"] - continue - - route = doc["route"] - if not route.startwith("/"): - route = "/" + route - - url_with_route = self._webserver_url + route - - # Send the event to the webserver API - funct = getattr(requests, doc["type"]) - if not doc["content"]: - response = funct(url_with_route) - else: - response = funct(url_with_route, **doc["content"]) - - # Update the last handled timestamp - self._last_handled_event_timestamp = doc["timestamp"] + self._update_last_handled_event_timestamp() + return + self._process_event(event_doc) if db_cursor.retrieved > 0: - # Update the local app registry - # To keep track and properly retrieve the next events after that - self._manager.app_registry.set_item( - "last_handled_event_timestamp", - get_timestamp_str(self._last_handled_event_timestamp)) - - if not self._manager.is_running: - # Simply exit without emiting the signal - return - - elapsed_time = round(time.time() - start_time) - waiting_time = max(0, CHECK_NEW_EVENTS_INTERVAL_SECONDS - elapsed_time) - waiting_time_msec = waiting_time * 1000 - - self.task_completed.emit(waiting_time_msec) + self._update_last_handled_event_timestamp() + + # Watch for new event documents directly on the collection + with self._manager.collection.watch([{"$match": {"operationType": "insert"}}]) as stream: + while self._manager.is_running: + try: + # Non-blocking method to get the next change + change = stream.try_next() + if change: + event_doc = change["fullDocument"] + self._process_event(event_doc) + self._update_last_handled_event_timestamp() + except pymongo.errors.PyMongoError as e: + print(f"Error in change stream: {e}") + self._error_count += 1 + if self._error_count > 4: + print("Stopping the Event Handling due to the errors.") + return + + sleep(0.2) class EventsHandlerManager: @@ -172,9 +224,6 @@ def _start_worker(self): self._is_running = True self._worker_thread.start(QtCore.QThread.HighestPriority) - def _restart_worker(self, waiting_time_msec): - self._timer.start(waiting_time_msec) - def start(self): if self._worker_thread: raise RuntimeError("The Event Handler cannot be started multiple times.") @@ -182,7 +231,6 @@ def start(self): raise RuntimeError("Webserver not set. Cannot start the event handler.") self._worker_thread = EventHandlerWorker(self) - self._worker_thread.task_completed.connect(self._restart_worker) if not self._webserver.is_running: # The webserver is not yet running, wait a bit before starting the loop @@ -245,12 +293,14 @@ def _validate_event_values(event_route: str, @require_events_handler -def register_event(event_route: str, - event_type: str, - content: Optional[dict], - target_users: Optional[list] = None, - target_groups: Optional[list] = None, - expire_in: Optional[int] = None): +def send_event(event_route: str, + event_type: str, + content: Optional[dict], + target_users: Optional[list] = None, + target_groups: Optional[list] = None, + expire_in: Optional[int] = None, + wait_for_responses: bool = False, + waiting_time_secs: int = DEFAULT_RESPONSES_WAITING_TIME_SECS): if target_users is None: target_users = [] elif isinstance(target_users, str): @@ -268,9 +318,10 @@ def register_event(event_route: str, if not is_valid: raise ValueError(invalid_msg) - timestamp = datetime.now() + timestamp = datetime.now(timezone.utc) event = { "timestamp": timestamp, + "user_id": _EVENT_HANDLER.user_profile["user_id"], "route": event_route, "type": event_type, "target_users": target_users, @@ -279,7 +330,14 @@ def register_event(event_route: str, } if expire_in is not None: - expire_in += MINIMUM_EVENT_EXPIRE_IN_SECONDS event["expire_at"] = timestamp + timedelta(seconds=expire_in) - _EVENT_HANDLER.collection.insert_one(event) + if wait_for_responses: + event["responses"] = [] + + event_doc_id = _EVENT_HANDLER.collection.insert_one(event).inserted_id + + if wait_for_responses: + sleep(waiting_time_secs) + event_doc = _EVENT_HANDLER.collection.find_one({"_id": event_doc_id}) + return event_doc["responses"] diff --git a/src/quadpype/events/notification.py b/src/quadpype/events/notification.py index 984adfb541..83f41e95a3 100644 --- a/src/quadpype/events/notification.py +++ b/src/quadpype/events/notification.py @@ -17,6 +17,7 @@ async def show_tray_message(message: str): tray_icon_widget.showMessage("Notification", message) return {"message": "Message displayed"} + @router.post("/popup/", tags=["popup"]) async def show_popup_message(message: str): tray_icon_widget: Optional[SystemTrayIcon] = get_tray_icon_widget() diff --git a/src/quadpype/hosts/aftereffects/addon.py b/src/quadpype/hosts/aftereffects/addon.py index 39ca383a0c..7e3a0e09e9 100644 --- a/src/quadpype/hosts/aftereffects/addon.py +++ b/src/quadpype/hosts/aftereffects/addon.py @@ -12,7 +12,7 @@ def add_implementation_envs(self, env, _app): """Modify environments to contain all required for implementation.""" defaults = { "QUADPYPE_LOG_NO_COLORS": "True", - "WEBSOCKET_URL": "ws://localhost:8097/ws/" + "QUADPYPE_WEBSOCKET_URL": "ws://localhost:8017/ws/" } for key, value in defaults.items(): if not env.get(key): diff --git a/src/quadpype/hosts/aftereffects/api/README.md b/src/quadpype/hosts/aftereffects/api/README.md index 938fad87d0..803af3ce2e 100644 --- a/src/quadpype/hosts/aftereffects/api/README.md +++ b/src/quadpype/hosts/aftereffects/api/README.md @@ -59,7 +59,7 @@ Expected deployed extension location on default Windows: For easier debugging of Javascript: https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1 Add (optional) --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome -then localhost:8092 +then localhost:8017 Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01 ## Resources diff --git a/src/quadpype/hosts/aftereffects/api/checksums b/src/quadpype/hosts/aftereffects/api/checksums index 469abab1c8..90ea7ef012 100644 --- a/src/quadpype/hosts/aftereffects/api/checksums +++ b/src/quadpype/hosts/aftereffects/api/checksums @@ -1,9 +1,9 @@ -19D4C7C6C4BEA8D552873219030B5CB71D8A8DC2526B7F3909043725E36838EA:.debug +C07F1436D9ED900B3E94605183DB29E82301191D9DC3D55917CF9A68842E6F92:.debug 48F60C67335C984141456712CE8E6F9D82B8C0DF52353571C8D4FCFAB9724A94:index.html D6CCD9DD044B02E4F2ED1B49E9C083D823D6E3940F9825DD932D07C8B0229C78:css\boilerplate.css 0D183A454C941E5C937DCFDABFFE257F4FBB49BD418DF200AB6C01A39836BA27:css\styles.css 1CE5197F378AD02BCE717B29CB25E7423AFA2E6D22996BABB7297E025D192B4F:css\topcoat-desktop-dark.min.css -4AC922CCE8FC86C9404831E609F3A718A3C0910690E528E413D52FC89810B0A3:CSXS\manifest.xml +F7B0CEE06DDED97BDD89D21B246F80285AC2CFAD205966F39A39F4C644D4D5B6:CSXS\manifest.xml 9F51E857F342D27FF05A97A09E96401EE7EB901C978A31F560F0A49C3A6592FB:icons\ayon_logo.png A1B1F026B3BB43F4801DB4FAB5CDAE7C1DDCABA064FEDB0A227F633F32EB4F61:icons\iconDarkNormal.png 6C04A66B67CCDA76D4689BF5F80DFB46377E8B3A652CFC5B3D268E9E27DC9853:icons\iconDarkRollover.png @@ -11,7 +11,7 @@ A1B1F026B3BB43F4801DB4FAB5CDAE7C1DDCABA064FEDB0A227F633F32EB4F61:icons\iconDarkN 2BEECE898BE17D6843AD22D78139E306DEE28030FC79712B32D99F8487328AB9:icons\iconNormal.png DD24CF1239E1FFA18B7EE76084E75CE74178CD60A1CF388784934D3DF2F9EED6:icons\iconQuadPype.png F77A105590E0231818FBAFE766C7A672DC5C6A432E3416594C76D2249D2E978B:icons\iconRollover.png -96F6BD26A8E430D1DE26E2D516DB9DA63328F15936D1F758969524924F9FA5AD:js\main.js +76F230D6FA0F205C83F3FBBC8999D6DBE6A752D5BA7868FE463A1710A20C2C02:js\main.js 423DF7EC0AE7C32886E06C000AE90E3FDAA919DAC563DD9DDEBB787E9E1B6025:js\themeManager.js 5F8F8EBCFFC668009CF48DF1301F8C962A6D54F6EA12F0B0469C42C9DE48F742:js\libs\CSInterface.js 4D9586A075F082A04FD40178499C472012B351DB4C1A4D210907A0891F7D8AD9:js\libs\jquery-2.0.2.min.js diff --git a/src/quadpype/hosts/aftereffects/api/extension.zxp b/src/quadpype/hosts/aftereffects/api/extension.zxp index be989ad46e..22a1442f63 100644 Binary files a/src/quadpype/hosts/aftereffects/api/extension.zxp and b/src/quadpype/hosts/aftereffects/api/extension.zxp differ diff --git a/src/quadpype/hosts/aftereffects/api/extension/.debug b/src/quadpype/hosts/aftereffects/api/extension/.debug index a81c433392..8812ae67bf 100644 --- a/src/quadpype/hosts/aftereffects/api/extension/.debug +++ b/src/quadpype/hosts/aftereffects/api/extension/.debug @@ -6,25 +6,25 @@ - + + + + - + - + - - - - + - + - + diff --git a/src/quadpype/hosts/aftereffects/api/extension/CSXS/manifest.xml b/src/quadpype/hosts/aftereffects/api/extension/CSXS/manifest.xml index a3289b331e..e1267bcb2e 100644 --- a/src/quadpype/hosts/aftereffects/api/extension/CSXS/manifest.xml +++ b/src/quadpype/hosts/aftereffects/api/extension/CSXS/manifest.xml @@ -1,6 +1,5 @@ - - + + @@ -39,7 +38,7 @@ - + ./index.html ./jsx/hostscript.jsx diff --git a/src/quadpype/hosts/aftereffects/api/extension/js/main.js b/src/quadpype/hosts/aftereffects/api/extension/js/main.js index f47d60fdd7..b4c7fdfb56 100644 --- a/src/quadpype/hosts/aftereffects/api/extension/js/main.js +++ b/src/quadpype/hosts/aftereffects/api/extension/js/main.js @@ -57,7 +57,7 @@ function get_extension_version(){ function main(websocket_url){ // creates connection to 'websocket_url', registers routes - var default_url = 'ws://localhost:8099/ws/'; + var default_url = 'ws://localhost:8017/ws/'; if (websocket_url == ''){ websocket_url = default_url; @@ -386,7 +386,7 @@ function main(websocket_url){ } /** main entry point **/ -startUp("WEBSOCKET_URL"); +startUp("QUADPYPE_WEBSOCKET_URL"); (function () { 'use strict'; diff --git a/src/quadpype/hosts/blender/api/lib.py b/src/quadpype/hosts/blender/api/lib.py index f76f1980eb..0cda91741b 100644 --- a/src/quadpype/hosts/blender/api/lib.py +++ b/src/quadpype/hosts/blender/api/lib.py @@ -7,7 +7,8 @@ import bpy import addon_utils from quadpype.lib import Logger, NumberDef - +from quadpype.pipeline import get_current_project_name, get_current_asset_name +from quadpype.client import get_asset_by_name if TYPE_CHECKING: from quadpype.pipeline.create import CreateContext # noqa: F401 @@ -487,7 +488,7 @@ def setattr_deep(root, path, value): setattr_deep(obj, key, value) -def collect_animation_defs(create_context, step=True, fps=False): +def collect_animation_defs(step=True, fps=False): """Get the basic animation attribute definitions for the publisher. Arguments: @@ -504,18 +505,15 @@ def collect_animation_defs(create_context, step=True, fps=False): # get scene values as defaults scene = bpy.context.scene - # frame_start = scene.frame_start - # frame_end = scene.frame_end - # handle_start = 0 - # handle_end = 0 # use task entity attributes to set defaults based on current context - task_entity = create_context.get_current_task_entity() - attrib: dict = task_entity["attrib"] - frame_start = attrib["frameStart"] - frame_end = attrib["frameEnd"] - handle_start = attrib["handleStart"] - handle_end = attrib["handleEnd"] + project_name = get_current_project_name() + asset_name = get_current_asset_name() + attrib = get_asset_by_name(project_name, asset_name) + frame_start = attrib.get("frameStart", scene.frame_start) + frame_end = attrib.get("frameEnd", scene.frame_end) + handle_start = attrib.get("handleStart", 0) + handle_end = attrib.get("handleEnd", 0) # build attributes defs = [ diff --git a/src/quadpype/hosts/blender/api/ops.py b/src/quadpype/hosts/blender/api/ops.py index c2ffca6472..8af0bf0d7e 100644 --- a/src/quadpype/hosts/blender/api/ops.py +++ b/src/quadpype/hosts/blender/api/ops.py @@ -47,7 +47,7 @@ class BlenderApplication: def get_app(cls): if cls._instance is None: # If any other addon or plug-in may have initialed a Qt application - # before AYON then we should take the existing instance instead. + # before QuadPype then we should take the existing instance instead. application = QtWidgets.QApplication.instance() if application is None: application = QtWidgets.QApplication(sys.argv) @@ -354,8 +354,19 @@ class LaunchWorkFiles(LaunchQtApp): _tool_name = "workfiles" def execute(self, context): - return super().execute(context) + result = super().execute(context) + self._window.set_context({ + "asset": get_current_asset_name(), + "task": get_current_task_name() + }) + return result + def before_window_show(self): + self._window.root = str(Path( + os.environ.get("AVALON_WORKDIR", ""), + os.environ.get("AVALON_SCENEDIR", ""), + )) + self._window.refresh() class SetFrameRange(bpy.types.Operator): bl_idname = "wm.set_frame_range" diff --git a/src/quadpype/hosts/blender/api/pipeline.py b/src/quadpype/hosts/blender/api/pipeline.py index 90b4b1b664..e1a07832c3 100644 --- a/src/quadpype/hosts/blender/api/pipeline.py +++ b/src/quadpype/hosts/blender/api/pipeline.py @@ -626,7 +626,6 @@ def ls() -> Iterator: called containers. """ container_ids = { - AYON_CONTAINER_ID, # Backwards compatibility AVALON_CONTAINER_ID } diff --git a/src/quadpype/hosts/blender/plugins/create/create_animation.py b/src/quadpype/hosts/blender/plugins/create/create_animation.py index 1cc6447d2e..a24e9961f6 100644 --- a/src/quadpype/hosts/blender/plugins/create/create_animation.py +++ b/src/quadpype/hosts/blender/plugins/create/create_animation.py @@ -32,6 +32,5 @@ def create( return collection def get_instance_attr_defs(self): - defs = lib.collect_animation_defs(self.create_context, - step=False) + defs = lib.collect_animation_defs(step=False) return defs diff --git a/src/quadpype/hosts/blender/plugins/create/create_pointcache.py b/src/quadpype/hosts/blender/plugins/create/create_pointcache.py index ff5a2752df..c02dc0a004 100644 --- a/src/quadpype/hosts/blender/plugins/create/create_pointcache.py +++ b/src/quadpype/hosts/blender/plugins/create/create_pointcache.py @@ -29,7 +29,6 @@ def create( return collection def get_instance_attr_defs(self): - defs = lib.collect_animation_defs(self.create_context, - step=False) + defs = lib.collect_animation_defs(step=False) return defs diff --git a/src/quadpype/hosts/blender/plugins/create/create_render.py b/src/quadpype/hosts/blender/plugins/create/create_render.py index e9d237d682..221449ea07 100644 --- a/src/quadpype/hosts/blender/plugins/create/create_render.py +++ b/src/quadpype/hosts/blender/plugins/create/create_render.py @@ -46,6 +46,6 @@ def create( return collection def get_instance_attr_defs(self): - defs = lib.collect_animation_defs(self.create_context) + defs = lib.collect_animation_defs() return defs diff --git a/src/quadpype/hosts/blender/plugins/create/create_review.py b/src/quadpype/hosts/blender/plugins/create/create_review.py index 3bc1131296..acccbaba2c 100644 --- a/src/quadpype/hosts/blender/plugins/create/create_review.py +++ b/src/quadpype/hosts/blender/plugins/create/create_review.py @@ -27,6 +27,6 @@ def create( return collection def get_instance_attr_defs(self): - defs = lib.collect_animation_defs(self.create_context) + defs = lib.collect_animation_defs() return defs diff --git a/src/quadpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py b/src/quadpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py index 0b0d350b87..6e3927c294 100644 --- a/src/quadpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py +++ b/src/quadpype/hosts/blender/plugins/publish/validate_no_colons_in_name.py @@ -4,10 +4,11 @@ import quadpype.hosts.blender.api.action -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - OptionalPyblishPluginMixin, - PublishValidationError +from quadpype.pipeline import OptionalPyblishPluginMixin +from quadpype.hosts.blender.api import plugin +from quadpype.pipeline.publish import ( + PublishValidationError, + ValidateContentsOrder ) diff --git a/src/quadpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/src/quadpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py index 3de1f32b15..de763aaa95 100644 --- a/src/quadpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py +++ b/src/quadpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -6,8 +6,6 @@ OptionalPyblishPluginMixin, PublishValidationError ) -from ayon_blender.api import plugin - class ValidateRenderCameraIsSet( plugin.BlenderInstancePlugin, diff --git a/src/quadpype/hosts/photoshop/addon.py b/src/quadpype/hosts/photoshop/addon.py index 432be25246..c9eba0bef9 100644 --- a/src/quadpype/hosts/photoshop/addon.py +++ b/src/quadpype/hosts/photoshop/addon.py @@ -15,7 +15,7 @@ def add_implementation_envs(self, env, _app): """Modify environments to contain all required for implementation.""" defaults = { "QUADPYPE_LOG_NO_COLORS": "True", - "WEBSOCKET_URL": "ws://localhost:8099/ws/" + "QUADPYPE_WEBSOCKET_URL": "ws://localhost:8018/ws/" } for key, value in defaults.items(): if not env.get(key): diff --git a/src/quadpype/hosts/photoshop/api/checksums b/src/quadpype/hosts/photoshop/api/checksums index 9a47a117fe..103dfcb93e 100644 --- a/src/quadpype/hosts/photoshop/api/checksums +++ b/src/quadpype/hosts/photoshop/api/checksums @@ -1,11 +1,11 @@ 8716BC5C7DBF3338B8F104CFDEED84B1F88E2D4CC2A1AA2E5AAE577EDD396DB8:.debug 28997C43711E01361728A5FA0F67E7EEBDD616FB083B0210934C18FDA3D5E92B:index.html -7046A70229FD9B8833C76A46E067734945E2C465C607965D9945E06EC4AD80BB:client\client.js +E971793EC8E773D2D9FAAC01A35C94DEA2864BAD426460CA025285B5B78C943B:client\client.js 5F8F8EBCFFC668009CF48DF1301F8C962A6D54F6EA12F0B0469C42C9DE48F742:client\CSInterface.js FD0E8BED2FA88E12F5D49646944793C134428F80311BC03F3BCB1196CE3AF0AD:client\loglevel.min.js 61E9E273ADF6E290D9E6911500D44F2AB02DAEFCD902888106017735590E26C1:client\wsrpc.js 78E2A27896C24A4AF1D232AAC321E09B8995CC9A86C9ABB19C8A97A861946F41:client\wsrpc.min.js -5009853F7DDD19A2CB8F1A2920CF8D1008E61274766023A85114EF04D56039B4:CSXS\manifest.xml +1630FED4CC9468A7C9DC3B1F8E5CE3159E3583579C1FE7CB35D4DD877C1A3856:CSXS\manifest.xml 52437DE708F4A7153DB0A0A5DB66658C2A12F3A8673054E87C7B84895D4CE584:host\index.jsx CC7DDC2DA053A7B922BC0DA7023F734BB80D6E00B87926715206D530664BC415:host\json.js D6F0B54C43673F18785AE31D4659289CCB08D9C79C1BBEF588ACBA8C9DBD96F4:host\JSX.js diff --git a/src/quadpype/hosts/photoshop/api/extension.zxp b/src/quadpype/hosts/photoshop/api/extension.zxp index 14fdd01218..71fbd7d11e 100644 Binary files a/src/quadpype/hosts/photoshop/api/extension.zxp and b/src/quadpype/hosts/photoshop/api/extension.zxp differ diff --git a/src/quadpype/hosts/photoshop/api/extension/CSXS/manifest.xml b/src/quadpype/hosts/photoshop/api/extension/CSXS/manifest.xml index dda2115061..280c829176 100644 --- a/src/quadpype/hosts/photoshop/api/extension/CSXS/manifest.xml +++ b/src/quadpype/hosts/photoshop/api/extension/CSXS/manifest.xml @@ -1,5 +1,5 @@ - + diff --git a/src/quadpype/hosts/photoshop/api/extension/client/client.js b/src/quadpype/hosts/photoshop/api/extension/client/client.js index 0a3515a927..6283ea8229 100644 --- a/src/quadpype/hosts/photoshop/api/extension/client/client.js +++ b/src/quadpype/hosts/photoshop/api/extension/client/client.js @@ -28,7 +28,7 @@ } /** main entry point **/ - startUp("WEBSOCKET_URL"); + startUp("QUADPYPE_WEBSOCKET_URL"); // get websocket server url from environment value async function startUp(url){ @@ -67,7 +67,7 @@ function main(websocket_url){ // creates connection to 'websocket_url', registers routes log.warn("websocket_url", websocket_url); - var default_url = 'ws://localhost:8099/ws/'; + var default_url = 'ws://localhost:8018/ws/'; if (websocket_url == ''){ websocket_url = default_url; diff --git a/src/quadpype/hosts/tvpaint/api/communication_server.py b/src/quadpype/hosts/tvpaint/api/communication_server.py index 427ca29ced..0d324df2cc 100644 --- a/src/quadpype/hosts/tvpaint/api/communication_server.py +++ b/src/quadpype/hosts/tvpaint/api/communication_server.py @@ -717,12 +717,12 @@ def launch(self, launch_args): self._create_routes() - os.environ["WEBSOCKET_URL"] = "ws://localhost:{}".format( + os.environ["QUADPYPE_WEBSOCKET_URL"] = "ws://localhost:{}".format( self.websocket_server.port ) log.info("Added request handler for url: {}".format( - os.environ["WEBSOCKET_URL"] + os.environ["QUADPYPE_WEBSOCKET_URL"] )) self._start_webserver() diff --git a/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md b/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md index 70a96b2919..ed7bfde228 100644 --- a/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md +++ b/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md @@ -7,7 +7,7 @@ This implementation is using TVPaint plugin (C/C++) which can communicate with p Current implementation is based on websocket protocol, using json-rpc communication (specification 2.0). Project is in beta stage, tested only on Windows. -To be able to load plugin, environment variable `WEBSOCKET_URL` must be set otherwise plugin won't load at all. Plugin should not affect TVPaint if python server crash, but buttons won't work. +To be able to load plugin, environment variable `QUADPYPE_WEBSOCKET_URL` must be set otherwise plugin won't load at all. Plugin should not affect TVPaint if python server crash, but buttons won't work. ## Requirements - Python server - python >= 3.6 diff --git a/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp b/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp index 5971432af9..eac4d3e5d5 100644 --- a/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp +++ b/src/quadpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp @@ -543,7 +543,7 @@ int FAR PASCAL PI_Open( PIFilter* iFilter ) { PI_Parameters( iFilter, NULL ); // NULL as iArg means "open the requester" } - char *env_value = std::getenv("WEBSOCKET_URL"); + char *env_value = std::getenv("QUADPYPE_WEBSOCKET_URL"); if (env_value != NULL) { communication = new Communicator(env_value); communication->connect(); diff --git a/src/quadpype/lib/dateutils.py b/src/quadpype/lib/dateutils.py index a75ad3072a..b00cdd012b 100644 --- a/src/quadpype/lib/dateutils.py +++ b/src/quadpype/lib/dateutils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Get configuration data.""" -import datetime +from datetime import datetime, timezone def get_datetime_data(datetime_obj=None): @@ -35,7 +35,7 @@ def get_datetime_data(datetime_obj=None): """ if not datetime_obj: - datetime_obj = datetime.datetime.now() + datetime_obj = datetime.now() year = datetime_obj.strftime("%Y") @@ -76,8 +76,8 @@ def get_datetime_data(datetime_obj=None): } -def get_timestamp_str(datetime_obj=None): - """Get standardized timestamp from datetime object. +def get_timestamp_str(datetime_obj=None, local_timezone=False): + """Get standardized timestamp string from a datetime object. Args: datetime_obj (datetime.datetime): Object of datetime. Current time @@ -85,11 +85,19 @@ def get_timestamp_str(datetime_obj=None): """ if datetime_obj is None: - datetime_obj = datetime.datetime.now() - return datetime_obj.astimezone().isoformat() + if local_timezone: + datetime_obj = datetime.now().astimezone() + else: + datetime_obj = datetime.now(timezone.utc) + + if datetime_obj.tzinfo is None: + # If no timezone info passed, we assume it's UTC + datetime_obj = datetime_obj.replace(tzinfo=timezone.utc) + return datetime_obj.isoformat() + def get_datetime_from_timestamp_str(timestamp_str): - return datetime.datetime.fromisoformat(timestamp_str) + return datetime.fromisoformat(timestamp_str) def get_formatted_current_time(): diff --git a/src/quadpype/modules/webserver/server.py b/src/quadpype/modules/webserver/server.py index b3a3bffe06..f7cda09e03 100644 --- a/src/quadpype/modules/webserver/server.py +++ b/src/quadpype/modules/webserver/server.py @@ -32,6 +32,12 @@ templates = Jinja2Templates(directory=Path(__file__).parent.joinpath("templates")) + +@WEB_API.post("/ping/", tags=["ping"]) +async def ping(): + return {"message": "pong"} + + @WEB_API.get('/favicon.ico', include_in_schema=False) async def favicon(): return FileResponse(get_app_favicon_filepath()) diff --git a/src/quadpype/tools/adobe_webserver/app.py b/src/quadpype/tools/adobe_webserver/app.py index 85265a5c86..51d0cf5fa1 100644 --- a/src/quadpype/tools/adobe_webserver/app.py +++ b/src/quadpype/tools/adobe_webserver/app.py @@ -39,13 +39,13 @@ def __init__(self): port = None host_name = "localhost" - websocket_url = os.getenv("WEBSOCKET_URL") + websocket_url = os.getenv("QUADPYPE_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 + port = 8016 # fallback self.port = port self.host_name = host_name @@ -76,7 +76,7 @@ async def send_context_change(self, host): but one already running, without this publish would point to old context. """ - client = WSRPCClient(os.getenv("WEBSOCKET_URL"), + client = WSRPCClient(os.getenv("QUADPYPE_WEBSOCKET_URL"), loop=asyncio.get_event_loop()) await client.connect() diff --git a/src/quadpype/version.py b/src/quadpype/version.py index cf221b3986..6c21500d38 100644 --- a/src/quadpype/version.py +++ b/src/quadpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """File declaring QuadPype version.""" -__version__ = "4.0.9" +__version__ = "4.0.10" diff --git a/src/tools/_lib/database/database_init.js b/src/tools/_lib/database/database_init.js new file mode 100644 index 0000000000..c028d6c142 --- /dev/null +++ b/src/tools/_lib/database/database_init.js @@ -0,0 +1,4 @@ +rs.initiate({ + _id: "rs0", + members: [{ _id: 0, host: "localhost:27017" }] +}); diff --git a/src/tools/_lib/database/docker-compose.yml b/src/tools/_lib/database/docker-compose.yml new file mode 100644 index 0000000000..a459d48a04 --- /dev/null +++ b/src/tools/_lib/database/docker-compose.yml @@ -0,0 +1,14 @@ +services: + mongodb: + image: mongo:latest + container_name: ${LOCAL_MONGO_CONTAINER_ID:-mongodb} + ports: + - "${LOCAL_MONGO_PORT:-27017}:27017" + command: ["--replSet", "rs0"] + volumes: + - ./database_init.js:/docker-entrypoint-initdb.d/database_init.js:ro + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/src/tools/_lib/run/pre_run.py b/src/tools/_lib/run/pre_run.py index 88672f7e27..2a2d6591e5 100644 --- a/src/tools/_lib/run/pre_run.py +++ b/src/tools/_lib/run/pre_run.py @@ -12,15 +12,21 @@ def main(): description='Mandatory script that will execute the platform dependant code before QuadPype run' ) parser.add_argument("-d", "--dev", action="store_true") + parser.add_argument("-p", "--prod", action="store_true") parser.add_argument("-m", "--mongo-uri", help='Format should be like: mongodb://uri.to.my.mongo-db:27017') args = parser.parse_args() - # Build the arguments string for the platform dependant script + # Build the argument string for the platform dependant script args_string = "" if args.dev: args_string += "-d" + if args.prod and not args.dev: + # If the used add the flags for dev and prod, + # we start in dev, to avoid potential mistakes or issues + args_string += "-p" + if args.mongo_uri: # First, check MongoDB connection string format validity match = re.fullmatch(r"^mongodb(\+srv)?://([\w.%-]+:[\w.%-]+@)?[\w.%-]+(:\d{1,5})?/?$", args.mongo_uri) diff --git a/src/tools/local_database_start.ps1 b/src/tools/local_database_start.ps1 index d385de3ca9..742787005c 100644 --- a/src/tools/local_database_start.ps1 +++ b/src/tools/local_database_start.ps1 @@ -17,16 +17,22 @@ if ($? -eq $true) { # Check if there is already a Docker container using the required port docker ps -a | findstr $QUADPYPE_MONGO_CONTAINER_PORT > $null if ($? -eq $true) { - write-output "Port 27017 is already used by Docker, operation aborted." + write-output "Port $QUADPYPE_MONGO_CONTAINER_PORT is already used by Docker, operation aborted." return 1 } # Check if there is already a process using the required port -$PORT_TCP_CONNECTION = Get-NetTCPConnection | Where-Object Localport -eq 27017 +$PORT_TCP_CONNECTION = Get-NetTCPConnection | Where-Object Localport -eq $QUADPYPE_MONGO_CONTAINER_PORT if ($PORT_TCP_CONNECTION) { $OWNING_PROCESS_NAME = (Get-Process -Id $PORT_TCP_CONNECTION.OwningProcess).Name - write-output "Port 27017 is already used by $OWNING_PROCESS_NAME, operation aborted." + write-output "Port $QUADPYPE_MONGO_CONTAINER_PORT is already used by $OWNING_PROCESS_NAME, operation aborted." return 1 } -docker run -p "${QUADPYPE_MONGO_CONTAINER_PORT}:${QUADPYPE_MONGO_CONTAINER_PORT}" --name $QUADPYPE_MONGO_CONTAINER_NAME -d mongo > $null +$env:LOCAL_MONGO_PORT = "${QUADPYPE_MONGO_CONTAINER_PORT}" +$env:LOCAL_MONGO_CONTAINER_ID = "${QUADPYPE_MONGO_CONTAINER_NAME}" + +$SCRIPT_DIR = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$DOCKER_COMPOSE_FILE_PATH = Join-Path -Path $SCRIPT_DIR -ChildPath "_lib\database\docker-compose.yml" + +docker-compose -f $DOCKER_COMPOSE_FILE_PATH up -d diff --git a/src/tools/local_database_transfer.ps1 b/src/tools/local_database_transfer.ps1 index 393fa70a9b..be484aa1bb 100644 --- a/src/tools/local_database_transfer.ps1 +++ b/src/tools/local_database_transfer.ps1 @@ -1,7 +1,7 @@ $SCRIPT_DIR = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent function migrate_settings() { - $MIGRATE_DB_SCRIPT_PATH = Join-Path -Path (Split-Path $SCRIPT_DIR -Parent) -ChildPath "tools\_lib\database\transfer_settings.js" + $MIGRATE_DB_SCRIPT_PATH = Join-Path -Path $SCRIPT_DIR -ChildPath "_lib\database\transfer_settings.js" $MIGRATE_VAL = mongosh --file $MIGRATE_DB_SCRIPT_PATH --eval "MONGO_URI='$MONGO_URI', MONGO_DESTINATION='$MONGO_DESTINATION'" --quiet return $MIGRATE_VAL } diff --git a/src/tools/pre_run.ps1 b/src/tools/pre_run.ps1 index 42beca5f84..9855471560 100644 --- a/src/tools/pre_run.ps1 +++ b/src/tools/pre_run.ps1 @@ -1,5 +1,6 @@ Param( [alias("d")][switch]$DEV=$false, + [alias("p")][switch]$PROD=$false, [alias("m")][string]$MONGO_URI="" ) @@ -11,8 +12,14 @@ while ((Split-Path $PATH_QUADPYPE_ROOT -Leaf) -ne "src") { $PATH_QUADPYPE_ROOT = (get-item $PATH_QUADPYPE_ROOT).Parent.FullName } -if ($MONGO_URI) { +if ($DEV) { + $QUADPYPE_MONGO = "mongodb://localhost:27017" +} elseif ($MONGO_URI) { $QUADPYPE_MONGO = $MONGO_URI +} elseif ($PROD) { + $QUADPYPE_MONGO = "IGNORED" +} else { + $QUADPYPE_MONGO = [System.Environment]::GetEnvironmentVariable("QUADPYPE_MONGO", [System.EnvironmentVariableTarget]::User) } if (($QUADPYPE_MONGO -eq "")) { @@ -23,6 +30,12 @@ if (($QUADPYPE_MONGO -eq "")) { exit 1 } +if (($QUADPYPE_MONGO -ne "IGNORED")) { + [System.Environment]::SetEnvironmentVariable("QUADPYPE_MONGO", $QUADPYPE_MONGO, [System.EnvironmentVariableTarget]::User) +} else { + [System.Environment]::SetEnvironmentVariable("QUADPYPE_MONGO", $null, [System.EnvironmentVariableTarget]::User) +} + [System.Environment]::SetEnvironmentVariable("QUADPYPE_ROOT", $PATH_QUADPYPE_ROOT, [System.EnvironmentVariableTarget]::User) [System.Environment]::SetEnvironmentVariable("PYENV_ROOT", (Resolve-Path -Path "~\.pyenv"), [System.EnvironmentVariableTarget]::User) @@ -34,7 +47,15 @@ if (Test-Path -Path $PATH_ADDITIONAL_ENV_FILE) { Remove-Item $PATH_ADDITIONAL_ENV_FILE -Force -ErrorAction SilentlyContinue | Out-Null } -New-Item "$($PATH_ADDITIONAL_ENV_FILE)" -ItemType File -Value "QUADPYPE_ROOT=$PATH_QUADPYPE_ROOT" | Out-Null +$ENV_FILE_CONTENT = "" + +if (($QUADPYPE_MONGO -ne "IGNORED")) { + $ENV_FILE_CONTENT = [string]::Concat($ENV_FILE_CONTENT,"QUADPYPE_MONGO=$QUADPYPE_MONGO$([Environment]::NewLine)") +} + +$ENV_FILE_CONTENT = [string]::Concat($ENV_FILE_CONTENT,"QUADPYPE_ROOT=$PATH_QUADPYPE_ROOT$([Environment]::NewLine)") + +New-Item "$($PATH_ADDITIONAL_ENV_FILE)" -ItemType File -Value "${ENV_FILE_CONTENT}" | Out-Null # Launch the activate script