Studio: an exception in one plugin handler terminates the viewer
Intro
I use MuJoCo for headless physics simulation and offscreen rendering distributed across many workers, and I have been writing a Studio plugin that adds a panel to the viewer.
My setup
Observed on both:
- MuJoCo 3.13.0 (PyPI wheel), Python 3.11.5, arm64, macOS 26.6.2
- MuJoCo 3.13.0 (PyPI wheel), Python 3.14.4, x86_64, Ubuntu 26.04
API: Python.
What's happening? What did you expect?
PluginRegistry.dispatch calls handlers without isolation:
# python/mujoco/experimental/studio/plugin_registry.py
for _, handler in message_handlers:
if handler(message):
returnSo an exception raised in any plugin handler propagates out of the viewer loop and terminates the application. One mistake in one plugin takes down MuJoCo and every other registered plugin with it.
I expected a failing plugin to be reported and, ideally, isolated, since third party plugin code is the intended use of this API and a typo in one is the expected failure.
Two presentations, depending on which thread the viewer runs on
Viewer on the main thread (blocking launcher): a readable Python traceback, process exits 1.
Traceback (most recent call last):
...
File ".../studio/launch_native.py", line 38, in run_native_viewer
viewer_protocol.run_viewer_loop(viewer)
File ".../studio/viewer_protocol.py", line 283, in run_viewer_loop
viewer.dispatch(messages.BuildGuiEvent())
ValueError: a deliberate typo in plugin codeViewer on a worker thread (the default launch_passive path, observed on
Linux): SIGABRT with a native stack and no Python frame, because the exception
escapes into the C++ callback that invoked it.
Current thread 0x... [repro.py] (most recent call first):
Garbage-collecting
<no Python frame>
Current thread's C stack trace (most recent call first):
Binary file ".../python", at _Py_DumpStack+0x4a
...
Binary file ".../libc.so.6", at abort+0x27
Binary file ".../studio/native_viewer_cc...so", at +0x264946The second is considerably harder to diagnose, since nothing indicates which plugin
failed or why. In my case the underlying cause was passing arguments matching the
wrong imgui.MenuItem overload, so a TypeError from unpacking a bool, and the
only signal was a C++ abort.
Suggested fix
Catch per handler in the dispatch loop, so one plugin cannot take down the application or its siblings, and attribute the failure:
for _, handler in message_handlers:
try:
if handler(message):
return
except Exception:
logging.exception("plugin handler %r failed handling %s",
handler, type(message).__name__)Swallowing errors has its own cost, so alternatives worth considering: keep the
raise but wrap it with the plugin name and message type, or disable a handler after
it raises, since a BuildGuiEvent handler runs every frame and will otherwise
flood the log.
Naming the responsible plugin would help most. Even without isolation, a message saying which handler raised would have saved a long detour on the abort case.
Steps for reproduction
pip install mujoco==3.13.0- Save
minimal.xmlandrepro.pybelow into the same directory. - On Linux:
python repro.py. The viewer window opens and the process dies withSIGABRTon the first GUI frame. - On macOS,
launch_passivecannot open a window at all (filed separately), so use a blocking main-thread launcher to reproduce; the failure then presents as the Python traceback shown above.
Minimal model for reproduction
Loadable as-is, no binary assets required:
<mujoco model="studio_repro">
<option timestep="0.002"/>
<worldbody>
<light pos="0 0 2"/>
<camera name="fixed" pos="0.8 -0.8 0.6" xyaxes="1 1 0 -0.4 0.4 1"/>
<geom name="floor" type="plane" size="2 2 0.05" rgba="0.4 0.45 0.5 1"/>
<body name="box" pos="0 0 0.3">
<freejoint name="box_free"/>
<geom name="box" type="box" size="0.05 0.05 0.05" rgba="0.8 0.2 0.2 1"/>
</body>
</worldbody>
</mujoco>Code required for reproduction
repro.py:
import time
import mujoco
from mujoco.experimental.studio import (launch_passive, messages, step_control,
viewer_app, viewer_protocol)
class Broken:
"""A plugin with an ordinary mistake in its GUI handler."""
name = "broken"
@messages.handler
def on_build_gui(self, event: messages.BuildGuiEvent) -> None:
raise ValueError("a deliberate typo in plugin code")
model = mujoco.MjModel.from_xml_path("minimal.xml")
data = mujoco.MjData(model)
handle = launch_passive.launch_passive(
viewer_protocol.ViewerConfig(title="plugin exception repro", width=900, height=600),
viewer_plugins=[viewer_app.ViewerApp(), Broken()],
sim_plugins=[step_control.StepControl()],
)
handle.send_to_viewer(messages.ModelEvent(model=model, path="minimal.xml"))
for _ in range(500):
model, data = handle.sync(model, data)
time.sleep(0.02)
print("finished normally", flush=True) # not reachedRemoving the raise from on_build_gui makes the same script run to completion,
which isolates the exception as the cause.
Confirmations
- I searched the latest documentation thoroughly before posting.
- I searched previous Issues and Discussions, I am certain this has not been raised before.
Source: google-deepmind/mujoco