#3581·mujoco

Studio: an exception in one plugin handler terminates the viewer

Author: larrygao001Created Sep 14, 2026Updated Sep 17, 2026
Labelsbug

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
# python/mujoco/experimental/studio/plugin_registry.py
for _, handler in message_handlers:
    if handler(message):
        return

So 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 code

Viewer 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 +0x264946

The 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:

python
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

  1. pip install mujoco==3.13.0
  2. Save minimal.xml and repro.py below into the same directory.
  3. On Linux: python repro.py. The viewer window opens and the process dies with SIGABRT on the first GUI frame.
  4. On macOS, launch_passive cannot 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:

xml
<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:

python
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 reached

Removing the raise from on_build_gui makes the same script run to completion, which isolates the exception as the cause.

Confirmations