v0.2: Working bridge — temp-file output, eval with stdout capture
- Changed from exec(open()) to temp-file output pattern (/tmp/hhb_out_*.txt) - eval command now captures print() via io.StringIO redirect - get-selected, get-param, get-inputs: working with temp-file output - Verified live on Houdini 21.0.729 (Fedora, Wayland) - Known limitation: python /tmp/script.py via hcommand does not capture print(); workaround is writing to /tmp/hhb_out_*.txt
This commit is contained in:
+116
-72
@@ -2,14 +2,13 @@
|
|||||||
# ──────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────
|
||||||
# hhb — Hermes-Houdini Bridge Client
|
# hhb — Hermes-Houdini Bridge Client
|
||||||
# Sends Python/hou commands to a running Houdini session
|
# Sends Python/hou commands to a running Houdini session
|
||||||
# via SSH + hcommand.
|
# via SSH + hcommand. Results returned via temp file.
|
||||||
# ──────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HOST="${HHB_HOST:-niklas@192.168.178.39}"
|
HOST="${HHB_HOST:-niklas@192.168.178.39}"
|
||||||
PORT="${HHB_PORT:-12345}"
|
PORT="${HHB_PORT:-12345}"
|
||||||
HCOMMAND="${HHB_HCOMMAND:-/opt/hfs21.0/bin/hcommand}"
|
HCOMMAND="${HHB_HCOMMAND:-/opt/hfs21.0/bin/hcommand}"
|
||||||
WORKSPACE="${HHB_WORKSPACE:-/opt/data/projects/hermes-houdini-bridge}"
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
@@ -17,49 +16,51 @@ Usage: hhb <command> [args]
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
eval <python_code> Execute Python code in the running Houdini session.
|
eval <python_code> Execute Python code in the running Houdini session.
|
||||||
Example: hhb eval "print(hou.selectedNodes())"
|
|
||||||
|
|
||||||
script <file.py> Send a Python script file to Houdini and execute it.
|
script <file.py> Send a Python script file to Houdini and execute it.
|
||||||
Example: hhb script ./examples/create_pyro_setup.py
|
|
||||||
|
|
||||||
get-selected Print paths and types of selected nodes.
|
get-selected Print paths and types of selected nodes.
|
||||||
get-param <parm> Get value of a parameter on the first selected node.
|
get-param <parm> Get value of a parameter on the first selected node.
|
||||||
set-param <parm> <val> Set a parameter on the first selected node.
|
set-param <parm> <val> Set a parameter on the first selected node.
|
||||||
get-inputs Print input connections of the first selected node.
|
get-inputs Print input connections of the first selected node.
|
||||||
status Check if the bridge port is open.
|
status Check if the bridge port is open.
|
||||||
help Show this help.
|
help Show this help.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Helpers ──────────────────────────────────────────────
|
# ── Core: write Python script, execute, read back output ─
|
||||||
|
|
||||||
run_remote_python() {
|
run_houdini_python() {
|
||||||
# Write Python code to a temp file, execute via hcommand.
|
# $1: Python code (as a string)
|
||||||
# This avoids quoting hell with complex Python snippets.
|
# Writes code to /tmp/hhb_script.py, executes via hcommand,
|
||||||
|
# reads output from /tmp/hhb_out.txt
|
||||||
local python_code="$1"
|
local python_code="$1"
|
||||||
local tmpfile="/tmp/hhb_$$.py"
|
local sid="$$"
|
||||||
|
|
||||||
# Upload script
|
# Upload the script
|
||||||
printf '%s' "$python_code" | ssh "$HOST" "cat > $tmpfile"
|
printf '%s' "$python_code" | ssh "$HOST" "cat > /tmp/hhb_script_${sid}.py"
|
||||||
|
|
||||||
# Execute via hscript's python command — reads file and execs
|
# Execute via hcommand
|
||||||
local output
|
ssh "$HOST" "$HCOMMAND $PORT 'python /tmp/hhb_script_${sid}.py'" 2>/dev/null || true
|
||||||
output=$(ssh "$HOST" \
|
|
||||||
"$HCOMMAND $PORT \"python exec(open('$tmpfile').read())\"" 2>&1) || {
|
# Read output file
|
||||||
local rc=$?
|
ssh "$HOST" "cat /tmp/hhb_out_${sid}.txt 2>/dev/null" || echo ""
|
||||||
ssh "$HOST" "rm -f $tmpfile" 2>/dev/null
|
|
||||||
echo "hhb: hcommand failed (exit $rc)" >&2
|
|
||||||
echo "$output" >&2
|
|
||||||
return $rc
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
ssh "$HOST" "rm -f $tmpfile" 2>/dev/null
|
ssh "$HOST" "rm -f /tmp/hhb_script_${sid}.py /tmp/hhb_out_${sid}.txt" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
# hcommand output has a prefix line like "-> foobar", strip it
|
# Wrap user code to capture output via temp file
|
||||||
echo "$output" | sed 's/^->\s*//'
|
wrap_python() {
|
||||||
|
local code="$1"
|
||||||
|
local sid="$$"
|
||||||
|
cat <<PYEOF
|
||||||
|
import hou, json, sys
|
||||||
|
try:
|
||||||
|
${code}
|
||||||
|
except Exception as e:
|
||||||
|
with open('/tmp/hhb_out_${sid}.txt', 'w') as f:
|
||||||
|
f.write(json.dumps({"error": str(e)}))
|
||||||
|
PYEOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Commands ──────────────────────────────────────────────
|
# ── Commands ──────────────────────────────────────────────
|
||||||
@@ -75,94 +76,137 @@ cmd_status() {
|
|||||||
|
|
||||||
cmd_eval() {
|
cmd_eval() {
|
||||||
[[ $# -ge 1 ]] || { echo "Usage: hhb eval <python_code>"; exit 1; }
|
[[ $# -ge 1 ]] || { echo "Usage: hhb eval <python_code>"; exit 1; }
|
||||||
run_remote_python "$*"
|
local sid="$$"
|
||||||
|
local code
|
||||||
|
code=$(cat <<PYEOF
|
||||||
|
import hou, json, sys, io
|
||||||
|
_stdout = io.StringIO()
|
||||||
|
sys.stdout = _stdout
|
||||||
|
$*
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
with open('/tmp/hhb_out_${sid}.txt', 'w') as f:
|
||||||
|
f.write(_stdout.getvalue())
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
run_houdini_python "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_script() {
|
cmd_script() {
|
||||||
[[ $# -ge 1 ]] || { echo "Usage: hhb script <file.py>"; exit 1; }
|
[[ $# -ge 1 ]] || { echo "Usage: hhb script <file.py>"; exit 1; }
|
||||||
local script_path="$1"
|
local script_path="$1"
|
||||||
|
local sid="$$"
|
||||||
[[ -f "$script_path" ]] || { echo "File not found: $script_path"; exit 1; }
|
[[ -f "$script_path" ]] || { echo "File not found: $script_path"; exit 1; }
|
||||||
|
|
||||||
scp -q "$script_path" "$HOST:/tmp/hhb_script_$$.py"
|
# Read the script, wrap in output capture
|
||||||
local output
|
local code
|
||||||
output=$(ssh "$HOST" \
|
code=$(cat "$script_path")
|
||||||
"$HCOMMAND $PORT \"python exec(open('/tmp/hhb_script_$$.py').read())\"" 2>&1) || {
|
local wrapped
|
||||||
local rc=$?
|
wrapped=$(cat <<PYEOF
|
||||||
ssh "$HOST" "rm -f /tmp/hhb_script_$$.py" 2>/dev/null
|
import hou, json
|
||||||
echo "hhb: hcommand failed (exit $rc)" >&2
|
try:
|
||||||
echo "$output" >&2
|
${code}
|
||||||
return $rc
|
except Exception as e:
|
||||||
}
|
with open('/tmp/hhb_out_${sid}.txt', 'w') as f:
|
||||||
ssh "$HOST" "rm -f /tmp/hhb_script_$$.py" 2>/dev/null
|
f.write(json.dumps({"error": str(e)}))
|
||||||
echo "$output" | sed 's/^->\s*//'
|
PYEOF
|
||||||
|
)
|
||||||
|
run_houdini_python "$wrapped"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_get_selected() {
|
cmd_get_selected() {
|
||||||
run_remote_python '
|
local sid="$$"
|
||||||
import hou
|
local code
|
||||||
|
code=$(cat <<'PYEOF'
|
||||||
nodes = hou.selectedNodes()
|
nodes = hou.selectedNodes()
|
||||||
|
lines = []
|
||||||
if not nodes:
|
if not nodes:
|
||||||
print("No nodes selected.")
|
lines.append("No nodes selected.")
|
||||||
else:
|
else:
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
print(f"{n.path():50s} [{n.type().name()}]")
|
lines.append(f"{n.path():50s} [{n.type().name()}]")
|
||||||
'
|
with open('/tmp/hhb_out_PLACEHOLDER.txt', 'w') as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
code="${code//PLACEHOLDER/$sid}"
|
||||||
|
run_houdini_python "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_get_param() {
|
cmd_get_param() {
|
||||||
[[ $# -ge 1 ]] || { echo "Usage: hhb get-param <parm_name>"; exit 1; }
|
[[ $# -ge 1 ]] || { echo "Usage: hhb get-param <parm_name>"; exit 1; }
|
||||||
local parm="$1"
|
local parm="$1"
|
||||||
run_remote_python "
|
local sid="$$"
|
||||||
import hou
|
local code
|
||||||
|
code=$(cat <<PYEOF
|
||||||
nodes = hou.selectedNodes()
|
nodes = hou.selectedNodes()
|
||||||
if not nodes:
|
if not nodes:
|
||||||
print('No nodes selected.')
|
msg = "No nodes selected."
|
||||||
else:
|
else:
|
||||||
p = nodes[0].parm('$parm')
|
p = nodes[0].parm("PARM_PLACEHOLDER")
|
||||||
if p is None:
|
if p is None:
|
||||||
print(f'Parameter \"$parm\" not found on {nodes[0].path()}')
|
msg = f'Parameter "PARM_PLACEHOLDER" not found on {nodes[0].path()}'
|
||||||
else:
|
else:
|
||||||
print(f'{nodes[0].path()}.{p.name()} = {p.eval()}')
|
msg = f'{nodes[0].path()}.{p.name()} = {p.eval()}'
|
||||||
"
|
with open('/tmp/hhb_out_PLACEHOLDER_SID.txt', 'w') as f:
|
||||||
|
f.write(msg)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
code="${code//PARM_PLACEHOLDER/$parm}"
|
||||||
|
code="${code//PLACEHOLDER_SID/$sid}"
|
||||||
|
run_houdini_python "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_set_param() {
|
cmd_set_param() {
|
||||||
[[ $# -ge 2 ]] || { echo "Usage: hhb set-param <parm_name> <value>"; exit 1; }
|
[[ $# -ge 2 ]] || { echo "Usage: hhb set-param <parm_name> <value>"; exit 1; }
|
||||||
local parm="$1"
|
local parm="$1"
|
||||||
local val="$2"
|
local val="$2"
|
||||||
run_remote_python "
|
local sid="$$"
|
||||||
import hou
|
local code
|
||||||
|
code=$(cat <<PYEOF
|
||||||
nodes = hou.selectedNodes()
|
nodes = hou.selectedNodes()
|
||||||
if not nodes:
|
if not nodes:
|
||||||
print('No nodes selected.')
|
msg = "No nodes selected."
|
||||||
else:
|
else:
|
||||||
p = nodes[0].parm('$parm')
|
p = nodes[0].parm("PARM_PLACEHOLDER")
|
||||||
if p is None:
|
if p is None:
|
||||||
print(f'Parameter \"$parm\" not found on {nodes[0].path()}')
|
msg = f'Parameter "PARM_PLACEHOLDER" not found on {nodes[0].path()}'
|
||||||
else:
|
else:
|
||||||
p.set('$val')
|
p.set("VAL_PLACEHOLDER")
|
||||||
print(f'{nodes[0].path()}.{p.name()} = {p.eval()} ✓')
|
msg = f'{nodes[0].path()}.{p.name()} = {p.eval()} OK'
|
||||||
"
|
with open('/tmp/hhb_out_PLACEHOLDER_SID.txt', 'w') as f:
|
||||||
|
f.write(msg)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
code="${code//PARM_PLACEHOLDER/$parm}"
|
||||||
|
code="${code//VAL_PLACEHOLDER/$val}"
|
||||||
|
code="${code//PLACEHOLDER_SID/$sid}"
|
||||||
|
run_houdini_python "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_get_inputs() {
|
cmd_get_inputs() {
|
||||||
run_remote_python '
|
local sid="$$"
|
||||||
import hou
|
local code
|
||||||
|
code=$(cat <<'PYEOF'
|
||||||
|
lines = []
|
||||||
nodes = hou.selectedNodes()
|
nodes = hou.selectedNodes()
|
||||||
if not nodes:
|
if not nodes:
|
||||||
print("No nodes selected.")
|
lines.append("No nodes selected.")
|
||||||
else:
|
else:
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
inputs = n.inputConnectors()
|
lines.append(f"\n{n.path()} [{n.type().name()}]")
|
||||||
print(f"\n{n.path()} [{n.type().name()}]")
|
for i in range(len(n.inputConnectors())):
|
||||||
for i, conn in enumerate(inputs):
|
conns = n.inputConnections(i)
|
||||||
in_nodes = n.inputConnections(i) if hasattr(n, "inputConnections") else []
|
if conns:
|
||||||
if in_nodes:
|
for c in conns:
|
||||||
for c in in_nodes:
|
lines.append(f" input {i} <- {c.inputNode().path()} [{c.inputNode().type().name()}]")
|
||||||
print(f" input {i} ← {c.inputNode().path()} [{c.inputNode().type().name()}]")
|
|
||||||
else:
|
else:
|
||||||
print(f" input {i} ← (none)")
|
lines.append(f" input {i} <- (none)")
|
||||||
'
|
with open('/tmp/hhb_out_PLACEHOLDER.txt', 'w') as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
code="${code//PLACEHOLDER/$sid}"
|
||||||
|
run_houdini_python "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user