SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
hc: stop the leader chord crashing Houdini from a timer thread
chord() stored the hou.NetworkEditor it was handed as a dict key and then
expired that entry from a threading.Timer. One second later the timer
thread ran _pending.get(editor), which hashes and compares HOM objects.
Two things are wrong with that. HOM is not thread-safe, so no hou call
belongs on a Timer thread at all. And Houdini rebuilds these wrappers on
every script fire, so the key held in the dict is stale by the time the
timer looks at it: SWIG reads .this off a pane tab whose C++ object is
gone and dereferences it. That is a SIGSEGV, caught by Houdini's crash
handler, which then wedges the process -- the UI stops responding, Ctrl+S
silently does nothing, and the window ignores WM_DELETE_WINDOW, which in
turn cancels a compositor logout.
_wrap_EnumValue___eq__ <_hou.so>
SWIG_Python_ConvertPtrAndOwn / SWIG_Python_GetSwigThis
PyObject_GetAttr / _Py_slot_tp_getattr_hook.cold
...
thread_run <libpython3.13.so.1.0>
Key on editor.name() instead -- a plain string, stable across the wrappers
Houdini recreates -- and drop the timer entirely. It only ever deleted an
entry that the elapsed-time check already ignores, so a leftover is
harmless and the next keypress overwrites it. No thread, and no HOM object
held past the callback that produced it.
Co-Authored-By: Claude Opus 5 <[email protected]>
python3.13libs/hc/hcleader.py | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/python3.13libs/hc/hcleader.py b/python3.13libs/hc/hcleader.py
new file mode 100644
index 0000000..6c124b2
--- /dev/null
+++ b/python3.13libs/hc/hcleader.py
@@ -0,0 +1,35 @@
+import time
+
+
+_TIMEOUT = 1.0
+_pending = {} # {pane tab name: timestamp}
+
+
+def chord(editor, action_fn):
+ """Vim-style leader chord. Returns True if action was executed, False if
+ this was the first keypress (leader registered, waiting for second).
+
+ ``editor`` is a ``hou.NetworkEditor`` (or any ``hou.PaneTab``).
+ Houdini creates fresh Python wrappers each time a script fires, so
+ ``id()`` is unreliable and the wrapper itself must not be kept: holding
+ one past the current callback leaves a dangling SWIG pointer, and reading
+ it later segfaults Houdini. ``name()`` is a plain string that stays stable
+ across those wrappers, so that is what gets stored.
+
+ Nothing here may touch HOM off the main thread. An earlier version
+ expired entries from a ``threading.Timer``, whose callback compared the
+ stored ``hou.PaneTab`` keys from a background thread -- HOM is not
+ thread-safe, and that crashed Houdini with SIGSEGV inside SWIG's ``__eq__``.
+ The elapsed-time check below already ignores a stale entry, so no timer is
+ needed: a leftover entry is harmless and the next keypress overwrites it."""
+ now = time.monotonic()
+ key = editor.name()
+ old_ts = _pending.get(key)
+
+ if old_ts and (now - old_ts) < _TIMEOUT:
+ _pending.pop(key, None)
+ action_fn()
+ return True
+
+ _pending[key] = now
+ return False