OP-2414 - Hound

This commit is contained in:
Petr Kalis 2022-02-04 19:20:59 +01:00
parent 0611e60813
commit f29b8e7158
8 changed files with 9 additions and 831 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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