git.lucas.co / hou-control
SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git

commitca10f814b3aa88c93acf0211481aca40e59214cd
parentaf0605e4cf
authorLucas Galante <[email protected]>
date2026-09-11 12:20
hc: start the agent bridge before the UI setup that can kill it

hrpyc.start_server was the last statement in uiready.py, behind the hotkey
load, the node recolour, the desktop switch, the stowbar toggle, the split
handles and the status bar. Any exception in those aborted the module and took
the bridge with it, silently, because nothing in the file reported. A session
found earlier today with Houdini running and nothing on port 18811 is what
that looks like from outside.

It is the wrong way round. The bridge is how you inspect a session whose
startup went wrong, so it cannot be the first thing a bad startup destroys.
It now starts immediately after the imports, with the main-thread pump
installed first as before -- rpyc answers on a connection thread and HOM is
not thread-safe.

The startup steps that follow now run through _step(), which prints a
traceback and carries on. They are independent: a broken hc_hotkeys.json
should not also cost the status bar, and a step that fails silently leaves a
session half set up for no visible reason.

Verified end to end against a real hrpyc server in a headless hython:
hrun 'hou.applicationVersionString()' returns '22.0.429', and resolves from
any working directory.

Co-Authored-By: Claude Opus 5 <[email protected]>

 python3.13libs/uiready.py | 83 +++++++++++++++++++++++++++++++++--------------
 1 file changed, 58 insertions(+), 25 deletions(-)

diff --git a/python3.13libs/uiready.py b/python3.13libs/uiready.py
index fcb8f40..b89bb2d 100644
--- a/python3.13libs/uiready.py
+++ b/python3.13libs/uiready.py
@@ -1,14 +1,54 @@
 import hou
 import hrpyc
+import sys
+import traceback
+
 from hc import HCSession, HCSettings
 
-hc_session = HCSession()
+# Start the agent bridge before anything else.
+#
+# It used to be the last statement in this file, behind every piece of startup
+# below, so one exception in hotkey loading or node recolouring or the status
+# bar aborted the module and took the bridge down with it -- silently, since
+# nothing here reports. That is exactly backwards: the bridge is how you
+# inspect a session whose startup went wrong, so it must not depend on that
+# startup having gone right.
+#
+# rpyc's ThreadedServer runs every request on a connection thread, and HOM is
+# not thread-safe -- a hou call off the main thread segfaults Houdini inside
+# SWIG. Install the main-thread pump before the server can accept anything;
+# see hc.hcmainthread and the runner in houdini-agent/bridge.py.
+from hc import hcmainthread
 
-hc_session.reloadHotkeys()
-hc_session.initializeNetworkEditorsDeferred()
-hc_session.updateNodeColors()
+hcmainthread.install()
+try:
+    # use_thread=True by default, so this returns rather than blocking startup.
+    hrpyc.start_server(port=18811)
+except OSError:
+    pass  # already bound, e.g. a second Houdini on the same port
 
-import sys
+
+def _step(label, fn, *args):
+    """Run one startup step; report a failure instead of aborting the rest.
+
+    These are independent -- a broken hotkey file should not cost you the
+    status bar -- and a printed traceback beats a session that is half set up
+    for no visible reason.
+    """
+    try:
+        fn(*args)
+        return True
+    except Exception:
+        print(f"[uiready] {label} failed:", file=sys.stderr)
+        traceback.print_exc()
+        return False
+
+
+hc_session = HCSession()
+
+_step("reloadHotkeys", hc_session.reloadHotkeys)
+_step("initializeNetworkEditorsDeferred", hc_session.initializeNetworkEditorsDeferred)
+_step("updateNodeColors", hc_session.updateNodeColors)
 
 desktop_mode = HCSettings().desktopMode()
 
@@ -69,24 +109,17 @@ if desktop_mode == "detached":
     hou.ui.addEventLoopCallback(_safeDeferredInit)
 else:
     # Attached mode runs at module level (has been stable)
-    for d in hou.ui.desktops():
-        if d.name() == "hc_attached":
-            d.setAsCurrent()
-            break
-    hc_session.toggleStowbars()
-    hc_session.splitHandles().show()
-    from hc import HCStatusBar
-    HCStatusBar().show()
-
-# rpyc's ThreadedServer runs every request on a connection thread, and HOM
-# is not thread-safe -- a hou call off the main thread segfaults Houdini
-# inside SWIG. Pump a main-thread dispatch queue before the server can accept
-# anything, so the agent bridge has somewhere to hand its work; see
-# hc.hcmainthread and the runner in houdini-agent/bridge.py.
-from hc import hcmainthread
-hcmainthread.install()
+    def _setDesktop():
+        for d in hou.ui.desktops():
+            if d.name() == "hc_attached":
+                d.setAsCurrent()
+                return
 
-try:
-    hrpyc.start_server(port=18811)
-except OSError:
-    pass
+    def _showStatusBar():
+        from hc import HCStatusBar
+        HCStatusBar().show()
+
+    _step("set hc_attached desktop", _setDesktop)
+    _step("toggleStowbars", hc_session.toggleStowbars)
+    _step("splitHandles", lambda: hc_session.splitHandles().show())
+    _step("statusBar", _showStatusBar)