Files
animation/tools/md_bridge.py
T

147 lines
5.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python
"""md_bridge.py -- terminal client for the TinqsMDBridge Marvelous Designer plug-in.
Talks newline-delimited JSON over a localhost TCP socket to the TinqsMDBridge
plug-in (tools/md_bridge/TinqsMDBridge.py, registered manually via MD's
Plugin tab > Plug-in Manager > + ADD). Clicking Plugin > TinqsMDBridge starts
a BRIDGE SESSION: MD's UI freezes and the plug-in serves requests on the main
thread until --stop is sent or the idle timeout (default 300 s) expires. MD
cannot be used interactively during a session -- that's by design, see
docs/md-bridge.md.
Same wire protocol and `result` convention as tools/iclone_bridge.py, but on
port 18900 so both bridges can run at once.
Usage:
python tools/md_bridge.py --ping
python tools/md_bridge.py --exec "result = dir(pattern_api)"
python tools/md_bridge.py --file some_script.py
python tools/md_bridge.py --stop
python tools/md_bridge.py --port 18901 --timeout 120 --exec "..."
The exec namespace is persistent per MD session and pre-seeded with MD's api
modules (import_api, export_api, pattern_api, fabric_api, utility_api,
ApiTypes) plus a BRIDGE info dict. See docs/md-bridge.md.
"""
import argparse
import json
import socket
import sys
import time
DEFAULT_PORT = 18900
DEFAULT_TIMEOUT = 30.0
# Sent by --ping: bridge metadata seeded by the plug-in at startup.
PING_CODE = "result = BRIDGE\n"
def _send(sock, obj):
sock.sendall((json.dumps(obj) + "\n").encode("utf-8"))
def _recv_line(sock, timeout):
sock.settimeout(timeout)
buf = b""
while not buf.endswith(b"\n"):
chunk = sock.recv(4096)
if not chunk:
if buf:
break
raise ConnectionError(
"connection closed by the MD bridge before a response was received")
buf += chunk
return buf.decode("utf-8")
def run(code, port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT):
"""Send `code` to be exec()'d inside Marvelous Designer.
Returns the parsed response dict. Raises OSError on transport failure;
the dict's "ok" key reports exec-level success/failure.
"""
req = {"id": int(time.time() * 1000), "code": code}
with socket.create_connection(("127.0.0.1", port), timeout=timeout) as sock:
_send(sock, req)
line = _recv_line(sock, timeout)
return json.loads(line)
def ping(port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT):
"""Convenience wrapper: run(PING_CODE, ...)."""
return run(PING_CODE, port=port, timeout=timeout)
def _read_code(args):
if args.exec is not None:
return args.exec
with open(args.file, "r", encoding="utf-8") as f:
return f.read()
def _main(argv=None):
p = argparse.ArgumentParser(
description="Client for the TinqsMDBridge Marvelous Designer socket plug-in.")
p.add_argument("--port", type=int, default=DEFAULT_PORT,
help="bridge TCP port (default: %(default)s; must match "
"TINQS_MD_BRIDGE_PORT inside MD if overridden there)")
p.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT,
help="socket timeout in seconds (default: %(default)s; raise "
"for long Simulate() calls)")
group = p.add_mutually_exclusive_group(required=True)
group.add_argument("--ping", action="store_true",
help="check the bridge is alive; reports executor mode, "
"api modules, embedded python")
group.add_argument("--stop", action="store_true",
help="end the bridge session (unfreezes MD's UI)")
group.add_argument("--exec", metavar="CODE",
help="python source to exec() inside Marvelous Designer")
group.add_argument("--file", metavar="PATH",
help="path to a python file to exec() inside Marvelous Designer")
args = p.parse_args(argv)
try:
if args.ping:
resp = ping(port=args.port, timeout=args.timeout)
elif args.stop:
resp = run("__STOP__", port=args.port, timeout=args.timeout)
else:
resp = run(_read_code(args), port=args.port, timeout=args.timeout)
except ConnectionRefusedError:
print(
"error: connection refused on 127.0.0.1:{} -- is Marvelous Designer "
"running? was Plugin > TinqsMDBridge clicked this session?".format(args.port),
file=sys.stderr,
)
return 1
except OSError as exc:
print("error: could not reach the MD bridge on 127.0.0.1:{}: {}".format(
args.port, exc), file=sys.stderr)
return 1
if resp.get("stdout"):
text = resp["stdout"]
print(text, end="" if text.endswith("\n") else "\n")
if resp.get("ok"):
if args.ping:
info = resp.get("result") or {}
py = str(info.get("python", "?")).split()[0]
print("ok: MD bridge mode={} qt={} python {} api={}".format(
info.get("mode", "?"), info.get("qt_binding"), py,
",".join(info.get("api_modules", []))))
if info.get("api_missing"):
print("warning: missing api modules: {}".format(
",".join(info["api_missing"])))
else:
print(json.dumps(resp.get("result"), indent=2))
return 0
print("error: {}".format(resp.get("error", "unknown error")), file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(_main())