From f29b8e71589b15b0cfefa59b7f9c29b6c95f7c7a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Feb 2022 19:20:59 +0100 Subject: [PATCH] OP-2414 - Hound --- openpype/hosts/harmony/api/TB_sceneOpened.js | 4 +- openpype/hosts/harmony/api/lib.py | 2 +- openpype/hosts/harmony/api/pipeline.py | 16 +- .../hosts/harmony/api/toonboom/__init__.py | 32 -- openpype/hosts/harmony/api/toonboom/avalon.js | 262 ------------- openpype/hosts/harmony/api/toonboom/lib.py | 363 ------------------ openpype/hosts/harmony/api/toonboom/server.py | 159 -------- openpype/tools/stdout_broker/app.py | 2 +- 8 files changed, 9 insertions(+), 831 deletions(-) delete mode 100644 openpype/hosts/harmony/api/toonboom/__init__.py delete mode 100644 openpype/hosts/harmony/api/toonboom/avalon.js delete mode 100644 openpype/hosts/harmony/api/toonboom/lib.py delete mode 100644 openpype/hosts/harmony/api/toonboom/server.py diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 028a12b654..5a3fe9ce82 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -8,7 +8,7 @@ This script implements client communication with Avalon server to bridge gap between Python and QtScript. */ - +/* jshint proto: true */ var LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH'); LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH + '/openHarmony.js'; LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH.replace(/\\/g, "/"); @@ -356,7 +356,7 @@ function start() { app.avalonMenu = null; for (var i = 0 ; i < actions.length; i++) { - label = System.getenv('AVALON_LABEL') + label = System.getenv('AVALON_LABEL'); if (actions[i].text == label) { app.avalonMenu = true; } diff --git a/openpype/hosts/harmony/api/lib.py b/openpype/hosts/harmony/api/lib.py index 088a4e3d3a..4fee7ab07d 100644 --- a/openpype/hosts/harmony/api/lib.py +++ b/openpype/hosts/harmony/api/lib.py @@ -489,7 +489,7 @@ def send(request): def select_nodes(nodes): """ Selects nodes in Node View """ - selected_nodes = self.send( + _ = self.send( { "function": "AvalonHarmony.selectNodes", "args": nodes diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index fc93d18ff5..306f270410 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -149,6 +149,7 @@ def application_launch(): # send scripts to Harmony harmony.send({"script": pype_harmony_js}) harmony.send({"script": script}) + inject_avalon_js() def export_template(backdrops, nodes, filepath): @@ -218,16 +219,6 @@ def inject_avalon_js(): harmony.send({"script": script}) -def install(): - """Install Harmony-specific functionality of avalon-core. - - This function is called automatically on calling `api.install(harmony)`. - """ - print("Installing Avalon Harmony...") - pyblish.api.register_host("harmony") - avalon.api.on("application.launched", inject_avalon_js) - - def ls(): """Yields containers from Harmony scene. @@ -325,7 +316,7 @@ def containerise(name, context, loader=None, suffix=None, - nodes=[]): + nodes=None): """Imprint node with metadata. Containerisation enables a tracking of version, author and origin @@ -342,6 +333,9 @@ def containerise(name, Returns: container (str): Path of container assembly. """ + if not nodes: + nodes = [] + data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, diff --git a/openpype/hosts/harmony/api/toonboom/__init__.py b/openpype/hosts/harmony/api/toonboom/__init__.py deleted file mode 100644 index feb810c878..0000000000 --- a/openpype/hosts/harmony/api/toonboom/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -from .lib import ( - launch, - on_file_changed, - open_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root, - save_file, - send, - show, - save_scene, - setup_startup_scripts, - check_libs -) - -__all__ = [ - # Library API. - "launch", - "on_file_changed", - "open_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root", - "save_file", - "send", - "show", - "save_scene", - "setup_startup_scripts", - "check_libs" -] diff --git a/openpype/hosts/harmony/api/toonboom/avalon.js b/openpype/hosts/harmony/api/toonboom/avalon.js deleted file mode 100644 index ad65ad94ee..0000000000 --- a/openpype/hosts/harmony/api/toonboom/avalon.js +++ /dev/null @@ -1,262 +0,0 @@ -function Client() -{ - var self = this; - self.socket = new QTcpSocket(this); - self.received = ""; - - self.log_debug = function(data) - { - message = typeof(data.message) != "undefined" ? data.message : data; - MessageLog.trace("(DEBUG): " + message.toString()); - }; - - - self.log_info = function(data) - { - message = typeof(data.message) != "undefined" ? data.message : data; - MessageLog.trace("(INFO): " + message.toString()); - }; - - - self.log_warning = function(data) - { - message = typeof(data.message) != "undefined" ? data.message : data; - MessageLog.trace("(WARNING): " + message.toString()); - }; - - - self.log_error = function(data) - { - message = typeof(data.message) != "undefined" ? data.message : data; - MessageLog.trace("(ERROR): " + message.toString()); - }; - - self.process_request = function(request) - { - self.log_debug("Processing: " + JSON.stringify(request)); - var result = null; - - if (request["function"] != null) - { - try - { - var func = eval(request["function"]); - - if (request.args == null) - { - result = func(); - }else - { - result = func(request.args); - } - } - - catch (error) - { - result = "Error processing request.\nRequest:\n" + JSON.stringify(request) + "\nError:\n" + error; - } - } - - return result; - }; - - self.on_ready_read = function() - { - self.log_debug("Receiving data..."); - data = self.socket.readAll(); - - if (data.size() != 0) - { - for ( var i = 0; i < data.size(); ++i) - { - self.received = self.received.concat(String.fromCharCode(data.at(i))); - } - } - - self.log_debug("Received: " + self.received); - - request = JSON.parse(self.received); - self.log_debug("Request: " + JSON.stringify(request)); - - request.result = self.process_request(request); - - if (!request.reply) - { - request.reply = true; - self._send(JSON.stringify(request)); - } - - self.received = ""; - }; - - self.on_connected = function() - { - self.log_debug("Connected to server."); - self.socket.readyRead.connect(self.on_ready_read); - }; - - self._send = function(message) - { - self.log_debug("Sending: " + message); - - var data = new QByteArray(); - outstr = new QDataStream(data, QIODevice.WriteOnly); - outstr.writeInt(0); - data.append("UTF-8"); - outstr.device().seek(0); - outstr.writeInt(data.size() - 4); - var codec = QTextCodec.codecForUtfText(data); - self.socket.write(codec.fromUnicode(message)); - }; - - self.send = function(request, wait) - { - self._send(JSON.stringify(request)); - - while (wait) - { - try - { - JSON.parse(self.received); - break; - } - catch(err) - { - self.socket.waitForReadyRead(5000); - } - } - - self.received = ""; - }; - - self.on_disconnected = function() - { - self.socket.close(); - }; - - self.disconnect = function() - { - self.socket.close(); - }; - - self.socket.connected.connect(self.on_connected); - self.socket.disconnected.connect(self.on_disconnected); -} - -function start() -{ - var self = this; - var host = "127.0.0.1"; - var port = parseInt(System.getenv("AVALON_TOONBOOM_PORT")); - - // Attach the client to the QApplication to preserve. - var app = QCoreApplication.instance(); - - if (app.avalon_client == null) - { - app.avalon_client = new Client(); - app.avalon_client.socket.connectToHost(host, port); - } - - var menu_bar = QApplication.activeWindow().menuBar(); - var menu = menu_bar.addMenu("Avalon"); - - self.on_creator = function() - { - app.avalon_client.send( - { - "module": "avalon.toonboom", - "method": "show", - "args": ["creator"] - }, - false - ); - }; - var action = menu.addAction("Create..."); - action.triggered.connect(self.on_creator); - - self.on_workfiles = function() - { - app.avalon_client.send( - { - "module": "avalon.toonboom", - "method": "show", - "args": ["workfiles"] - }, - false - ); - }; - action = menu.addAction("Workfiles"); - action.triggered.connect(self.on_workfiles); - - self.on_load = function() - { - app.avalon_client.send( - { - "module": "avalon.toonboom", - "method": "show", - "args": ["loader"] - }, - false - ); - }; - action = menu.addAction("Load..."); - action.triggered.connect(self.on_load); - - self.on_publish = function() - { - app.avalon_client.send( - { - "module": "avalon.toonboom", - "method": "show", - "args": ["publish"] - }, - false - ); - }; - action = menu.addAction("Publish..."); - action.triggered.connect(self.on_publish); - - self.on_manage = function() - { - app.avalon_client.send( - { - "module": "avalon.toonboom", - "method": "show", - "args": ["sceneinventory"] - }, - false - ); - }; - action = menu.addAction("Manage..."); - action.triggered.connect(self.on_manage); - - // Watch scene file for changes. - app.on_file_changed = function(path) - { - var app = QCoreApplication.instance(); - if (app.avalon_on_file_changed){ - app.avalon_client.send( - { - "module": "avalon.toonboom", - "method": "on_file_changed", - "args": [path] - }, - false - ); - } - - app.watcher.addPath(path); - }; - - app.watcher = new QFileSystemWatcher(); - extension = ".xstage"; - var product_name = about.productName(); - if (product_name.toLowerCase().indexOf("storyboard") !== -1){ - extension = ".sboard"; - } - scene_path = scene.currentProjectPath() + "/" + scene.currentVersionName() + extension; - app.watcher.addPath(scene_path); - app.watcher.fileChanged.connect(app.on_file_changed); - app.avalon_on_file_changed = true; -} diff --git a/openpype/hosts/harmony/api/toonboom/lib.py b/openpype/hosts/harmony/api/toonboom/lib.py deleted file mode 100644 index e590c61f3c..0000000000 --- a/openpype/hosts/harmony/api/toonboom/lib.py +++ /dev/null @@ -1,363 +0,0 @@ -import os -import random -import sys -import queue -import shutil -import zipfile -import signal -import threading -import subprocess -import importlib -import logging -import filecmp -from uuid import uuid4 - -from .server import Server -from openpype.tools.utils import host_tools -from Qt import QtWidgets - -self = sys.modules[__name__] -self.server = None -self.pid = None -self.application_path = None -self.callback_queue = None -self.workfile_path = None -self.port = None -self.extension = None -self.application_name = None - -# Setup logging. -self.log = logging.getLogger(__name__) -self.log.setLevel(logging.DEBUG) - -signature = str(uuid4()).replace("-", "_") - - -def execute_in_main_thread(func_to_call_from_main_thread): - self.callback_queue.put(func_to_call_from_main_thread) - - -def main_thread_listen(): - callback = self.callback_queue.get() - callback() - - -def setup_startup_scripts(): - """Manages installation of avalon's TB_sceneOpened.js for Harmony launch. - - If a studio already has defined "TOONBOOM_GLOBAL_SCRIPT_LOCATION", copies - the TB_sceneOpened.js to that location if the file is different. - Otherwise, will set the env var to point to the avalon/harmony folder. - - Admins should be aware that this will overwrite TB_sceneOpened in the - "TOONBOOM_GLOBAL_SCRIPT_LOCATION", and that if they want to have additional - logic, they will need to one of the following: - * Create a Harmony package to manage startup logic - * Use TB_sceneOpenedUI.js instead to manage startup logic - * Add their startup logic to avalon/harmony/TB_sceneOpened.js - """ - avalon_dcc_dir = os.path.dirname(os.path.dirname(__file__)) - startup_js = "TB_sceneOpened.js" - - if os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"): - - avalon_harmony_startup = os.path.join(avalon_dcc_dir, startup_js) - - env_harmony_startup = os.path.join( - os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"), startup_js) - - if not filecmp.cmp(avalon_harmony_startup, env_harmony_startup): - try: - shutil.copy(avalon_harmony_startup, env_harmony_startup) - except Exception as e: - self.log.error(e) - self.log.warning( - "Failed to copy {0} to {1}! " - "Defaulting to Avalon TOONBOOM_GLOBAL_SCRIPT_LOCATION." - .format(avalon_harmony_startup, env_harmony_startup)) - - os.environ["TOONBOOM_GLOBAL_SCRIPT_LOCATION"] = avalon_dcc_dir - else: - os.environ["TOONBOOM_GLOBAL_SCRIPT_LOCATION"] = avalon_dcc_dir - - -def check_libs(): - """Check if `OpenHarmony`_ is available. - - Avalon expects either path in `LIB_OPENHARMONY_PATH` or `openHarmony.js` - present in `TOONBOOM_GLOBAL_SCRIPT_LOCATION`. - - Throws: - RuntimeError: If openHarmony is not found. - - .. _OpenHarmony: - https://github.com/cfourney/OpenHarmony - - """ - if not os.getenv("LIB_OPENHARMONY_PATH"): - - if os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"): - if os.path.exists( - os.path.join( - os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"), - "openHarmony.js")): - - os.environ["LIB_OPENHARMONY_PATH"] = \ - os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION") - return - - else: - self.log.error(("Cannot find OpenHarmony library. " - "Please set path to it in LIB_OPENHARMONY_PATH " - "environment variable.")) - raise RuntimeError("Missing OpenHarmony library.") - - -def launch(application_path, zip_file): - """Setup for Toon Boom application launch. - - Launches Toon Boom application and the server, then starts listening on the - main thread for callbacks from the server. This is to have Qt applications - run in the main thread. - - Args: - application_path (str): Path to application executable. - zip_file (str): Path to application scene file zipped. - application_name (str): Application identifier. - """ - self.port = random.randrange(5000, 6000) - os.environ["AVALON_TOONBOOM_PORT"] = str(self.port) - self.application_path = application_path - - self.application_name = "harmony" - if "storyboard" in application_path.lower(): - self.application_name = "storyboardpro" - - extension_mapping = {"harmony": "xstage", "storyboardpro": "sboard"} - self.extension = extension_mapping[self.application_name] - - # Launch Harmony. - setup_startup_scripts() - - if os.environ.get("AVALON_TOONBOOM_WORKFILES_ON_LAUNCH", False): - host_tools.show_workfiles(save=False) - - # No launch through Workfiles happened. - if not self.workfile_path: - launch_zip_file(zip_file) - - self.callback_queue = queue.Queue() - while True: - main_thread_listen() - - -def get_local_path(filepath): - """From the provided path get the equivalent local path.""" - basename = os.path.splitext(os.path.basename(filepath))[0] - harmony_path = os.path.join( - os.path.expanduser("~"), ".avalon", self.application_name - ) - return os.path.join(harmony_path, basename) - - -def launch_zip_file(filepath): - """Launch a Harmony application instance with the provided zip file.""" - self.log.debug("Localizing {}".format(filepath)) - - local_path = get_local_path(filepath) - scene_path = os.path.join( - local_path, os.path.basename(local_path) + "." + self.extension - ) - extract_zip_file = False - if os.path.exists(scene_path): - # Check remote scene is newer than local. - if os.path.getmtime(scene_path) < os.path.getmtime(filepath): - shutil.rmtree(local_path) - extract_zip_file = True - else: - extract_zip_file = True - - if extract_zip_file: - with zipfile.ZipFile(filepath, "r") as zip_ref: - zip_ref.extractall(local_path) - - # Close existing scene. - if self.pid: - os.kill(self.pid, signal.SIGTERM) - - # Stop server. - if self.server: - self.server.stop() - - # Launch Avalon server. - self.server = Server(self.port) - thread = threading.Thread(target=self.server.start) - thread.daemon = True - thread.start() - - # Save workfile path for later. - self.workfile_path = filepath - - self.log.debug("Launching {}".format(scene_path)) - process = subprocess.Popen([self.application_path, scene_path]) - self.pid = process.pid - - -def file_extensions(): - return [".zip"] - - -def has_unsaved_changes(): - if self.server: - return self.server.send({"function": "scene.isDirty"})["result"] - - return False - - -def save_file(filepath): - temp_path = self.get_local_path(filepath) - - if os.path.exists(temp_path): - shutil.rmtree(temp_path) - - self.server.send( - {"function": "scene.saveAs", "args": [temp_path]} - )["result"] - - zip_and_move(temp_path, filepath) - - self.workfile_path = filepath - - func = """function add_path(path) - { - var app = QCoreApplication.instance(); - app.watcher.addPath(path); - } - add_path - """ - - scene_path = os.path.join( - temp_path, os.path.basename(temp_path) + "." + self.extension - ) - self.server.send( - {"function": func, "args": [scene_path]} - ) - - -def open_file(filepath): - launch_zip_file(filepath) - - -def current_file(): - """Returning None to make Workfiles app look at first file extension.""" - return None - - -def work_root(session): - return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") - - -def zip_and_move(source, destination): - """Zip a directory and move to `destination` - - Args: - - source (str): Directory to zip and move to destination. - - destination (str): Destination file path to zip file. - """ - os.chdir(os.path.dirname(source)) - shutil.make_archive(os.path.basename(source), "zip", source) - shutil.move(os.path.basename(source) + ".zip", destination) - self.log.debug("Saved \"{}\" to \"{}\"".format(source, destination)) - - -def on_file_changed(path): - """Threaded zipping and move of the project directory. - - This method is called when the scene file is changed. - """ - - self.log.debug("File changed: " + path) - - if self.workfile_path is None: - return - - thread = threading.Thread( - target=zip_and_move, args=(os.path.dirname(path), self.workfile_path) - ) - thread.start() - - -def send(request): - """Public method for sending requests to Toon Boom application.""" - return self.server.send(request) - - -def show(module_name): - """Call show on "module_name". - - This allows to make a QApplication ahead of time and always "exec_" to - prevent crashing. - - Args: - module_name (str): Name of module to call "show" on. - """ - # Need to have an existing QApplication. - app = QtWidgets.QApplication.instance() - if not app: - app = QtWidgets.QApplication(sys.argv) - - # Get tool name from module name - # TODO this is for backwards compatibility not sure if `avalon.js` - # is automatically updated. - # Previous javascript sent 'module_name' which contained whole tool import - # string e.g. "avalon.tools.workfiles" now it should be only "workfiles" - tool_name = module_name.split(".")[-1] - if tool_name == "publish": - host_tools.show_tool_by_name(tool_name) - return - - # Get and show tool. - # TODO convert toonboom implementation to run in Qt application as main - # thread - tool_window = host_tools.get_tool_by_name(tool_name) - tool_window.show() - - # QApplication needs to always execute, except when publishing. - app.exec_() - - -def save_scene(): - """Saves the Toon Boom scene safely. - - The built-in (to Avalon) background zip and moving of the Harmony scene - folder, interfers with server/client communication by sending two requests - at the same time. This only happens when sending "scene.saveAll()". This - method prevents this double request and safely saves the scene. - """ - # Need to turn off the backgound watcher else the communication with - # the server gets spammed with two requests at the same time. - func = """function %s_func() - { - var app = QCoreApplication.instance(); - app.avalon_on_file_changed = false; - scene.saveAll(); - return ( - scene.currentProjectPath() + "/" + scene.currentVersionName() - ); - } - %s_func - """ % (signature, signature) - scene_path = self.send({"function": func})["result"] + "." + self.extension - - # Manually update the remote file. - self.on_file_changed(scene_path) - - # Re-enable the background watcher. - func = """function %s_func() - { - var app = QCoreApplication.instance(); - app.avalon_on_file_changed = true; - } - %s_func - """ % (signature, signature) - self.send({"function": func}) diff --git a/openpype/hosts/harmony/api/toonboom/server.py b/openpype/hosts/harmony/api/toonboom/server.py deleted file mode 100644 index 3e2d529f71..0000000000 --- a/openpype/hosts/harmony/api/toonboom/server.py +++ /dev/null @@ -1,159 +0,0 @@ -import socket -import logging -import json -import traceback -import importlib -import functools - -from . import lib - - -class Server(object): - - def __init__(self, port): - self.connection = None - self.received = "" - self.port = port - - # Setup logging. - self.log = logging.getLogger(__name__) - self.log.setLevel(logging.DEBUG) - - # Create a TCP/IP socket - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # Bind the socket to the port - server_address = ("localhost", port) - self.log.debug("Starting up on {}".format(server_address)) - self.socket.bind(server_address) - - # Listen for incoming connections - self.socket.listen(1) - - def process_request(self, request): - """ - Args: - request (dict): { - "module": (str), # Module of method. - "method" (str), # Name of method in module. - "args" (list), # Arguments to pass to method. - "kwargs" (dict), # Keywork arguments to pass to method. - "reply" (bool), # Optional wait for method completion. - } - """ - self.log.debug("Processing request: {}".format(request)) - - try: - module = importlib.import_module(request["module"]) - method = getattr(module, request["method"]) - - args = request.get("args", []) - kwargs = request.get("kwargs", {}) - partial_method = functools.partial(method, *args, **kwargs) - - lib.execute_in_main_thread(partial_method) - except Exception: - self.log.error(traceback.format_exc()) - - def receive(self): - """Receives data from `self.connection`. - - When the data is a json serializable string, a reply is sent then - processing of the request. - """ - - while True: - # Receive the data in small chunks and retransmit it - request = None - while True: - if self.connection is None: - break - - data = self.connection.recv(4096) - if data: - self.received += data.decode("utf-8") - else: - break - - self.log.debug("Received: {}".format(self.received)) - - try: - request = json.loads(self.received) - break - except json.decoder.JSONDecodeError: - pass - - if request is None: - break - - self.received = "" - - self.log.debug("Request: {}".format(request)) - if "reply" not in request.keys(): - request["reply"] = True - self._send(json.dumps(request)) - - self.process_request(request) - - def start(self): - """Entry method for server. - - Waits for a connection on `self.port` before going into listen mode. - """ - - # Wait for a connection - self.log.debug("Waiting for a connection.") - self.connection, client_address = self.socket.accept() - - self.log.debug("Connection from: {}".format(client_address)) - - self.receive() - - def stop(self): - self.log.debug("Shutting down server.") - if self.connection is None: - self.log.debug("Connect to shutdown.") - socket.socket( - socket.AF_INET, socket.SOCK_STREAM - ).connect(("localhost", self.port)) - - self.connection.close() - self.connection = None - - self.socket.close() - - def _send(self, message): - """Send a message to Harmony. - - Args: - message (str): Data to send to Harmony. - """ - - # Wait for a connection. - while not self.connection: - pass - - self.log.debug("Sending: {}".format(message)) - self.connection.sendall(message.encode("utf-8")) - - def send(self, request): - """Send a request in dictionary to Harmony. - - Waits for a reply from Harmony. - - Args: - request (dict): Data to send to Harmony. - """ - self._send(json.dumps(request)) - - result = None - while True: - try: - result = json.loads(self.received) - break - except json.decoder.JSONDecodeError: - pass - - self.received = "" - - return result diff --git a/openpype/tools/stdout_broker/app.py b/openpype/tools/stdout_broker/app.py index cfc05b0cc3..b271d8f6d9 100644 --- a/openpype/tools/stdout_broker/app.py +++ b/openpype/tools/stdout_broker/app.py @@ -179,7 +179,7 @@ class StdOutBroker: self.original_stderr_write(text) if self.send_to_tray: self.log_queue.append(text) - + def _process_queue(self): """Sends lines and purges queue""" if not self.send_to_tray: